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",
+  "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",
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",
+  "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",
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",
+  "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",
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",
+    "Pandemonium",
     "Refusal",
     "Relayup",
     "Rollerball",
index 01efa97..90196f6 100644 (file)
@@ -481,6 +481,7 @@ p.
     "Iceage",
     "Kingsmaker",
     "Magnetic",
+    "Pandemonium",
     "Refusal",
     "Relayup",
     "Rollerball",
index 3566c88..7729aaf 100644 (file)
@@ -479,6 +479,7 @@ p.
     "Iceage",
     "Kingsmaker",
     "Magnetic",
+    "Pandemonium",
     "Refusal",
     "Relayup",
     "Rollerball",
index a4276ed..a688a2e 100644 (file)
@@ -309,18 +309,12 @@ export class OmegaRules extends ChessRules {
     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);
-    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) {
-      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));
     }
   }
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);
-            const res = (this.underCheck(oppCol) && !this.atLeastOneMove());
+            const res = (
+              this.underCheck(oppCol) && !this.atLeastOneMove("noReserve")
+            );
             this.undo(mv);
             if (res) continue;
           }
@@ -548,14 +550,16 @@ export class ShogiRules extends ChessRules {
     return this.filterValid(moves);
   }
 
-  atLeastOneMove() {
+  atLeastOneMove(noReserve) {
     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;
     }
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'),
+  ('Pandemonium', 'Noise and confusion'),
   ('Parachute', 'Landing on the board'),
   ('Pawnmassacre', 'Pieces upside down'),
   ('Pawns', 'Reach the last rank (v1)'),