Add Pandemonium
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 16 Feb 2021 21:42:26 +0000 (22:42 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 16 Feb 2021 21:42:26 +0000 (22:42 +0100)
29 files changed:
client/public/images/pieces/Pandemonium/ba.svg [new symlink]
client/public/images/pieces/Pandemonium/bc.svg [new symlink]
client/public/images/pieces/Pandemonium/bd.svg [new symlink]
client/public/images/pieces/Pandemonium/bg.svg [new symlink]
client/public/images/pieces/Pandemonium/bh.svg [new symlink]
client/public/images/pieces/Pandemonium/bm.svg [new symlink]
client/public/images/pieces/Pandemonium/bs.svg [new symlink]
client/public/images/pieces/Pandemonium/bw.svg [new symlink]
client/public/images/pieces/Pandemonium/wa.svg [new symlink]
client/public/images/pieces/Pandemonium/wc.svg [new symlink]
client/public/images/pieces/Pandemonium/wd.svg [new symlink]
client/public/images/pieces/Pandemonium/wg.svg [new symlink]
client/public/images/pieces/Pandemonium/wh.svg [new symlink]
client/public/images/pieces/Pandemonium/wm.svg [new symlink]
client/public/images/pieces/Pandemonium/ws.svg [new symlink]
client/public/images/pieces/Pandemonium/ww.svg [new symlink]
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/translations/rules/Pandemonium/en.pug [new file with mode: 0644]
client/src/translations/rules/Pandemonium/es.pug [new file with mode: 0644]
client/src/translations/rules/Pandemonium/fr.pug [new file with mode: 0644]
client/src/translations/variants/en.pug
client/src/translations/variants/es.pug
client/src/translations/variants/fr.pug
client/src/variants/Omega.js
client/src/variants/Pandemonium.js [new file with mode: 0644]
client/src/variants/Shogi.js
server/db/populate.sql

diff --git a/client/public/images/pieces/Pandemonium/ba.svg b/client/public/images/pieces/Pandemonium/ba.svg
new file mode 120000 (symlink)
index 0000000..c301d86
--- /dev/null
@@ -0,0 +1 @@
+../bq.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/bc.svg b/client/public/images/pieces/Pandemonium/bc.svg
new file mode 120000 (symlink)
index 0000000..78e54f8
--- /dev/null
@@ -0,0 +1 @@
+../Perfect/bs.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/bd.svg b/client/public/images/pieces/Pandemonium/bd.svg
new file mode 120000 (symlink)
index 0000000..09e6ea3
--- /dev/null
@@ -0,0 +1 @@
+../Alice/bu.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/bg.svg b/client/public/images/pieces/Pandemonium/bg.svg
new file mode 120000 (symlink)
index 0000000..c301d86
--- /dev/null
@@ -0,0 +1 @@
+../bq.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/bh.svg b/client/public/images/pieces/Pandemonium/bh.svg
new file mode 120000 (symlink)
index 0000000..b30a26a
--- /dev/null
@@ -0,0 +1 @@
+../Alice/bc.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/bm.svg b/client/public/images/pieces/Pandemonium/bm.svg
new file mode 120000 (symlink)
index 0000000..d3aaacf
--- /dev/null
@@ -0,0 +1 @@
+../Perfect/be.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/bs.svg b/client/public/images/pieces/Pandemonium/bs.svg
new file mode 120000 (symlink)
index 0000000..1200186
--- /dev/null
@@ -0,0 +1 @@
+../Alice/bo.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/bw.svg b/client/public/images/pieces/Pandemonium/bw.svg
new file mode 120000 (symlink)
index 0000000..c301d86
--- /dev/null
@@ -0,0 +1 @@
+../bq.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/wa.svg b/client/public/images/pieces/Pandemonium/wa.svg
new file mode 120000 (symlink)
index 0000000..aed155f
--- /dev/null
@@ -0,0 +1 @@
+../wq.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/wc.svg b/client/public/images/pieces/Pandemonium/wc.svg
new file mode 120000 (symlink)
index 0000000..b0eb8b9
--- /dev/null
@@ -0,0 +1 @@
+../Perfect/ws.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/wd.svg b/client/public/images/pieces/Pandemonium/wd.svg
new file mode 120000 (symlink)
index 0000000..c1403b3
--- /dev/null
@@ -0,0 +1 @@
+../Alice/wu.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/wg.svg b/client/public/images/pieces/Pandemonium/wg.svg
new file mode 120000 (symlink)
index 0000000..aed155f
--- /dev/null
@@ -0,0 +1 @@
+../wq.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/wh.svg b/client/public/images/pieces/Pandemonium/wh.svg
new file mode 120000 (symlink)
index 0000000..d23af91
--- /dev/null
@@ -0,0 +1 @@
+../Alice/wc.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/wm.svg b/client/public/images/pieces/Pandemonium/wm.svg
new file mode 120000 (symlink)
index 0000000..f47feb0
--- /dev/null
@@ -0,0 +1 @@
+../Perfect/we.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/ws.svg b/client/public/images/pieces/Pandemonium/ws.svg
new file mode 120000 (symlink)
index 0000000..4a85712
--- /dev/null
@@ -0,0 +1 @@
+../Alice/wo.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Pandemonium/ww.svg b/client/public/images/pieces/Pandemonium/ww.svg
new file mode 120000 (symlink)
index 0000000..aed155f
--- /dev/null
@@ -0,0 +1 @@
+../wq.svg
\ No newline at end of file
index ca3b13a..4d93adc 100644 (file)
@@ -269,6 +269,7 @@ export const translations = {
   "New fairy pieces": "New fairy pieces",
   "No paralyzed pieces": "No paralyzed pieces",
   "No-check mode": "No-check mode",
   "New fairy pieces": "New fairy pieces",
   "No paralyzed pieces": "No paralyzed pieces",
   "No-check mode": "No-check mode",
+  "Noise and confusion": "Noise and confusion",
   "Non-conformism and utopia": "Non-conformism and utopia",
   "Occupy the enemy palace": "Occupy the enemy palace",
   "Paralyzed pieces": "Paralyzed pieces",
   "Non-conformism and utopia": "Non-conformism and utopia",
   "Occupy the enemy palace": "Occupy the enemy palace",
   "Paralyzed pieces": "Paralyzed pieces",
index 30095bf..a07a5c1 100644 (file)
@@ -269,6 +269,7 @@ export const translations = {
   "New fairy pieces": "Nuevas piezas magicas",
   "No paralyzed pieces": "No piezas paralizadas",
   "No-check mode": "Modo sin jaque",
   "New fairy pieces": "Nuevas piezas magicas",
   "No paralyzed pieces": "No piezas paralizadas",
   "No-check mode": "Modo sin jaque",
+  "Noise and confusion": "Ruido y confusión",
   "Non-conformism and utopia": "No-conformismo y utopía",
   "Occupy the enemy palace": "Ocupar el palacio enemigo",
   "Paralyzed pieces": "Piezas paralizadas",
   "Non-conformism and utopia": "No-conformismo y utopía",
   "Occupy the enemy palace": "Ocupar el palacio enemigo",
   "Paralyzed pieces": "Piezas paralizadas",
index 0ebc817..247b46f 100644 (file)
@@ -269,6 +269,7 @@ export const translations = {
   "New fairy pieces": "Nouvelles pièces féériques",
   "No paralyzed pieces": "Pas de pièces paralysées",
   "No-check mode": "Mode sans échec",
   "New fairy pieces": "Nouvelles pièces féériques",
   "No paralyzed pieces": "Pas de pièces paralysées",
   "No-check mode": "Mode sans échec",
+  "Noise and confusion": "Bruit et confusion",
   "Non-conformism and utopia": "Non-conformisme et utopie",
   "Occupy the enemy palace": "Occuper le palais ennemi",
   "Paralyzed pieces": "Pièces paralysées",
   "Non-conformism and utopia": "Non-conformisme et utopie",
   "Occupy the enemy palace": "Occuper le palais ennemi",
   "Paralyzed pieces": "Pièces paralysées",
diff --git a/client/src/translations/rules/Pandemonium/en.pug b/client/src/translations/rules/Pandemonium/en.pug
new file mode 100644 (file)
index 0000000..f5d518d
--- /dev/null
@@ -0,0 +1,63 @@
+p.boxed
+  | All pieces can promote. Captured units can be dropped later.
+  | 10x10 board. Some new pieces.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR:
+  figcaption Initial deterministic position.
+
+p.
+  At the very first move, players may decide to swap positions of adjacent
+  knights and bishops. Either on both sides, or on one only. To bypass this
+  step (or end it after a first swap), move your king to the opponent's king.
+
+figure.showPieces.text-center
+  img(src="/images/pieces/Pandemonium/wc.svg" style="width:60px")
+  img(src="/images/pieces/Pandemonium/bm.svg" style="width:60px")
+  figcaption Cardinal = Bishop + Knight, Marshal = Rook + Knight.
+
+p.
+  Known pieces move as usual, with one exception: the pawn only promotes
+  to a queen (named "Gilding" in this game). Additionally, all pieces can
+  promote &mdash; except queen and king:
+ul
+  li Rook promotes into Dragon = Rook + King.
+  li Knight promotes into Scepter = Knight + King.
+  li Bishop promotes into Horse = Bishop + King.
+  li Cardinal promotes into Queen (called "Whole" here).
+  li Marshal promotes into Queen (called "Apricot" here).
+p.
+  All these promotions are optional. They are available after a move ending
+  at or starting from the last rank.
+
+p.
+  Each captured piece is first returned to its unpromoted version (if
+  applicable), and then added to a reserve. It can be dropped later
+  in place of a move.
+
+h3 Some details
+
+p.
+  Pawns can initially advance three squares.
+  On the third rank, they can still advance two squares.
+  A dropped pawn cannot give checkmate.
+
+p.
+  While castling, the king moves three squares lateraly.
+  Castling is possible even if the king or the rooks moved.
+  However, it can only be done once.
+
+p.
+  If after a move both kings are facing each other (on a rank or file)
+  without intervening pieces, then the player who made the move loses.
+
+h3 More information
+
+p
+  | See the 
+  a(href="https://www.chessvariants.com/rules/pandemonium")
+    | chessvariants page
+  | .
+
+p Inventor: Daphne Snowmoon (2020).
diff --git a/client/src/translations/rules/Pandemonium/es.pug b/client/src/translations/rules/Pandemonium/es.pug
new file mode 100644 (file)
index 0000000..8aed7fe
--- /dev/null
@@ -0,0 +1,65 @@
+p.boxed
+  | Todas las piezas pueden promocionarse. Las unidades capturadas son
+  | en paracaídas más tarde. Tablero de ajedrez 10x10. Algunas piezas nuevas.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR:
+  figcaption Posición inicial determinista.
+
+p.
+  En el primer movimiento, los jugadores pueden decidir intercambiar
+  posiciones de caballos y alfiles adyacentes. O en ambos lados o solo en uno.
+  Para omitir este paso (o finalizar después de un primer intercambio),
+  lleva a tu rey al rey contrario.
+
+figure.showPieces.text-center
+  img(src="/images/pieces/Pandemonium/wc.svg" style="width:60px")
+  img(src="/images/pieces/Pandemonium/bm.svg" style="width:60px")
+  figcaption Cardenal = Alfil + Caballo, Mariscal = Torre + Caballo.
+
+p.
+  Las piezas conocidas se mueven como de costumbre, con una excepción:
+  los peones solo se promueven en una reina (llamado "Gilding" en este juego).
+  Además, todas las piezas pueden promocionarse &mdash; excepto rey y reina:
+ul
+  li Tower ascendido a Dragón = Torre + Rey.
+  li Caballo ascendido a Cetro = Cavalier + Rey.
+  li Alfil ascendido a Jinete = Alfil + Rey.
+  li Cardenal ascendido a Dama (llamado "Whole" aquí).
+  li Mariscal ascendió a Dama (aquí se llama "Apricot").
+p.
+  Todas estas promociones son opcionales. Están disponibles después de un
+  movimiento que termina o comienza en la última fila.
+
+p.
+  Cada pieza capturada se devuelve primero a su forma no promocionada (el caso
+  aplicable), luego se agrega a una reserva. Se puede lanzar en paracaídas
+  más tarde en lugar de una jugada.
+
+h3 Algunos detalles.
+
+p.
+  Los peones pueden avanzar inicialmente tres espacios.
+  En la tercera fila, todavía pueden avanzar dos espacios.
+  Un peón lanzado en paracaídas no puede dar jaque mate.
+
+p.
+  Durante el enroque, el rey mueve tres casillas hacia los lados.
+  El enroque es posible incluso si el rey o las torres se han movido.
+  Sin embargo, solo se puede realizar una vez.
+
+p.
+  Si después de una jugada los dos reyes se encuentran cara a cara (en fila
+  o una columna) sin piezas intermedias, entonces el jugador que hizo
+  el movimiento pierde.
+
+h3 Más información
+
+p
+  | Ver la 
+  a(href="https://www.chessvariants.com/rules/pandemonium")
+    | página chessvariants
+  | .
+
+p Inventor: Daphne Snowmoon (2020).
diff --git a/client/src/translations/rules/Pandemonium/fr.pug b/client/src/translations/rules/Pandemonium/fr.pug
new file mode 100644 (file)
index 0000000..bdd6d3b
--- /dev/null
@@ -0,0 +1,65 @@
+p.boxed
+  | Toutes les pièces peuvent être promues. Les unités capturées sont
+  | parachutées plus tard. Échiquier 10x10. Quelques nouvelles pièces.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR:
+  figcaption Position initiale déterministe.
+
+p.
+  Au tout premier coup, les joueurs peuvent décider d'échanger les positions
+  de cavaliers et fous adjacents. Soit des deux côtés, soit d'un seul.
+  Pour sauter cette étape (ou terminer après un premier échange),
+  amenez votre roi sur le roi adverse.
+
+figure.showPieces.text-center
+  img(src="/images/pieces/Pandemonium/wc.svg" style="width:60px")
+  img(src="/images/pieces/Pandemonium/bm.svg" style="width:60px")
+  figcaption Cardinal = Fou + Cavalier, Maréchal = Tour + Cavalier.
+
+p.
+  Les pièces connues se déplacent comme d'habitude, à une exception près :
+  les pions ne se promeuvent qu'en dame (appelée "Gilding" dans ce jeu).
+  De plus, toutes les pièces peuvent se promouvoir &mdash; sauf roi et dame :
+ul
+  li Tour promue en Dragon = Tour + Roi.
+  li Cavalier promu en Sceptre = Cavalier + Roi.
+  li Fou promu en Cheval = Fou + Roi.
+  li Cardinal promu en Dame (appelée "Whole" ici).
+  li Maréchal promu en Dame (appelée "Apricot" ici).
+p.
+  Toutes ces promotions sont optionnelles. Elles sont disponibles après un
+  coup terminant ou commençant sur la dernière rangée.
+
+p.
+  Chaque pièce capturée est d'abord ramenée à sa forme non promue (le cas
+  échéant), puis ajoutée à une réserve. Elle peut être parachutée plus tard
+  à la place d'un coup.
+
+h3 Quelques détails.
+
+p.
+  Les pions peuvent initialement avancer de trois cases.
+  Sur la troisième rangée, ils peuvent encore avancer de deux cases.
+  Un pion parachuté ne peut pas donner échec et mat.
+
+p.
+  Lors du roque, le roi se déplace de trois cases latéralement.
+  Le roque est possible même si le roi ou les tours ont bougé.
+  Cependant, il ne peut être exécuté qu'une fois.
+
+p.
+  Si après un coup les deux rois se retrouvent face à face (sur une rangée
+  ou une colonne) sans pièces intermédiaires, alors le joueur ayant effectué
+  le coup perd.
+
+h3 Plus d'information
+
+p
+  | Voir la 
+  a(href="https://www.chessvariants.com/rules/pandemonium")
+    | page chessvariants
+  | .
+
+p Inventeur : Daphne Snowmoon (2020).
index eed4c98..966f96f 100644 (file)
@@ -471,6 +471,7 @@ p.
     "Iceage",
     "Kingsmaker",
     "Magnetic",
     "Iceage",
     "Kingsmaker",
     "Magnetic",
+    "Pandemonium",
     "Refusal",
     "Relayup",
     "Rollerball",
     "Refusal",
     "Relayup",
     "Rollerball",
index 01efa97..90196f6 100644 (file)
@@ -481,6 +481,7 @@ p.
     "Iceage",
     "Kingsmaker",
     "Magnetic",
     "Iceage",
     "Kingsmaker",
     "Magnetic",
+    "Pandemonium",
     "Refusal",
     "Relayup",
     "Rollerball",
     "Refusal",
     "Relayup",
     "Rollerball",
index 3566c88..7729aaf 100644 (file)
@@ -479,6 +479,7 @@ p.
     "Iceage",
     "Kingsmaker",
     "Magnetic",
     "Iceage",
     "Kingsmaker",
     "Magnetic",
+    "Pandemonium",
     "Refusal",
     "Relayup",
     "Rollerball",
     "Refusal",
     "Relayup",
     "Rollerball",
index a4276ed..a688a2e 100644 (file)
@@ -309,18 +309,12 @@ export class OmegaRules extends ChessRules {
     return moves;
   }
 
     return moves;
   }
 
-  addPawnMoves([x1, y1], [x2, y2], moves, promotions) {
-    let finalPieces = [V.PAWN];
+  addPawnMoves([x1, y1], [x2, y2], moves) {
     const color = this.turn;
     const lastRank = (color == "w" ? 1 : V.size.x - 2);
     const color = this.turn;
     const lastRank = (color == "w" ? 1 : V.size.x - 2);
-    if (x2 == lastRank) {
-      // promotions arg: special override for Hiddenqueen variant
-      if (!!promotions) finalPieces = promotions;
-      else if (!!V.PawnSpecs.promotions) finalPieces = V.PawnSpecs.promotions;
-    }
-    let tr = null;
+    const finalPieces = (x2 == lastRank ? V.PawnSpecs.promotions : [V.PAWN]);
     for (let piece of finalPieces) {
     for (let piece of finalPieces) {
-      tr = (piece != V.PAWN ? { c: color, p: piece } : null);
+      const tr = (piece != V.PAWN ? { c: color, p: piece } : null);
       moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
     }
   }
       moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
     }
   }
diff --git a/client/src/variants/Pandemonium.js b/client/src/variants/Pandemonium.js
new file mode 100644 (file)
index 0000000..34dc77e
--- /dev/null
@@ -0,0 +1,785 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class PandemoniumRules extends ChessRules {
+
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      {
+        threeSquares: true,
+        promotions: [V.GILDING]
+      }
+    );
+  }
+
+  static get GILDING() {
+    return "g";
+  }
+
+  static get SCEPTER() {
+    return "s";
+  }
+
+  static get HORSE() {
+    return "h";
+  }
+
+  static get DRAGON() {
+    return "d";
+  }
+
+  static get CARDINAL() {
+    return "c";
+  }
+
+  static get WHOLE() {
+    return "w";
+  }
+
+  static get MARSHAL() {
+    return "m";
+  }
+
+  static get APRICOT() {
+    return "a";
+  }
+
+  static get PIECES() {
+    return (
+      ChessRules.PIECES.concat([
+        V.GILDING, V.SCEPTER, V.HORSE, V.DRAGON,
+        V.CARDINAL, V.WHOLE, V.MARSHAL, V.APRICOT])
+    );
+  }
+
+  getPpath(b) {
+    const prefix = (ChessRules.PIECES.includes(b[1]) ? "" : "Pandemonium/");
+    return prefix + b;
+  }
+
+  static get size() {
+    return { x: 10, y: 10};
+  }
+
+  getColor(i, j) {
+    if (i >= V.size.x) return i == V.size.x ? "w" : "b";
+    return this.board[i][j].charAt(0);
+  }
+
+  getPiece(i, j) {
+    if (i >= V.size.x) return V.RESERVE_PIECES[j];
+    return this.board[i][j].charAt(1);
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    // Sub-turn is useful only at first move...
+    this.subTurn = 1;
+    // Also init reserves (used by the interface to show landable pieces)
+    const reserve =
+      V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
+    this.reserve = {
+      w: {
+        [V.PAWN]: reserve[0],
+        [V.ROOK]: reserve[1],
+        [V.KNIGHT]: reserve[2],
+        [V.BISHOP]: reserve[3],
+        [V.QUEEN]: reserve[4],
+        [V.CARDINAL]: reserve[5],
+        [V.MARSHAL]: reserve[6],
+      },
+      b: {
+        [V.PAWN]: reserve[7],
+        [V.ROOK]: reserve[8],
+        [V.KNIGHT]: reserve[9],
+        [V.BISHOP]: reserve[10],
+        [V.QUEEN]: reserve[11],
+        [V.CARDINAL]: reserve[12],
+        [V.MARSHAL]: reserve[13]
+      }
+    };
+  }
+
+  static IsGoodEnpassant(enpassant) {
+    if (enpassant != "-") {
+      const squares = enpassant.split(",");
+      if (squares.length > 2) return false;
+      for (let sq of squares) {
+        if (!sq.match(/[a-j0-9]/)) return false;
+      }
+    }
+    return true;
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // Check reserves
+    if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{14,14}$/))
+      return false;
+    return true;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { reserve: fenParts[5] }
+    );
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getReserveFen();
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getReserveFen();
+  }
+
+  getReserveFen() {
+    let counts = new Array(14);
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+      counts[i] = this.reserve["w"][V.RESERVE_PIECES[i]];
+      counts[7 + i] = this.reserve["b"][V.RESERVE_PIECES[i]];
+    }
+    return counts.join("");
+  }
+
+  setFlags(fenflags) {
+    // white a-castle, h-castle, king pos, then same for black.
+    this.castleFlags = { w: [-1, -1, -1], b: [-1, -1, -1] };
+    for (let i = 0; i < 6; i++) {
+      this.castleFlags[i < 3 ? "w" : "b"][i % 3] =
+        V.ColumnToCoord(fenflags.charAt(i));
+    }
+  }
+
+  static GenRandInitFen(randomness) {
+    // No randomization here for now (but initial setup choice)
+    return (
+      "rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR " +
+      "w 0 ajeaje - 00000000000000"
+    );
+    // TODO later: randomization too --> 2 bishops, not next to each other.
+    // then knights next to bishops. Then other pieces (...).
+  }
+
+  getEnpassantFen() {
+    const L = this.epSquares.length;
+    if (!this.epSquares[L - 1]) return "-"; //no en-passant
+    let res = "";
+    this.epSquares[L - 1].forEach(sq => {
+      res += V.CoordsToSquare(sq) + ",";
+    });
+    return res.slice(0, -1); //remove last comma
+  }
+
+  getEpSquare(moveOrSquare) {
+    if (!moveOrSquare) return undefined;
+    if (typeof moveOrSquare === "string") {
+      const square = moveOrSquare;
+      if (square == "-") return undefined;
+      let res = [];
+      square.split(",").forEach(sq => {
+        res.push(V.SquareToCoords(sq));
+      });
+      return res;
+    }
+    // Argument is a move:
+    const move = moveOrSquare;
+    const [sx, sy, ex] = [move.start.x, move.start.y, move.end.x];
+    if (this.getPiece(sx, sy) == V.PAWN && Math.abs(sx - ex) >= 2) {
+      const step = (ex - sx) / Math.abs(ex - sx);
+      let res = [{
+        x: sx + step,
+        y: sy
+      }];
+      if (sx + 2 * step != ex) {
+        // 3-squares jump
+        res.push({
+          x: sx + 2 * step,
+          y: sy
+        });
+      }
+      return res;
+    }
+    return undefined; //default
+  }
+
+  getReservePpath(index, color) {
+    const p = V.RESERVE_PIECES[index];
+    const prefix = (ChessRules.PIECES.includes(p) ? "" : "Pandemonium/");
+    return prefix + color + p;;
+  }
+
+  // Ordering on reserve pieces
+  static get RESERVE_PIECES() {
+    return (
+      [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.CARDINAL, V.MARSHAL]
+    );
+  }
+
+  getReserveMoves([x, y]) {
+    const color = this.turn;
+    const p = V.RESERVE_PIECES[y];
+    if (this.reserve[color][p] == 0) return [];
+    const bounds = (p == V.PAWN ? [1, V.size.x - 1] : [0, V.size.x]);
+    let moves = [];
+    for (let i = bounds[0]; i < bounds[1]; i++) {
+      for (let j = 0; j < V.size.y; j++) {
+        if (this.board[i][j] == V.EMPTY) {
+          let mv = new Move({
+            appear: [
+              new PiPo({
+                x: i,
+                y: j,
+                c: color,
+                p: p
+              })
+            ],
+            vanish: [],
+            start: { x: x, y: y }, //a bit artificial...
+            end: { x: i, y: j }
+          });
+          if (p == V.PAWN) {
+            // Do not drop on checkmate:
+            this.play(mv);
+            const res = (
+              this.underCheck(oppCol) && !this.atLeastOneMove("noReserve")
+            );
+            this.undo(mv);
+            if (res) continue;
+          }
+          moves.push(mv);
+        }
+      }
+    }
+    return moves;
+  }
+
+  static get PromoteMap() {
+    return {
+      r: 'd',
+      n: 's',
+      b: 'h',
+      c: 'w',
+      m: 'a'
+    };
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const c = this.getColor(x, y);
+    const oppCol = V.GetOppCol(c);
+    if (this.movesCount <= 1) {
+      if (this.kingPos[c][0] == x && this.kingPos[c][1] == y) {
+        // Pass (if setup is ok)
+        return [
+          new Move({
+            appear: [],
+            vanish: [],
+            start: { x: this.kingPos[c][0], y: this.kingPos[c][1] },
+            end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] }
+          })
+        ];
+      }
+      const firstRank = (this.movesCount == 0 ? 9 : 0);
+      // TODO: initDestFile currently hardcoded for deterministic setup
+      const initDestFile = new Map([[1, 2], [8, 7]]);
+      // Only option is knight / bishop swap:
+      if (x == firstRank && !!initDestFile.get(y)) {
+        const destFile = initDestFile.get(y);
+        return [
+          new Move({
+            appear: [
+              new PiPo({
+                x: x,
+                y: destFile,
+                c: c,
+                p: V.KNIGHT
+              }),
+              new PiPo({
+                x: x,
+                y: y,
+                c: c,
+                p: V.BISHOP
+              })
+            ],
+            vanish: [
+              new PiPo({
+                x: x,
+                y: y,
+                c: c,
+                p: V.KNIGHT
+              }),
+              new PiPo({
+                x: x,
+                y: destFile,
+                c: c,
+                p: V.BISHOP
+              })
+            ],
+            start: { x: x, y: y },
+            end: { x: x, y: destFile }
+          })
+        ];
+      }
+      return [];
+    }
+    // Normal move (after initial setup)
+    if (x >= V.size.x) return this.getReserveMoves(x, y);
+    const p = this.getPiece(x, y);
+    const sq = [x, y];
+    let moves = [];
+    if (ChessRules.PIECES.includes(p))
+      moves = super.getPotentialMovesFrom(sq);
+    if ([V.GILDING, V.APRICOT, V.WHOLE].includes(p))
+      moves = super.getPotentialQueenMoves(sq);
+    switch (p) {
+      case V.SCEPTER:
+        moves = this.getPotentialScepterMoves(sq);
+        break;
+      case V.HORSE:
+        moves = this.getPotentialHorseMoves(sq);
+        break;
+      case V.DRAGON:
+        moves = this.getPotentialDragonMoves(sq);
+        break;
+      case V.CARDINAL:
+        moves = this.getPotentialCardinalMoves(sq);
+        break;
+      case V.MARSHAL:
+        moves = this.getPotentialMarshalMoves(sq);
+        break;
+    }
+    // Maybe apply promotions:
+    if (Object.keys(V.PromoteMap).includes(p)) {
+      const promoted = V.PromoteMap[p];
+      const lastRank = (c == 'w' ? 0 : 9);
+      let promotions = [];
+      moves.forEach(m => {
+        if (m.start.x == lastRank || m.end.x == lastRank) {
+          let pMove = JSON.parse(JSON.stringify(m));
+          pMove.appear[0].p = promoted;
+          promotions.push(pMove);
+        }
+      });
+      Array.prototype.push.apply(moves, promotions);
+    }
+    return moves;
+  }
+
+  getPotentialPawnMoves([x, y]) {
+    const color = this.turn;
+    const shiftX = V.PawnSpecs.directions[color];
+    let moves = [];
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      this.addPawnMoves([x, y], [x + shiftX, y], moves);
+      if ((color == 'w' && x >= V.size.x - 3) || (color == 'b' && x <= 3)) {
+        if (this.board[x + 2 * shiftX][y] == V.EMPTY) {
+          moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+          if (
+            (
+              (color == 'w' && x >= V.size.x - 2) ||
+              (color == 'b' && x <= 2)
+            )
+            &&
+            this.board[x + 3 * shiftX][y] == V.EMPTY
+          ) {
+            moves.push(this.getBasicMove([x, y], [x + 3 * shiftX, y]));
+          }
+        }
+      }
+    }
+    for (let shiftY of [-1, 1]) {
+      if (y + shiftY >= 0 && y + shiftY < V.size.y) {
+        if (
+          this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+          this.canTake([x, y], [x + shiftX, y + shiftY])
+        ) {
+          this.addPawnMoves([x, y], [x + shiftX, y + shiftY], moves);
+        }
+      }
+    }
+    Array.prototype.push.apply(
+      moves,
+      this.getEnpassantCaptures([x, y], shiftX)
+    );
+    return moves;
+  }
+
+  getPotentialMarshalMoves(sq) {
+    return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
+      this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")
+    );
+  }
+
+  getPotentialCardinalMoves(sq) {
+    return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
+      this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")
+    );
+  }
+
+  getPotentialScepterMoves(sq) {
+    const steps =
+      V.steps[V.KNIGHT].concat(V.steps[V.BISHOP]).concat(V.steps[V.ROOK]);
+    return this.getSlideNJumpMoves(sq, steps, "oneStep");
+  }
+
+  getPotentialHorseMoves(sq) {
+    return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
+      this.getSlideNJumpMoves(sq, V.steps[V.ROOK], "oneStep"));
+  }
+
+  getPotentialDragonMoves(sq) {
+    return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
+      this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], "oneStep"));
+  }
+
+  getEnpassantCaptures([x, y], shiftX) {
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1];
+    let moves = [];
+    if (!!epSquare) {
+      for (let epsq of epSquare) {
+        // TODO: some redundant checks
+        if (epsq.x == x + shiftX && Math.abs(epsq.y - y) == 1) {
+          let enpassantMove = this.getBasicMove([x, y], [epsq.x, epsq.y]);
+          // WARNING: the captured pawn may be diagonally behind us,
+          // if it's a 3-squares jump and we take on 1st passing square
+          const px = this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX;
+          enpassantMove.vanish.push({
+            x: px,
+            y: epsq.y,
+            p: "p",
+            c: this.getColor(px, epsq.y)
+          });
+          moves.push(enpassantMove);
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialKingMoves(sq) {
+    // Initialize with normal moves
+    let moves = this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+    const c = this.turn;
+    if (
+      this.castleFlags[c][0] < V.size.y ||
+      this.castleFlags[c][1] < V.size.y
+    ) {
+      moves = moves.concat(this.getCastleMoves(sq));
+    }
+    return moves;
+  }
+
+  getCastleMoves([x, y]) {
+    const c = this.getColor(x, y);
+    if (
+      ((c == 'w' && x == 9) || (c == 'b' && x == 0)) &&
+      y == this.castleFlags[c][2]
+    ) {
+      const finalSquares = [
+        [1, 2],
+        [7, 6]
+      ];
+      return super.getCastleMoves([x, y], finalSquares, false, [V.ROOK]);
+    }
+    return [];
+  }
+
+  isAttacked(sq, color) {
+    return (
+      this.isAttackedByPawn(sq, color) ||
+      this.isAttackedByRook(sq, color) ||
+      this.isAttackedByKnight(sq, color) ||
+      this.isAttackedByBishop(sq, color) ||
+      this.isAttackedByKing(sq, color) ||
+      this.isAttackedByQueens(sq, color) ||
+      this.isAttackedByScepter(sq, color) ||
+      this.isAttackedByDragon(sq, color) ||
+      this.isAttackedByHorse(sq, color) ||
+      this.isAttackedByMarshal(sq, color) ||
+      this.isAttackedByCardinal(sq, color)
+    );
+  }
+
+  isAttackedByQueens([x, y], color) {
+    // pieces: because queen = gilding = whole = apricot
+    const pieces = [V.QUEEN, V.GILDING, V.WHOLE, V.APRICOT];
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    for (let step of steps) {
+      let rx = x + step[0],
+          ry = y + step[1];
+      while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY) {
+        rx += step[0];
+        ry += step[1];
+      }
+      if (
+        V.OnBoard(rx, ry) &&
+        this.board[rx][ry] != V.EMPTY &&
+        pieces.includes(this.getPiece(rx, ry)) &&
+        this.getColor(rx, ry) == color
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  isAttackedByScepter(sq, color) {
+    const steps =
+      V.steps[V.KNIGHT].concat(V.steps[V.ROOK]).concat(V.steps[V.BISHOP]);
+    return (
+      super.isAttackedBySlideNJump(sq, color, steps, V.SCEPTER, "oneStep")
+    );
+  }
+
+  isAttackedByHorse(sq, color) {
+    return (
+      super.isAttackedBySlideNJump(sq, color, V.steps[V.BISHOP], V.HORSE) ||
+      super.isAttackedBySlideNJump(
+        sq, color, V.steps[V.ROOK], V.HORSE, "oneStep")
+    );
+  }
+
+  isAttackedByDragon(sq, color) {
+    return (
+      super.isAttackedBySlideNJump(sq, color, V.steps[V.ROOK], V.DRAGON) ||
+      super.isAttackedBySlideNJump(
+        sq, color, V.steps[V.BISHOP], V.DRAGON, "oneStep")
+    );
+  }
+
+  isAttackedByMarshal(sq, color) {
+    return (
+      super.isAttackedBySlideNJump(sq, color, V.MARSHAL, V.steps[V.ROOK]) ||
+      super.isAttackedBySlideNJump(
+        sq,
+        color,
+        V.MARSHAL,
+        V.steps[V.KNIGHT],
+        "oneStep"
+      )
+    );
+  }
+
+  isAttackedByCardinal(sq, color) {
+    return (
+      super.isAttackedBySlideNJump(sq, color, V.CARDINAL, V.steps[V.BISHOP]) ||
+      super.isAttackedBySlideNJump(
+        sq,
+        color,
+        V.CARDINAL,
+        V.steps[V.KNIGHT],
+        "oneStep"
+      )
+    );
+  }
+
+  getAllValidMoves() {
+    let moves = super.getAllPotentialMoves();
+    const color = this.turn;
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+      moves = moves.concat(
+        this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
+      );
+    }
+    return this.filterValid(moves);
+  }
+
+  atLeastOneMove(noReserve) {
+    if (!super.atLeastOneMove()) {
+      if (!noReserve) {
+        // Search one reserve move
+        for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+          let moves = this.filterValid(
+            this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
+          );
+          if (moves.length > 0) return true;
+        }
+      }
+      return false;
+    }
+    return true;
+  }
+
+  // Reverse 'PromoteMap'
+  static get P_CORRESPONDANCES() {
+    return {
+      d: 'r',
+      s: 'n',
+      h: 'b',
+      w: 'c',
+      a: 'm'
+    };
+  }
+
+  static MayDecode(piece) {
+    if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
+      return V.P_CORRESPONDANCES[piece];
+    return piece;
+  }
+
+  play(move) {
+    move.subTurn = this.subTurn; //much easier
+    if (this.movesCount >= 2 || this.subTurn == 2 || move.vanish.length == 0) {
+      this.turn = V.GetOppCol(this.turn);
+      this.subTurn = 1;
+      this.movesCount++;
+    }
+    else this.subTurn = 2;
+    move.flags = JSON.stringify(this.aggregateFlags());
+    this.epSquares.push(this.getEpSquare(move));
+    V.PlayOnBoard(this.board, move);
+    this.postPlay(move);
+  }
+
+  updateCastleFlags(move, piece) {
+    if (move.appear.length == 2) {
+      // Castling (only move which disable flags)
+      this.castleFlags[move.appear[0].c][0] = 10;
+      this.castleFlags[move.appear[0].c][1] = 10;
+    }
+  }
+
+  postPlay(move) {
+    if (move.vanish.length == 0 && move.appear.length == 0) return;
+    super.postPlay(move);
+    const color = move.appear[0].c;
+    if (move.vanish.length == 0)
+      // Drop unpromoted piece:
+      this.reserve[color][move.appear[0].p]--;
+    else if (move.vanish.length == 2)
+      // May capture a promoted piece:
+      this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
+  }
+
+  undo(move) {
+    this.epSquares.pop();
+    this.disaggregateFlags(JSON.parse(move.flags));
+    V.UndoOnBoard(this.board, move);
+    if (this.movesCount >= 2 || this.subTurn == 1 || move.vanish.length == 0) {
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount--;
+    }
+    this.subTurn = move.subTurn;
+    this.postUndo(move);
+  }
+
+  postUndo(move) {
+    if (move.vanish.length == 0 && move.appear.length == 0) return;
+    super.postUndo(move);
+    const color = move.appear[0].c;
+    if (move.vanish.length == 0)
+      this.reserve[color][move.appear[0].p]++;
+    else if (move.vanish.length == 2)
+      this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
+  }
+
+  getCurrentScore() {
+    const c = this.turn,
+          oppCol = V.GetOppCol(this.turn);
+    let facingKings = false;
+    if (
+      this.kingPos[c][0] == this.kingPos[oppCol][0] ||
+      this.kingPos[c][1] == this.kingPos[oppCol][1]
+    ) {
+      facingKings = true;
+      let step = [
+        this.kingPos[oppCol][0] - this.kingPos[c][0],
+        this.kingPos[oppCol][1] - this.kingPos[c][1]
+      ];
+      if (step[0] != 0) step[0] /= Math.abs(step[0]);
+      else step[1] /= Math.abs(step[1]);
+      let [x, y] =
+        [ this.kingPos[c][0] + step[0], this.kingPos[c][1] + step[1] ];
+      while (x != this.kingPos[oppCol][0] || y != this.kingPos[oppCol][1]) {
+        if (this.board[x][y] != V.EMPTY) {
+          facingKings = false;
+          break;
+        }
+        x += step[0];
+        y += step[1];
+      }
+    }
+    if (facingKings) return (c == "w" ? "1-0" : "0-1");
+    if (!this.atLeastOneMove()) return (c == "w" ? "0-1" : "1-0");
+    return "*";
+  }
+
+  static get VALUES() {
+    return Object.assign(
+      {
+        g: 9,
+        s: 5,
+        h: 6,
+        d: 7,
+        c: 7,
+        w: 9,
+        m: 8,
+        a: 9
+      },
+      ChessRules.VALUES
+    );
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+  getComputerMove() {
+    if (this.movesCount <= 1) {
+      // Special case: swap and pass at random
+      const moves1 = this.getAllValidMoves();
+      const m1 = moves1[randInt(moves1.length)];
+      this.play(m1);
+      if (m1.vanish.length == 0) {
+        this.undo(m1);
+        return m1;
+      }
+      const moves2 = this.getAllValidMoves();
+      const m2 = moves2[randInt(moves2.length)];
+      this.undo(m1);
+      return [m1, m2];
+    }
+    return super.getComputerMove();
+  }
+
+  evalPosition() {
+    let evaluation = super.evalPosition();
+    // Add reserves:
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+      const p = V.RESERVE_PIECES[i];
+      evaluation += this.reserve["w"][p] * V.VALUES[p];
+      evaluation -= this.reserve["b"][p] * V.VALUES[p];
+    }
+    return evaluation;
+  }
+
+  getNotation(move) {
+    if (move.vanish.length == 0) {
+      if (move.appear.length == 0) return "pass";
+      const pieceName =
+        (move.appear[0].p == V.PAWN ? "" : move.appear[0].p.toUpperCase());
+      return pieceName + "@" + V.CoordsToSquare(move.end);
+    }
+    if (move.appear.length == 2) {
+      if (move.appear[0].p != V.KING)
+        return V.CoordsToSquare(move.start) + "S" + V.CoordsToSquare(move.end);
+      return (move.end.y < move.start.y ? "0-0" : "0-0-0");
+    }
+    let notation = super.getNotation(move);
+    if (move.vanish[0].p != V.PAWN && move.appear[0].p != move.vanish[0].p)
+      // Add promotion indication:
+      notation += "=" + move.appear[0].p.toUpperCase();
+    return notation;
+  }
+
+};
index 9104ed9..ac98835 100644 (file)
@@ -282,7 +282,9 @@ export class ShogiRules extends ChessRules {
           if (p == V.PAWN) {
             // Do not drop on checkmate:
             this.play(mv);
           if (p == V.PAWN) {
             // Do not drop on checkmate:
             this.play(mv);
-            const res = (this.underCheck(oppCol) && !this.atLeastOneMove());
+            const res = (
+              this.underCheck(oppCol) && !this.atLeastOneMove("noReserve")
+            );
             this.undo(mv);
             if (res) continue;
           }
             this.undo(mv);
             if (res) continue;
           }
@@ -548,14 +550,16 @@ export class ShogiRules extends ChessRules {
     return this.filterValid(moves);
   }
 
     return this.filterValid(moves);
   }
 
-  atLeastOneMove() {
+  atLeastOneMove(noReserve) {
     if (!super.atLeastOneMove()) {
     if (!super.atLeastOneMove()) {
-      // Search one reserve move
-      for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
-        let moves = this.filterValid(
-          this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
-        );
-        if (moves.length > 0) return true;
+      if (!noReserve) {
+        // Search one reserve move
+        for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+          let moves = this.filterValid(
+            this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
+          );
+          if (moves.length > 0) return true;
+        }
       }
       return false;
     }
       }
       return false;
     }
index f9c9a33..c08dc9e 100644 (file)
@@ -116,6 +116,7 @@ insert or ignore into Variants (name, description) values
   ('Pacifist1', 'Convert & support (v1)'),
   ('Pacifist2', 'Convert & support (v2)'),
   ('Pacosako', 'Dance with the King'),
   ('Pacifist1', 'Convert & support (v1)'),
   ('Pacifist2', 'Convert & support (v2)'),
   ('Pacosako', 'Dance with the King'),
+  ('Pandemonium', 'Noise and confusion'),
   ('Parachute', 'Landing on the board'),
   ('Pawnmassacre', 'Pieces upside down'),
   ('Pawns', 'Reach the last rank (v1)'),
   ('Parachute', 'Landing on the board'),
   ('Pawnmassacre', 'Pieces upside down'),
   ('Pawns', 'Reach the last rank (v1)'),