Fixes + implement Cylinder Chess
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 25 Feb 2020 01:54:51 +0000 (02:54 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 25 Feb 2020 01:54:51 +0000 (02:54 +0100)
22 files changed:
client/src/base_rules.js
client/src/components/BaseGame.vue
client/src/components/Board.vue
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/translations/rules/Circular/en.pug
client/src/translations/rules/Circular/es.pug
client/src/translations/rules/Circular/fr.pug
client/src/translations/rules/Cylinder/en.pug [new file with mode: 0644]
client/src/translations/rules/Cylinder/es.pug [new file with mode: 0644]
client/src/translations/rules/Cylinder/fr.pug [new file with mode: 0644]
client/src/translations/rules/Hidden/en.pug
client/src/translations/rules/Hidden/es.pug
client/src/translations/rules/Hidden/fr.pug
client/src/variants/Checkered.js
client/src/variants/Circular.js
client/src/variants/Cylinder.js [new file with mode: 0644]
client/src/variants/Dark.js
client/src/variants/Hidden.js
client/src/variants/Suction.js
server/db/populate.sql

index 4b9a139..d5849f3 100644 (file)
@@ -60,6 +60,14 @@ export const ChessRules = class ChessRules {
     return V.ShowMoves;
   }
 
+  // Some variants always show the same orientation
+  static get CanFlip() {
+    return true;
+  }
+  get canFlip() {
+    return V.CanFlip;
+  }
+
   // Turn "wb" into "B" (for FEN)
   static board2fen(b) {
     return b[0] == "w" ? b[1].toUpperCase() : b[1];
@@ -378,7 +386,6 @@ export const ChessRules = class ChessRules {
   setFlags(fenflags) {
     // white a-castle, h-castle, black a-castle, h-castle
     this.castleFlags = { w: [true, true], b: [true, true] };
-    if (!fenflags) return;
     for (let i = 0; i < 4; i++)
       this.castleFlags[i < 2 ? "w" : "b"][i % 2] = fenflags.charAt(i) == "1";
   }
@@ -627,8 +634,8 @@ export const ChessRules = class ChessRules {
         x + shiftX == lastRank
           ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
           : [V.PAWN];
-      // One square forward
       if (this.board[x + shiftX][y] == V.EMPTY) {
+        // One square forward
         for (let piece of finalPieces) {
           moves.push(
             this.getBasicMove([x, y], [x + shiftX, y], {
index 47776de..3573643 100644 (file)
@@ -45,7 +45,7 @@ div#baseGame(
       #controls
         button(@click="gotoBegin()") <<
         button(@click="undo()") <
-        button(@click="flip()") &#8645;
+        button(v-if="canFlip" @click="flip()") &#8645;
         button(@click="play()") >
         button(@click="gotoEnd()") >>
       #belowControls
@@ -121,16 +121,28 @@ export default {
         : (this.vr ? this.vr.showMoves : "none");
     },
     showTurn: function() {
-      return this.game.score == '*' && this.vr && this.vr.showMoves != "all";
+      return (
+        this.game.score == '*' &&
+        this.vr &&
+        (this.vr.showMoves != "all" || !this.vr.canFlip)
+      );
     },
     turn: function() {
-      return this.vr
-        ? this.st.tr[(this.vr.turn == 'w' ? "White" : "Black") + " to move"]
+      if (!this.vr)
+        return "";
+      if (this.vr.showMoves != "all")
+        return this.st.tr[(this.vr.turn == 'w' ? "White" : "Black") + " to move"]
+      // Cannot flip: racing king or circular chess
+      return this.vr.movesCount == 0 && this.game.mycolor == "w"
+        ? this.st.tr["It's your turn!"]
         : "";
     },
     canAnalyze: function() {
       return this.game.mode != "analyze" && this.vr && this.vr.canAnalyze;
     },
+    canFlip: function() {
+      return this.vr && this.vr.canFlip;
+    },
     allowDownloadPGN: function() {
       return this.game.score != "*" || (this.vr && this.vr.showMoves == "all");
     }
@@ -444,6 +456,7 @@ export default {
 
 #controls
   margin: 0 auto
+  text-align: center
   button
     display: inline-block
     width: 20%
index d442346..bf43051 100644 (file)
@@ -59,7 +59,7 @@ export default {
         }
       },
       [...Array(sizeX).keys()].map(i => {
-        let ci = this.orientation == "w" ? i : sizeX - i - 1;
+        let ci = !V.CanFlip || this.orientation == "w" ? i : sizeX - i - 1;
         return h(
           "div",
           {
@@ -69,7 +69,7 @@ export default {
             style: { opacity: this.choices.length > 0 ? "0.5" : "1" }
           },
           [...Array(sizeY).keys()].map(j => {
-            let cj = this.orientation == "w" ? j : sizeY - j - 1;
+            let cj = !V.CanFlip || this.orientation == "w" ? j : sizeY - j - 1;
             let elems = [];
             if (
               this.vr.board[ci][cj] != V.EMPTY &&
index 09fa92f..963380a 100644 (file)
@@ -42,6 +42,7 @@ export const translations = {
   "Highlight last move and checks?": "Highlight last move and checks?",
   Instructions: "Instructions",
   "Invalid email": "Invalid email",
+  "It's your turn!": "It's your turn!",
   "is not online": "is not online",
   Language: "Language",
   "Live challenges": "Live challenges",
@@ -144,6 +145,7 @@ export const translations = {
   "Lose all pieces": "Lose all pieces",
   "Mate any piece": "Mate any piece",
   "Move twice": "Move twice",
+  "Neverending rows": "Neverending rows",
   "Pawns move diagonally": "Pawns move diagonally",
   "Reuse pieces": "Reuse pieces",
   "Reverse captures": "Reverse captures",
index 286ae42..8b71fb4 100644 (file)
@@ -42,6 +42,7 @@ export const translations = {
   "Highlight last move and checks?": "¿Resaltar el último movimiento y jaques?",
   Instructions: "Instrucciones",
   "Invalid email": "Email inválido",
+  "It's your turn!": "¡Es su turno!",
   "is not online": "no está en línea",
   Language: "Idioma",
   "Live challenges": "Desafíos en vivo",
@@ -144,6 +145,7 @@ export const translations = {
   "Lose all pieces": "Perder todas las piezas",
   "Mate any piece": "Matar cualquier pieza",
   "Move twice": "Mover dos veces",
+  "Neverending rows": "Filas interminables",
   "Pawns move diagonally": "Peones se mueven en diagonal",
   "Reuse pieces": "Reutilizar piezas",
   "Reverse captures": "Capturas invertidas",
index 6746e70..6d0f82d 100644 (file)
@@ -42,6 +42,7 @@ export const translations = {
   "Highlight last move and checks?": "Mettre en valeur le dernier coup et les échecs ?",
   Instructions: "Instructions",
   "Invalid email": "Email invalide",
+  "It's your turn!": "À vous de jouer !",
   "is not online": "n'est pas en ligne",
   Language: "Langue",
   "Live challenges": "Défis en direct",
@@ -144,6 +145,7 @@ export const translations = {
   "Lose all pieces": "Perdez toutes les pièces",
   "Mate any piece": "Mater n'importe quelle pièce",
   "Move twice": "Jouer deux coups",
+  "Neverending rows": "Rangées sans fin",
   "Pawns move diagonally": "Les pions vont en diagonale",
   "Reuse pieces": "Réutiliser les pièces",
   "Reverse captures": "Captures inversées",
index 7e78b2f..67639f6 100644 (file)
@@ -1,26 +1,27 @@
-p https://www.chessvariants.com/d.betza/chessvar/race.html 8x8 race chess
-
-p See also: https://www.chessvariants.com/shape.dir/x_torus.html
-
 p.boxed
-  | If a piece captures one of the same kind, both disappear.
-
-p.
-  The defensive power of pawns is thus increased, because they don't fear
-  captures (by other pawns).
+  | Ranks 1 and 8 communicate. Pawns all go the same way and never promote.
 
 p.
-  Endings are also affected quite a lot, and sometimes new threats occur:
-  on the diagram, 3.Bxg7 wins a pawn because 3...Bxg7 would make both
-  bishops disappear.
+  In the initial position, it is often possible to win in a few moves by
+  giving check first: that is why this is forbidden.
 
 figure.diagram-container
   .diagram
-    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
-  figcaption After 1.b3 c5 2.Bb2 Nc6
+    | fen:8/8/pppppppp/rkbrnnqb/8/8/PPPPPPPP/BBRNKQNR:
+  figcaption 1.Nc3+ would win quickly.
+
+p.
+  Note that since ranks 1 and 8 communicate, a black pawn on the 8th rank
+  threatens a piece on the first rank. For example pawn d6 to d8 would check
+  the white king.
+
+p.
+  Due to the unusual pawns rules (they all go the same way: up the board),
+  en passant captures do not exist. Castling is also disabled because
+  the king is vulnerable in both directions.
 
 h3 Source
 
 p
-  a(href="https://www.chessvariants.com/rules/antimatter-chess") Antimatter chess
+  a(href="https://www.chessvariants.com/d.betza/chessvar/race.html") 8x8 Race Chess
   | &nbsp;on chessvariants.com.
index b6adc80..dc11927 100644 (file)
@@ -1,23 +1,30 @@
 p.boxed
-  | Si una pieza captura otra del mismo tipo, las dos desaparecen.
+  | Las filas 1 y 8 se comunican.
+  | Todos los peones van en la misma dirección y nunca son promovidos.
 
 p.
-  El poder defensivo de los peones aumenta así, ya que no temen
-  más capturas (por otros peones).
-
-p.
-  Las finales también se ven muy afectadas y, a veces, nuevas amenazas
-  ocurren: en el diagrama, 3.Bxg7 gana un peón porque 3...Bxg7 causaría
-  la desaparición de los dos alfiles.
+  En la posición inicial, a menudo es posible ganar en unos pocos
+  movimientos que comienzan con jaque: por eso está prohibido.
 
 figure.diagram-container
   .diagram
-    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
-  figcaption Después de 1.b3 c5 2.Bb2 Nc6
+    | fen:8/8/pppppppp/rkbrnnqb/8/8/PPPPPPPP/BBRNKQNR:
+  figcaption 1.Nc3+ ganaría rápidamente.
+
+p.
+  Tenga en cuenta que dado que las filas 1 y 8 se comunican, un peón negro
+  en el octavo fila amenaza una pieza en el primero.
+  Por ejemplo, el peón d6 a d8 pondría en jaque el rey blanco.
+
+p.
+  Debido a las reglas de peones inusuales (todos van en la misma
+  dirección: arriba), no hay capturas en passant.
+  El enroque tampoco es posible porque el rey es vulnerable
+  en ambas direcciones.
 
 h3 Fuente
 
 p
   | La 
-  a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimateria
+  a(href="https://www.chessvariants.com/d.betza/chessvar/race.html") variante 8x8 Race
   | &nbsp;en chessvariants.com.
index 7b1cdfa..8bc281f 100644 (file)
@@ -1,23 +1,30 @@
 p.boxed
-  | Si une pièce en capture une autre du même type, les deux disparaissent.
+  | Les rangées 1 et 8 communiquent.
+  | Les pions vont tous dans le même sens et ne sont jamais promus.
 
 p.
-  Le pouvoir défensif des pions est ainsi augmenté, puisqu'ils ne craignent
-  plus les captures (par d'autres pions).
-
-p.
-  Les finales sont aussi beaucoup affectées, et parfois de nouvelles menaces
-  surviennent : sur le diagramme, 3.Bxg7 gagne un pion car 3...Bxg7 provoquerait
-  la disparition des deux fous.
+  Dans la position initiale, il est souvent possible de gagner en quelques
+  coups en commençant par donner échec : c'est pourquoi on l'interdit.
 
 figure.diagram-container
   .diagram
-    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
-  figcaption After 1.b3 c5 2.Bb2 Nc6
+    | fen:8/8/pppppppp/rkbrnnqb/8/8/PPPPPPPP/BBRNKQNR:
+  figcaption 1.Nc3+ gagnerait rapidement.
+
+p.
+  Notez que puisque les rangées 1 et 8 communiquent, un pion noir sur la 8eme
+  rangée menace une pièce sur la première. Par exemple pion d6 à d8 mettrait
+  en échec le roi blanc.
+
+p.
+  À cause des règles inhabituelles sur les pions (ils vont tous dans la même
+  direction : vers le haut), les captures en passant n'existent pas.
+  Le roque n'est pas possible non plus car le roi est vulnérable
+  dans les deux directions.
 
 h3 Source
 
 p
   | La 
-  a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimatière
+  a(href="https://www.chessvariants.com/d.betza/chessvar/race.html") variante 8x8 Race
   | &nbsp;sur chessvariants.com.
diff --git a/client/src/translations/rules/Cylinder/en.pug b/client/src/translations/rules/Cylinder/en.pug
new file mode 100644 (file)
index 0000000..0b43676
--- /dev/null
@@ -0,0 +1,24 @@
+p.boxed
+  | Columns 'a' and 'h' communicate.
+
+p.
+  Column 'a' is now just to the right of column 'h': a pawn on h2 can
+  take on a3, and a bishop on h3 can go to a4, and so on.
+
+p.
+  This allows a single piece to give a double check. On the following diagram
+  the bishop give check on b5 by two distinct paths: e2-d3-c4 and g2-h3-a4.
+  Covering both attacking lines is impossible so the king has to move.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:8/2p5/3n4/1k6/8/8/2PP4/5B1K:
+  .diagram.diag22
+    | fen:4k3/2p5/8/8/7N/8/3P4/7K a2,b3,b5,a6,g2,f3,f5,g6:
+  figcaption Left: double-check on b5. Right: possible knight moves from h4.
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/boardrules.dir/cylindrical.html") Cylinder Chess
+  | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Cylinder/es.pug b/client/src/translations/rules/Cylinder/es.pug
new file mode 100644 (file)
index 0000000..358a8e4
--- /dev/null
@@ -0,0 +1,27 @@
+p.boxed
+  | Las columnas 'a' y 'h' se comunican.
+
+p.
+  La columna 'a' está ahora a la derecha de la columna 'h':
+  un peón en h2 puede tomar a3, y un alfil en h3 puede ir en a4, etc.
+
+p.
+  Esto permite dobles jaques dadas por una sola pieza. En el diagrama
+  a continuación, el alfil da jaque por dos caminos diferentes: e2-d3-c4 y g2-h3-a4.
+  No es posible cubrir las dos líneas de ataque, por lo que el rey debe moverse.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:8/2p5/3n4/1k6/8/8/2PP4/5B1K:
+  .diagram.diag22
+    | fen:4k3/2p5/8/8/7N/8/3P4/7K a2,b3,b5,a6,g2,f3,f5,g6:
+  figcaption.
+    Izquierda: doble-jaque en b5. Derecha: posibles movimientos de caballo desde h4.
+
+h3 Fuente
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/boardrules.dir/cylindrical.html")
+    | variante cilíndrica
+  | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Cylinder/fr.pug b/client/src/translations/rules/Cylinder/fr.pug
new file mode 100644 (file)
index 0000000..cd41108
--- /dev/null
@@ -0,0 +1,27 @@
+p.boxed
+  | Les colonnes 'a' et 'h' commniquent.
+
+p.
+  La colonne 'a' est maintenant juste à la droite de la colonne 'h' :
+  un pion en h2 peut prendre en a3, et un fou en h3 peut aller en a4, etc.
+
+p.
+  Ceci permet des double échecs donnés par une seule pièce. Sur le diagramme
+  suivant, le fou donne échec par deux chemins différents : e2-d3-c4 et g2-h3-a4.
+  Il n'est pas possible de couvrir les deux lignes d'attaque, donc le roi doit bouger.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:8/2p5/3n4/1k6/8/8/2PP4/5B1K:
+  .diagram.diag22
+    | fen:4k3/2p5/8/8/7N/8/3P4/7K a2,b3,b5,a6,g2,f3,f5,g6:
+  figcaption.
+    Gauche: double-échec en b5. Droite: possibles coups de cavalier depuis h4.
+
+h3 Source
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/boardrules.dir/cylindrical.html")
+    | variante cylindrique
+  | &nbsp;sur chessvariants.com.
index 88e112b..e8ebbbb 100644 (file)
@@ -14,12 +14,14 @@ p The game is won by capturing the opposing King.
 
 figure.diagram-container
   .diagram
-    | fen:qbppnprp/prppbkpn/8/8/8/8/QBPPNPRP/PRPPBKPN:
+    | fen:qbppnprp/prpbpkpn/8/8/8/8/QBPPNPRP/PRPPKBPN:
   figcaption Possible starting position.
 
 p Notes
 ul
-  li The computer plays for now totally at random.
+  li.
+    The computer uses a basic strategy, way inferior to what a human could do
+    but still better than random play.
   li.
     Pieces are randomly set on the two first ranks.
     The king may be on second rank, and attacked by an enemy rook or queen.
index c8c0601..c480ad4 100644 (file)
@@ -15,12 +15,14 @@ p La victoria se obtiene capturando al rey contrario.
 
 figure.diagram-container
   .diagram
-    | fen:qbppnprp/prppbkpn/8/8/8/8/QBPPNPRP/PRPPBKPN:
+    | fen:qbppnprp/prpbpkpn/8/8/8/8/QBPPNPRP/PRPPKBPN:
   figcaption Posible posición inicial.
 
 p Notas
 ul
-  li La computadora actualmente está jugando completamente al azar.
+  li.
+    La computadora usa una estrategia básica, mucho más baja de lo que
+    humano podría hacer pero mejor que un juego aleatorio.
   li.
     Las piezas se distribuyen al azar en las dos primeras filas.
     El rey podría estar en la segunda fila, atacado por una torre o dama enemiga.
index 930400f..f491100 100644 (file)
@@ -15,12 +15,14 @@ p La victoire s'obtient en capturant le roi adverse.
 
 figure.diagram-container
   .diagram
-    | fen:qbppnprp/prppbkpn/8/8/8/8/QBPPNPRP/PRPPBKPN:
+    | fen:qbppnprp/prpbpkpn/8/8/8/8/QBPPNPRP/PRPPKBPN:
   figcaption Possible position initiale.
 
 p Notes
 ul
-  li L'ordinateur joue pour l'instant totalement au hasard.
+  li.
+    L'ordinateur utilise une stratégie basique, nettement inférieure à ce qu'un
+    humain pourrait faire mais meilleure qu'un jeu aléatoire.
   li.
     Les pièces sont réparties au hasard sur les deux premières rangées.
     Le roi pourrait être sur la seconde rangée, attaqué par une tour ou dame ennemie.
index d477a6d..e930fb8 100644 (file)
@@ -74,7 +74,6 @@ export const VariantRules = class CheckeredRules extends ChessRules {
       w: [...Array(8).fill(true)], //pawns can move 2 squares?
       b: [...Array(8).fill(true)]
     };
-    if (!fenflags) return;
     const flags = fenflags.substr(4); //skip first 4 digits, for castle
     for (let c of ["w", "b"]) {
       for (let i = 0; i < 8; i++)
@@ -112,7 +111,8 @@ export const VariantRules = class CheckeredRules extends ChessRules {
   getPotentialMovesFrom([x, y]) {
     let standardMoves = super.getPotentialMovesFrom([x, y]);
     const lastRank = this.turn == "w" ? 0 : 7;
-    if (this.getPiece(x, y) == V.KING) return standardMoves; //king has to be treated differently (for castles)
+    // King has to be treated differently (for castles)
+    if (this.getPiece(x, y) == V.KING) return standardMoves;
     let moves = [];
     standardMoves.forEach(m => {
       if (m.vanish[0].p == V.PAWN) {
index a5a5d64..4db5a52 100644 (file)
@@ -3,20 +3,36 @@ import { ArrayFun } from "@/utils/array";
 import { randInt, shuffle } from "@/utils/alea";
 
 export const VariantRules = class CircularRules extends ChessRules {
-  static get HasFlags() {
+  static get HasEnpassant() {
     return false;
   }
 
-  static get HasEnpassant() {
+  static get CanFlip() {
     return false;
   }
 
-  // TODO: CanFlip --> also for racing kings (answer is false)
+  setFlags(fenflags) {
+    this.pawnFlags = {
+      w: [...Array(8).fill(true)], //pawns can move 2 squares?
+      b: [...Array(8).fill(true)]
+    };
+    for (let c of ["w", "b"]) {
+      for (let i = 0; i < 8; i++)
+        this.pawnFlags[c][i] = fenflags.charAt((c == "w" ? 0 : 8) + i) == "1";
+    }
+  }
+
+  aggregateFlags() {
+    return this.pawnFlags;
+  }
+
+  disaggregateFlags(flags) {
+    this.pawnFlags = flags;
+  }
 
-  // TODO: shuffle on 1st and 5th ranks
   static GenRandInitFen() {
     let pieces = { w: new Array(8), b: new Array(8) };
-    // Shuffle pieces on first and last rank
+    // Shuffle pieces on first and fifth rank
     for (let c of ["w", "b"]) {
       let positions = ArrayFun.range(8);
 
@@ -60,23 +76,32 @@ export const VariantRules = class CircularRules extends ChessRules {
       pieces[c][rook2Pos] = "r";
     }
     return (
+      "8/8/pppppppp/" +
       pieces["b"].join("") +
-      "/pppppppp/8/8/8/8/PPPPPPPP/" +
+      "/8/8/PPPPPPPP/" +
       pieces["w"].join("").toUpperCase() +
-      " w 0"
+      // 16 flags: can pawns advance 2 squares?
+      " w 0 1111111111111111"
     );
   }
 
-  // TODO: adapt this for a circular board
+  // Output basically x % 8 (circular board)
+  static ComputeX(x) {
+    let res = x % V.size.x;
+    if (res < 0)
+      res += V.size.x;
+    return res;
+  }
+
   getSlideNJumpMoves([x, y], steps, oneStep) {
     let moves = [];
     outerLoop: for (let step of steps) {
-      let i = x + step[0];
+      let i = V.ComputeX(x + step[0]);
       let j = y + step[1];
       while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
         moves.push(this.getBasicMove([x, y], [i, j]));
         if (oneStep !== undefined) continue outerLoop;
-        i += step[0];
+        i = V.ComputeX(i + step[0]);
         j += step[1];
       }
       if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
@@ -85,66 +110,42 @@ export const VariantRules = class CircularRules extends ChessRules {
     return moves;
   }
 
-  // TODO: adapt: all pawns go in thz same direction!
   getPotentialPawnMoves([x, y]) {
     const color = this.turn;
     let moves = [];
     const [sizeX, sizeY] = [V.size.x, V.size.y];
-    const shiftX = color == "w" ? -1 : 1;
-    const firstRank = color == "w" ? sizeX - 1 : 0;
-    const startRank = color == "w" ? sizeX - 2 : 1;
-    const lastRank = color == "w" ? 0 : sizeX - 1;
-    const pawnColor = this.getColor(x, y); //can be different for checkered
-
-    // NOTE: next condition is generally true (no pawn on last rank)
-    if (x + shiftX >= 0 && x + shiftX < sizeX) {
-      const finalPieces =
-        x + shiftX == lastRank
-          ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
-          : [V.PAWN];
-      // One square forward
-      if (this.board[x + shiftX][y] == V.EMPTY) {
-        for (let piece of finalPieces) {
-          moves.push(
-            this.getBasicMove([x, y], [x + shiftX, y], {
-              c: pawnColor,
-              p: piece
-            })
-          );
-        }
-        // Next condition because pawns on 1st rank can generally jump
-        if (
-          [startRank, firstRank].includes(x) &&
-          this.board[x + 2 * shiftX][y] == V.EMPTY
-        ) {
-          // Two squares jump
-          moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
-        }
+    // All pawns go in the same direction!
+    const shiftX = -1;
+    const startRank = color == "w" ? sizeX - 2 : 2;
+
+    // One square forward
+    const nextRow = V.ComputeX(x + shiftX);
+    if (this.board[nextRow][y] == V.EMPTY) {
+      moves.push(this.getBasicMove([x, y], [nextRow, y]));
+      if (
+        x == startRank &&
+        this.pawnFlags[color][y] &&
+        this.board[x + 2 * shiftX][y] == V.EMPTY
+      ) {
+        // Two squares jump
+        moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
       }
-      // Captures
-      for (let shiftY of [-1, 1]) {
-        if (
-          y + shiftY >= 0 &&
-          y + shiftY < sizeY &&
-          this.board[x + shiftX][y + shiftY] != V.EMPTY &&
-          this.canTake([x, y], [x + shiftX, y + shiftY])
-        ) {
-          for (let piece of finalPieces) {
-            moves.push(
-              this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
-                c: pawnColor,
-                p: piece
-              })
-            );
-          }
-        }
+    }
+    // Captures
+    for (let shiftY of [-1, 1]) {
+      if (
+        y + shiftY >= 0 &&
+        y + shiftY < sizeY &&
+        this.board[nextRow][y + shiftY] != V.EMPTY &&
+        this.canTake([x, y], [nextRow, y + shiftY])
+      ) {
+        moves.push(this.getBasicMove([x, y], [nextRow, y + shiftY]));
       }
     }
 
     return moves;
   }
 
-  // What are the king moves from square x,y ?
   getPotentialKingMoves(sq) {
     return this.getSlideNJumpMoves(
       sq,
@@ -153,33 +154,45 @@ export const VariantRules = class CircularRules extends ChessRules {
     );
   }
 
-  // TODO: check boundaries here as well
+  filterValid(moves) {
+    const filteredMoves = super.filterValid(moves);
+    // If at least one full move made, everything is allowed:
+    if (this.movesCount >= 2)
+      return filteredMoves;
+    // Else, forbid check:
+    const oppCol = V.GetOppCol(this.turn);
+    return filteredMoves.filter(m => {
+      this.play(m);
+      const res = !this.underCheck(oppCol);
+      this.undo(m);
+      return res;
+    });
+  }
+
   isAttackedByPawn([x, y], colors) {
     for (let c of colors) {
-      let pawnShift = c == "w" ? 1 : -1;
-      if (x + pawnShift >= 0 && x + pawnShift < V.size.x) {
-        for (let i of [-1, 1]) {
-          if (
-            y + i >= 0 &&
-            y + i < V.size.y &&
-            this.getPiece(x + pawnShift, y + i) == V.PAWN &&
-            this.getColor(x + pawnShift, y + i) == c
-          ) {
-            return true;
-          }
+      let pawnShift = 1;
+      const attackerRow = V.ComputeX(x + pawnShift);
+      for (let i of [-1, 1]) {
+        if (
+          y + i >= 0 &&
+          y + i < V.size.y &&
+          this.getPiece(attackerRow, y + i) == V.PAWN &&
+          this.getColor(attackerRow, y + i) == c
+        ) {
+          return true;
         }
       }
     }
     return false;
   }
 
-  // TODO: adapt this function
   isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) {
     for (let step of steps) {
-      let rx = x + step[0],
+      let rx = V.ComputeX(x + step[0]),
           ry = y + step[1];
       while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
-        rx += step[0];
+        rx = V.ComputeX(rx + step[0]);
         ry += step[1];
       }
       if (
@@ -192,4 +205,37 @@ export const VariantRules = class CircularRules extends ChessRules {
     }
     return false;
   }
+
+  getFlagsFen() {
+    // Return pawns flags
+    let flags = "";
+    for (let c of ["w", "b"]) {
+      for (let i = 0; i < 8; i++) flags += this.pawnFlags[c][i] ? "1" : "0";
+    }
+    return flags;
+  }
+
+  updateVariables(move) {
+    const c = move.vanish[0].c;
+    const secondRank = {"w":6, "b":2};
+    // Update king position + flags
+    if (move.vanish[0].p == V.KING && move.appear.length > 0) {
+      this.kingPos[c][0] = move.appear[0].x;
+      this.kingPos[c][1] = move.appear[0].y;
+    }
+    else if (move.vanish[0].p == V.PAWN && secondRank[c] == move.start.x)
+      // This move turns off a 2-squares pawn flag
+      this.pawnFlags[c][move.start.y] = false;
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 5,
+      n: 3,
+      b: 4,
+      q: 10,
+      k: 1000
+    };
+  }
 };
diff --git a/client/src/variants/Cylinder.js b/client/src/variants/Cylinder.js
new file mode 100644 (file)
index 0000000..39a5185
--- /dev/null
@@ -0,0 +1,147 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt, shuffle } from "@/utils/alea";
+
+export const VariantRules = class CylinderRules extends ChessRules {
+  // Output basically x % 8 (circular board)
+  static ComputeY(y) {
+    let res = y % V.size.y;
+    if (res < 0)
+      res += V.size.y;
+    return res;
+  }
+
+  getSlideNJumpMoves([x, y], steps, oneStep) {
+    let moves = [];
+    outerLoop: for (let step of steps) {
+      let i = x + step[0];
+      let j = V.ComputeY(y + step[1]);
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+        if (oneStep !== undefined) continue outerLoop;
+        i += step[0];
+        j = V.ComputeY(j + step[1]);
+      }
+      if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
+        moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    return moves;
+  }
+
+  getPotentialPawnMoves([x, y]) {
+    const color = this.turn;
+    let moves = [];
+    const [sizeX, sizeY] = [V.size.x, V.size.y];
+    const shiftX = color == "w" ? -1 : 1;
+    const startRank = color == "w" ? sizeX - 2 : 1;
+    const lastRank = color == "w" ? 0 : sizeX - 1;
+
+    const finalPieces =
+      x + shiftX == lastRank
+        ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
+        : [V.PAWN];
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      // One square forward
+      for (let piece of finalPieces) {
+        moves.push(
+          this.getBasicMove([x, y], [x + shiftX, y], {
+            c: color,
+            p: piece
+          })
+        );
+      }
+      if (
+        x == startRank &&
+        this.board[x + 2 * shiftX][y] == V.EMPTY
+      ) {
+        // Two squares jump
+        moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+      }
+    }
+    // Captures
+    for (let shiftY of [-1, 1]) {
+      const nextFile = V.ComputeY(y + shiftY);
+      if (
+        this.board[x + shiftX][nextFile] != V.EMPTY &&
+        this.canTake([x, y], [x + shiftX, nextFile])
+      ) {
+        for (let piece of finalPieces) {
+          moves.push(
+            this.getBasicMove([x, y], [x + shiftX, nextFile], {
+              c: color,
+              p: piece
+            })
+          );
+        }
+      }
+    }
+
+    // En passant
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1]; //always at least one element
+    if (
+      !!epSquare &&
+      epSquare.x == x + shiftX &&
+      Math.abs( (epSquare.y - y) % V.size.y ) == 1
+    ) {
+      let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
+      enpassantMove.vanish.push({
+        x: x,
+        y: epSquare.y,
+        p: "p",
+        c: this.getColor(x, epSquare.y)
+      });
+      moves.push(enpassantMove);
+    }
+
+    return moves;
+  }
+
+  isAttackedByPawn([x, y], colors) {
+    for (let c of colors) {
+      let pawnShift = c == "w" ? 1 : -1;
+      if (x + pawnShift >= 0 && x + pawnShift < V.size.x) {
+        for (let i of [-1, 1]) {
+          const nextFile = V.ComputeY(y + i);
+          if (
+            this.getPiece(x + pawnShift, nextFile) == V.PAWN &&
+            this.getColor(x + pawnShift, nextFile) == c
+          ) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) {
+    for (let step of steps) {
+      let rx = x + step[0],
+          ry = V.ComputeY(y + step[1]);
+      while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
+        rx += step[0];
+        ry = V.ComputeY(ry + step[1]);
+      }
+      if (
+        V.OnBoard(rx, ry) &&
+        this.getPiece(rx, ry) === piece &&
+        colors.includes(this.getColor(rx, ry))
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 5,
+      n: 3,
+      b: 4,
+      q: 10,
+      k: 1000
+    };
+  }
+};
index 3944ca1..6cf3b67 100644 (file)
@@ -223,19 +223,18 @@ export const VariantRules = class DarkRules extends ChessRules {
 
       // Can I take something ? If yes, do it if it seems good...
       if (move.vanish.length == 2 && move.vanish[1].c != color) {
-        //avoid castle
+        // OK this isn't a castling move
         const myPieceVal = V.VALUES[move.appear[0].p];
         const hisPieceVal = V.VALUES[move.vanish[1].p];
-        if (myPieceVal <= hisPieceVal) move.eval = hisPieceVal - myPieceVal + 2;
-        //favor captures
+        // Favor captures
+        if (myPieceVal <= hisPieceVal) move.eval = hisPieceVal - myPieceVal + 1;
         else {
           // Taking a pawn with minor piece,
           // or minor piece or pawn with a rook,
           // or anything but a queen with a queen,
           // or anything with a king.
-          // ==> Do it at random, although
-          //     this is clearly inferior to what a human can deduce...
-          move.eval = Math.random() < 0.5 ? 1 : -1;
+          move.eval = hisPieceVal - myPieceVal;
+                      //Math.random() < 0.5 ? 1 : -1;
         }
       }
     }
index c8d98eb..96822b9 100644 (file)
@@ -214,9 +214,9 @@ export const VariantRules = class HiddenRules extends ChessRules {
       pieces[c][rook2Pos] = "u";
     }
     let upFen = pieces["b"].join("");
-    upFen = upFen.substr(0,8) + "/" + upFen.substr(8);
+    upFen = upFen.substr(0,8) + "/" + upFen.substr(8).split("").reverse().join("");
     let downFen = pieces["b"].join("").toUpperCase();
-    downFen = downFen.substr(0,8) + "/" + downFen.substr(8);
+    downFen = downFen.substr(0,8) + "/" + downFen.substr(8).split("").reverse().join("");
     return upFen + "/8/8/8/8/" + downFen + " w 0";
   }
 
@@ -255,9 +255,49 @@ export const VariantRules = class HiddenRules extends ChessRules {
   }
 
   getComputerMove() {
-    // Just return a random move. TODO: something smarter...
-    const moves = this.getAllValidMoves();
-    return moves[randInt(moves.length)];
+    const color = this.turn;
+    let moves = this.getAllValidMoves();
+    for (let move of moves) {
+      move.eval = 0; //a priori...
+
+      // Can I take something ? If yes, do it with some probability
+      if (move.vanish.length == 2 && move.vanish[1].c != color) {
+        // OK this isn't a castling move
+        const myPieceVal = V.VALUES[move.appear[0].p];
+        const hisPieceVal = Object.keys(V.HIDDEN_DECODE).includes(move.vanish[1].p)
+          ? undefined
+          : V.VALUES[move.vanish[1].p];
+        if (!hisPieceVal) {
+          // Opponent's piece is unknown: do not take too much risk
+          move.eval = -myPieceVal + 1.5; //so that pawns always take
+        }
+        // Favor captures
+        else if (myPieceVal <= hisPieceVal)
+          move.eval = hisPieceVal - myPieceVal + 1;
+        else {
+          // Taking a pawn with minor piece,
+          // or minor piece or pawn with a rook,
+          // or anything but a queen with a queen,
+          // or anything with a king.
+          move.eval = hisPieceVal - myPieceVal;
+        }
+      } else {
+        // If no capture, favor small step moves,
+        // but sometimes move the knight anyway
+        const penalty = V.Decode(move.vanish[0].p) != V.KNIGHT
+          ? Math.abs(move.end.x - move.start.x) + Math.abs(move.end.y - move.start.y)
+          : (Math.random() < 0.5 ? 3 : 1);
+        move.eval -= penalty / (V.size.x + V.size.y - 1);
+      }
+
+      // TODO: also favor movements toward the center?
+    }
+
+    moves.sort((a, b) => b.eval - a.eval);
+    let candidates = [0];
+    for (let j = 1; j < moves.length && moves[j].eval == moves[0].eval; j++)
+      candidates.push(j);
+    return moves[candidates[randInt(candidates.length)]];
   }
 
   getNotation(move) {
index 38ee5c2..297eef7 100644 (file)
@@ -236,4 +236,28 @@ export const VariantRules = class SuctionRules extends ChessRules {
     // Very simple criterion for now: kings position
     return this.kingPos["w"][0] + this.kingPos["b"][0];
   }
+
+  getNotation(move) {
+    // Translate final square
+    const finalSquare = V.CoordsToSquare(move.end);
+
+    const piece = this.getPiece(move.start.x, move.start.y);
+    if (piece == V.PAWN) {
+      // Pawn move
+      let notation = "";
+      if (move.vanish.length == 2) {
+        // Capture
+        const startColumn = V.CoordToColumn(move.start.y);
+        notation = startColumn + "x" + finalSquare;
+      }
+      else notation = finalSquare;
+      return notation;
+    }
+    // Piece movement
+    return (
+      piece.toUpperCase() +
+      (move.vanish.length == 2 ? "x" : "") +
+      finalSquare
+    );
+  }
 };
index a86a874..e629146 100644 (file)
@@ -13,6 +13,7 @@ insert or ignore into Variants (name,description) values
   ('Chess960', 'Standard rules'),
   ('Circular', 'Run forward'),
   ('Crazyhouse', 'Captures reborn'),
+  ('Cylinder', 'Neverending rows'),
   ('Dark', 'In the shadow'),
   ('Enpassant', 'Capture en passant'),
   ('Extinction', 'Capture all of a kind'),