From aa32568ef0c9fff28d17d19588b53ee049f0e1dc Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Fri, 8 Jan 2021 18:47:47 +0100
Subject: [PATCH] Synchrone2: almost OK (issues with undoing pass moves)

---
 .../src/translations/rules/Synchrone2/en.pug  |  26 ++-
 .../src/translations/rules/Synchrone2/es.pug  |  28 +++-
 .../src/translations/rules/Synchrone2/fr.pug  |  28 +++-
 client/src/variants/Synchrone1.js             |   6 +-
 client/src/variants/Synchrone2.js             | 154 +++++++++++++++---
 client/src/views/Rules.vue                    |   3 +-
 6 files changed, 212 insertions(+), 33 deletions(-)

diff --git a/client/src/translations/rules/Synchrone2/en.pug b/client/src/translations/rules/Synchrone2/en.pug
index 15050ab7..057fae77 100644
--- a/client/src/translations/rules/Synchrone2/en.pug
+++ b/client/src/translations/rules/Synchrone2/en.pug
@@ -1,9 +1,25 @@
-p.boxed TODO
+p.boxed
+  | After a synchronous turn, each player may capture the moving
+  | opponent's piece if it is on a square previously attacked.
 
-p No en passant captures.
+p
+  | Everything goes as in 
+  a(href="/#/variants/Synchrone1") Synchrone1
+  | &nbsp;variant, but after each pair of synchronous moves (resolving as
+  | usual), both players may decide to capture the enemy unit just moved.
+  | The capturing move must be legal according to the previous board situation.
+  | If you would rather (or cannot) achieve such a capture, pass by playing
+  | any of your piece onto your king, or your king on the enemy king.
 
-p No anticipated recaptures.
+p.
+  Because of the added capturing turn, there are no anticipated recaptures.
+  In this version, there are also no en-passant captures.
 
-p But, a deterministic "capturing turn" added.
+h3 More information
 
-p http://www.hexenspiel.de/engl/synchronous-chess/
+p
+  | This variant is described on 
+  a(href="http://www.hexenspiel.de/engl/synchronous-chess/") this page
+  | .
+
+p Inventors: Ralf Hansmann, Arnold J. Krasowsky, Andrey Krasowsky.
diff --git a/client/src/translations/rules/Synchrone2/es.pug b/client/src/translations/rules/Synchrone2/es.pug
index 21203baa..4fa8fc2e 100644
--- a/client/src/translations/rules/Synchrone2/es.pug
+++ b/client/src/translations/rules/Synchrone2/es.pug
@@ -1 +1,27 @@
-p.boxed TODO
+p.boxed
+  | Después de un turno sincrónico, cada jugador puede capturar opcionalmente
+  | la pieza del oponente se ha movido, si está en
+  | una casilla previamente atacada
+
+p
+  | Todo va como en la variante 
+  a(href="/#/variants/Synchrone1") Synchrone1
+  | , pero después de cada par de movimientos sincrónicos (resueltos como de
+  | costumbre), ambos jugadores pueden decidir capturar la pieza enemiga
+  | acaba de moverse. La jugada de captura debe ser legal dependiendo de la
+  | situación anterior en el tablero. Si prefiere (o no puede) realizar
+  | tal captura, pasa tocando cualquiera de tus piezas
+  | a tu rey, o del rey al rey contrario.
+
+p.
+  No hay capturas tempranas, ya que la ronda intermedia se llena
+  este papel. En esta versión tampoco existen capturas en passant.
+
+h3 Más información
+
+p
+  | Esta variante se describe en 
+  a(href="http://www.hexenspiel.de/engl/synchronous-chess/") esta página
+  | .
+
+p Inventores: Ralf Hansmann, Arnold J. Krasowsky, Andrey Krasowsky.
diff --git a/client/src/translations/rules/Synchrone2/fr.pug b/client/src/translations/rules/Synchrone2/fr.pug
index 21203baa..3eaeb73a 100644
--- a/client/src/translations/rules/Synchrone2/fr.pug
+++ b/client/src/translations/rules/Synchrone2/fr.pug
@@ -1 +1,27 @@
-p.boxed TODO
+p.boxed
+  | Après un tour synchrone, chaque joueur peut éventuellement capturer la
+  | pièce adverse s'étant déplacée, si elle se trouve sur
+  | une case attaquée précédemment
+
+p
+  | Tout se déroule comme dans la variante 
+  a(href="/#/variants/Synchrone1") Synchrone1
+  | , mais après chaque paire de coups synchrones (résolus comme d'habitude),
+  | les deux joueurs peuvent décider de capturer la pièce ennemie juste
+  | déplacée. Le coup capturant doit être légal selon la situation
+  | précédente sur l'échiquier. Si vous préférez (ou ne pouvez pas) effectuer
+  | une telle capture, passez en jouant n'importe laquelle de votre pièce
+  | vers votre roi, ou le roi vers le roi adverse.
+
+p.
+  Il n'y a pas de re-captures anticipée, puisque le tour intermédiaire remplit
+  ce rôle. Dans cette version, les prises en passant n'existent pas non plus.
+
+h3 Plus d'information
+
+p
+  | Cette variante est décrite sur 
+  a(href="http://www.hexenspiel.de/engl/synchronous-chess/") cette page
+  | .
+
+p Inventeurs : Ralf Hansmann, Arnold J. Krasowsky, Andrey Krasowsky.
diff --git a/client/src/variants/Synchrone1.js b/client/src/variants/Synchrone1.js
index ba9cf9f2..2c59bc6c 100644
--- a/client/src/variants/Synchrone1.js
+++ b/client/src/variants/Synchrone1.js
@@ -399,7 +399,7 @@ export class Synchrone1Rules extends ChessRules {
 
     // Update king position + flags
     let kingAppear = { 'w': false, 'b': false };
-    for (let i=0; i<smove.appear.length; i++) {
+    for (let i = 0; i < smove.appear.length; i++) {
       if (smove.appear[i].p == V.KING) {
         const c = smove.appear[i].c;
         kingAppear[c] = true;
@@ -407,7 +407,7 @@ export class Synchrone1Rules extends ChessRules {
         this.kingPos[c][1] = smove.appear[i].y;
       }
     }
-    for (let i=0; i<smove.vanish.length; i++) {
+    for (let i = 0; i < smove.vanish.length; i++) {
       if (smove.vanish[i].p == V.KING) {
         const c = smove.vanish[i].c;
         if (!kingAppear[c]) {
@@ -434,7 +434,7 @@ export class Synchrone1Rules extends ChessRules {
 
   postUndo(move) {
     if (this.turn == 'w') {
-      // Reset king positions: scan board
+      // Reset king positions: scan board (TODO: could be more efficient)
       this.scanKings();
       // Also reset whiteMove
       this.whiteMove = null;
diff --git a/client/src/variants/Synchrone2.js b/client/src/variants/Synchrone2.js
index 7f623982..00d68ad7 100644
--- a/client/src/variants/Synchrone2.js
+++ b/client/src/variants/Synchrone2.js
@@ -1,4 +1,4 @@
-import { ChessRules } from "@/base_rules";
+import { ChessRules, Move } from "@/base_rules";
 import { Synchrone1Rules } from "@/variants/Synchrone1";
 import { randInt } from "@/utils/alea";
 
@@ -16,7 +16,7 @@ export class Synchrone2Rules extends Synchrone1Rules {
     if (!Synchrone1Rules.IsGoodFen(fen)) return false;
     const fenParsed = V.ParseFen(fen);
     // 5) Check initFen (not really... TODO?)
-    if (!fenParsed.initFen || fenParsed.initFen == "-") return false;
+    if (!fenParsed.initFen) return false;
     return true;
   }
 
@@ -32,18 +32,16 @@ export class Synchrone2Rules extends Synchrone1Rules {
   }
 
   getInitfenFen() {
-    if (!this.whiteMove) return "-";
-    return JSON.stringify({
-      start: this.whiteMove.start,
-      end: this.whiteMove.end,
-      appear: this.whiteMove.appear,
-      vanish: this.whiteMove.vanish
-    });
+    const L = this.initfenStack.length;
+    return L > 0 ? this.initfenStack[L-1] : "-";
   }
 
   getFen() {
     return (
-      super.getFen() + " " +
+      super.getBaseFen() + " " +
+      super.getTurnFen() + " " +
+      this.movesCount + " " +
+      super.getFlagsFen() + " " +
       this.getInitfenFen() + " " +
       this.getWhitemoveFen()
     );
@@ -52,7 +50,7 @@ export class Synchrone2Rules extends Synchrone1Rules {
   static GenRandInitFen(randomness) {
     const res = ChessRules.GenRandInitFen(randomness);
     // Add initFen field:
-    return res.slice(0, -1) + " " + res.split(' ')[1] + " -";
+    return res.slice(0, -1) + res.split(' ')[0] + " -";
   }
 
   setOtherVariables(fen) {
@@ -64,25 +62,133 @@ export class Synchrone2Rules extends Synchrone1Rules {
       parsedFen.whiteMove != "-"
         ? JSON.parse(parsedFen.whiteMove)
         : null;
-    // And initFen (not empty)
-    this.initFen = parsedFen.initFen;
+    // And initFen (could be empty)
+    this.initfenStack = [];
+    if (parsedFen.initFen != "-") this.initfenStack.push(parsedFen.initFen);
   }
 
   getPotentialMovesFrom([x, y]) {
     if (this.movesCount % 4 <= 1) return super.getPotentialMovesFrom([x, y]);
-    // TODO: either add a "blackMove' field in FEN (bof...),
-    // or write an helper function to detect from diff positions,
-    // which piece moved (if not disappeared!), which moves are valid.
-    // + do not forget pass move (king 2 king): always possible at stage 2.
-    return [];
+    // Diff current and old board to know which pieces have moved,
+    // and to deduce possible moves at stage 2.
+    const L = this.initfenStack.length;
+    let initBoard = V.GetBoard(this.initfenStack[L-1]);
+    let appeared = [];
+    const c = this.turn;
+    const oppCol = V.GetOppCol(c);
+    for (let i=0; i<8; i++) {
+      for (let j=0; j<8; j++) {
+        if (this.board[i][j] != initBoard[i][j]) {
+          if (this.board[i][j] != V.EMPTY) {
+            const color = this.board[i][j].charAt(0);
+            appeared.push({ c: color, x: i, y: j });
+            // Pawns capture in diagonal => the following fix.
+            // (Other way would be to redefine getPotentialPawnMoves()...)
+            if (color == oppCol) initBoard[i][j] = this.board[i][j];
+          }
+        }
+      }
+    }
+    const saveBoard = this.board;
+    this.board = initBoard;
+    const movesInit = super.getPotentialMovesFrom([x, y]);
+    this.board = saveBoard;
+    const target = appeared.find(a => a.c == oppCol);
+    let movesNow = super.getPotentialMovesFrom([x, y]).filter(m => {
+      return (
+        m.end.x == target.x &&
+        m.end.y == target.y &&
+        movesInit.some(mi => mi.end.x == m.end.x && mi.end.y == m.end.y)
+      );
+    });
+    const passTarget =
+      (x != this.kingPos[c][0] || y != this.kingPos[c][1]) ? c : oppCol;
+    movesNow.push(
+      new Move({
+        start: { x: x, y: y },
+        end: {
+          x: this.kingPos[passTarget][0],
+          y: this.kingPos[passTarget][1]
+        },
+        appear: [],
+        vanish: []
+      })
+    );
+    return movesNow;
+  }
+
+  filterValid(moves) {
+    if (moves.length == 0) return [];
+    if (moves.length == 1 && moves[0].vanish.length == 0) return moves;
+    // filterValid can be called when it's "not our turn":
+    const color = moves.find(m => m.vanish.length > 0).vanish[0].c;
+    return moves.filter(m => {
+      if (m.vanish.length == 0) return true;
+      const piece = m.vanish[0].p;
+      if (piece == V.KING) {
+        this.kingPos[color][0] = m.appear[0].x;
+        this.kingPos[color][1] = m.appear[0].y;
+      }
+      V.PlayOnBoard(this.board, m);
+      let res = !this.underCheck(color);
+      V.UndoOnBoard(this.board, m);
+      if (piece == V.KING) this.kingPos[color] = [m.start.x, m.start.y];
+      return res;
+    });
+  }
+
+  getPossibleMovesFrom([x, y]) {
+    return this.filterValid(this.getPotentialMovesFrom([x, y]));
   }
 
   play(move) {
+    if (this.movesCount % 4 == 0) this.initfenStack.push(this.getBaseFen());
     move.flags = JSON.stringify(this.aggregateFlags());
     // Do not play on board (would reveal the move...)
     this.turn = V.GetOppCol(this.turn);
     this.movesCount++;
-    this.postPlay(move);
+    if ([0, 3].includes(this.movesCount % 4)) this.postPlay(move);
+    else super.postPlay(move); //resolve synchrone move
+  }
+
+  postPlay(move) {
+    if (this.turn == 'b') {
+      // NOTE: whiteMove is used read-only, so no need to copy
+      this.whiteMove = move;
+      return;
+    }
+
+    // A full "deterministic" turn just ended: no need to resolve
+    const smove = {
+      appear: this.whiteMove.appear.concat(move.appear),
+      vanish: this.whiteMove.vanish.concat(move.vanish)
+    };
+    V.PlayOnBoard(this.board, smove);
+    move.whiteMove = this.whiteMove; //for undo
+    this.whiteMove = null;
+
+    // Update king position + flags
+    let kingAppear = { 'w': false, 'b': false };
+    for (let i=0; i < smove.appear.length; i++) {
+      if (smove.appear[i].p == V.KING) {
+        const c = smove.appear[i].c;
+        kingAppear[c] = true;
+        this.kingPos[c][0] = smove.appear[i].x;
+        this.kingPos[c][1] = smove.appear[i].y;
+      }
+    }
+    for (let i = 0; i < smove.vanish.length; i++) {
+      if (smove.vanish[i].p == V.KING) {
+        const c = smove.vanish[i].c;
+        if (!kingAppear[c]) {
+          this.kingPos[c][0] = -1;
+          this.kingPos[c][1] = -1;
+        }
+        break;
+      }
+    }
+    super.updateCastleFlags(smove);
+    move.smove = smove;
   }
 
   undo(move) {
@@ -92,12 +198,13 @@ export class Synchrone2Rules extends Synchrone1Rules {
       V.UndoOnBoard(this.board, move.smove);
     this.turn = V.GetOppCol(this.turn);
     this.movesCount--;
-    this.postUndo(move);
+    if (this.movesCount % 4 == 0) this.initfenStack.pop();
+    if (move.vanish.length > 0) super.postUndo(move);
   }
 
   getCurrentScore() {
     if (this.movesCount % 4 != 0)
-      // Turn (2 x white + black) not over yet
+      // Turn (2 x [white + black]) not over yet
       return "*";
     // Was a king captured?
     if (this.kingPos['w'][0] < 0) return "0-1";
@@ -119,4 +226,9 @@ export class Synchrone2Rules extends Synchrone1Rules {
     return (whiteCanMove ? "1-0" : "0-1");
   }
 
+  getNotation(move) {
+    if (move.vanish.length == 0) return "pass";
+    return super.getNotation(move);
+  }
+
 };
diff --git a/client/src/views/Rules.vue b/client/src/views/Rules.vue
index 2edd8a88..dc96ffb4 100644
--- a/client/src/views/Rules.vue
+++ b/client/src/views/Rules.vue
@@ -123,9 +123,8 @@ export default {
           // NOTE: game might be null
           this.$refs["compgame"].launchGame(game);
         });
-      } else {
-        this.$refs["compgame"].launchGame();
       }
+      else this.$refs["compgame"].launchGame();
     },
     // The user wants to stop the game:
     stopGame: function() {
-- 
2.44.0