From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 25 Jul 2023 14:50:27 +0000 (+0200)
Subject: Fix Convert, start Chaining
X-Git-Url: https://git.auder.net/variants/current/doc/css/img/pieces/%7B%7B?a=commitdiff_plain;h=b98feb3f6bb7e03319474f7a032e296750eb179f;p=xogo.git

Fix Convert, start Chaining
---

diff --git a/base_rules.js b/base_rules.js
index aea3f57..c0d9b60 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -97,6 +97,14 @@ export default class ChessRules {
     return true;
   }
 
+  // Allow to take (moving: not disappearing) own pieces?
+  get hasSelfCaptures() {
+    return (
+      this.options["recycle"] ||
+      (this.options["teleport"] && this.subTurnTeleport == 1)
+    );
+  }
+
   get hasReserve() {
     return (
       !!this.options["crazyhouse"] ||
@@ -1720,10 +1728,7 @@ export default class ChessRules {
       });
       Array.prototype.push.apply(squares, zenCaptures);
     }
-    if (
-      this.options["recycle"] ||
-      (this.options["teleport"] && this.subTurnTeleport == 1)
-    ) {
+    if (this.hasSelfCaptures) {
       const selfCaptures = this.findDestSquares(
         [x, y],
         {
diff --git a/variants.js b/variants.js
index f3565dd..182fb8f 100644
--- a/variants.js
+++ b/variants.js
@@ -25,6 +25,7 @@ const variants = [
   {name: 'Cannibal', desc: 'Capture powers'},
   {name: 'Capablanca', desc: 'Capablanca Chess', disp: 'Capablanca'},
   {name: 'Capture', desc: 'Mandatory captures'},
+  {name: 'Chaining', desc: 'Speed-up development'},
   {name: 'Chakart', desc: 'Capture the princess'},
   {name: 'Checkered', desc: 'Shared pieces'},
   {name: 'Checkless', desc: 'No-check mode'},
diff --git a/variants/Chaining/class.js b/variants/Chaining/class.js
new file mode 100644
index 0000000..5cd4c1d
--- /dev/null
+++ b/variants/Chaining/class.js
@@ -0,0 +1,117 @@
+import ChessRules from "/base_rules.js";
+import PiPo from "/utils/PiPo.js";
+import Move from "/utils/Move.js";
+
+export default class ChainingRules extends ChessRules {
+
+  static get Options() {
+    return {
+      select: C.Options.select,
+      input: C.Options.input,
+      styles: ["atomic", "capture", "crazyhouse", "cylinder", "dark", "zen"]
+    };
+  }
+
+  get hasSelfCaptures() {
+    return true;
+  }
+
+  canSelfTake() {
+    return true; //self captures induce chaining
+  }
+
+  setOtherVariables(fenParsed, pieceArray) {
+    super.setOtherVariables(fenParsed, pieceArray);
+    // Stack of "last move" only for intermediate chaining
+    this.lastMoveEnd = [];
+  }
+
+  getBasicMove([sx, sy], [ex, ey], tr) {
+    const L = this.lastMoveEnd.length;
+    const piece = (L >= 1 ? this.lastMoveEnd[L-1].p : null);
+    if (
+      this.board[ex][ey] == "" ||
+      this.getColor(ex, ey) == C.GetOppTurn(this.turn)
+    ) {
+      if (piece && !tr)
+        tr = {c: this.turn, p: piece};
+      let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
+      if (piece)
+        mv.vanish.pop(); //end of a chain: initial piece remains
+      return mv;
+    }
+    // (Self)Capture: initial, or inside a chain
+    const initPiece = (piece || this.getPiece(sx, sy)),
+          destPiece = this.getPiece(ex, ey);
+    let mv = new Move({
+      start: {x: sx, y: sy},
+      end: {x: ex, y: ey},
+      appear: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: this.turn,
+          p: (!!tr ? tr.p : initPiece)
+        })
+      ],
+      vanish: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: this.turn,
+          p: destPiece
+        })
+      ]
+    });
+    if (!piece) {
+      // Initial capture
+      mv.vanish.unshift(
+        new PiPo({
+          x: sx,
+          y: sy,
+          c: this.turn,
+          p: initPiece
+        })
+      );
+    }
+    mv.chained = destPiece; //easier (no need to detect it)
+    return mv;
+  }
+
+  getPiece(x, y) {
+    const L = this.lastMoveEnd.length;
+    if (L >= 1 && this.lastMoveEnd[L-1].x == x && this.lastMoveEnd[L-1].y == y)
+      return this.lastMoveEnd[L-1].p;
+    return super.getPiece(x, y);
+  }
+
+  getPotentialMovesFrom([x, y], color) {
+    const L = this.lastMoveEnd.length;
+    if (
+      L >= 1 &&
+      (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
+    ) {
+      // A self-capture was played: wrong square
+      return [];
+    }
+    return super.getPotentialMovesFrom([x, y], color);
+  }
+
+  isLastMove(move) {
+    return !move.chained;
+  }
+
+  postPlay(move) {
+    super.postPlay(move);
+    if (!!move.converted) {
+      this.lastMoveEnd.push({
+        x: move.end.x,
+        y: move.end.y,
+        p: move.chained
+      });
+    }
+    else
+      this.lastMoveEnd = [];
+  }
+
+};
diff --git a/variants/Chaining/rules.html b/variants/Chaining/rules.html
new file mode 100644
index 0000000..f27cd96
--- /dev/null
+++ b/variants/Chaining/rules.html
@@ -0,0 +1,7 @@
+<p>
+  You can "capture" your own pieces, and then move them from the capturing
+  square in the same turn, with potential chaining if the captured unit
+  makes a self-capture too.
+</p>
+
+<p class="author">Benjamin Auder (2021).</p>
diff --git a/variants/Chaining/style.css b/variants/Chaining/style.css
new file mode 100644
index 0000000..a3550bc
--- /dev/null
+++ b/variants/Chaining/style.css
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
diff --git a/variants/Convert/class.js b/variants/Convert/class.js
index 566feb5..37c2c3d 100644
--- a/variants/Convert/class.js
+++ b/variants/Convert/class.js
@@ -4,15 +4,10 @@ import Move from "/utils/Move.js";
 
 export default class ConvertRules extends ChessRules {
 
-  // TODO: options ? (balance progressive ok it seems?)
   static get Options() {
     return {
       select: C.Options.select,
-      input: C.Options.input,
-      styles: [
-        "atomic", "cannibal", "capture", "cylinder",
-        "dark", "madrasi", "rifle", "teleport"
-      ]
+      styles: ["cylinder", "dark", "recycle", "teleport"]
     };
   }
 
@@ -37,21 +32,18 @@ export default class ConvertRules extends ChessRules {
 
   getBasicMove([sx, sy], [ex, ey], tr) {
     const L = this.lastMoveEnd.length;
-    const lm = this.lastMoveEnd[L-1];
-    const piece = (!!lm ? lm.p : null);
-    const c = this.turn;
+    const piece = (L >= 1 ? this.lastMoveEnd[L-1].p : null);
     if (this.board[ex][ey] == "") {
       if (piece && !tr)
-        tr = {c: c, p: piece};
+        tr = {c: this.turn, p: piece};
       let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
       if (piece)
-        mv.vanish.pop();
+        mv.vanish.pop(); //end of a chain: initial piece remains
       return mv;
     }
     // Capture: initial, or inside a chain
-    const initPiece = (piece || this.getPiece(sx, sy));
-    const oppCol = C.GetOppTurn(c);
-    const oppPiece = this.getPiece(ex, ey);
+    const initPiece = (piece || this.getPiece(sx, sy)),
+          destPiece = this.getPiece(ex, ey);
     let mv = new Move({
       start: {x: sx, y: sy},
       end: {x: ex, y: ey},
@@ -59,7 +51,7 @@ export default class ConvertRules extends ChessRules {
         new PiPo({
           x: ex,
           y: ey,
-          c: c,
+          c: this.turn,
           p: (!!tr ? tr.p : initPiece)
         })
       ],
@@ -67,8 +59,8 @@ export default class ConvertRules extends ChessRules {
         new PiPo({
           x: ex,
           y: ey,
-          c: oppCol,
-          p: oppPiece
+          c: C.GetOppTurn(this.turn),
+          p: destPiece
         })
       ]
     });
@@ -78,11 +70,12 @@ export default class ConvertRules extends ChessRules {
         new PiPo({
           x: sx,
           y: sy,
-          c: c,
+          c: this.turn,
           p: initPiece
         })
       );
     }
+    mv.converted = destPiece; //easier (no need to detect it)
     return mv;
   }
 
@@ -165,7 +158,7 @@ export default class ConvertRules extends ChessRules {
     return false;
   }
 
-  underAttack([x, y], color) {
+  underAttack([x, y], [color]) {
     let explored = [];
     return this.underAttack_aux([x, y], color, explored);
   }
@@ -176,321 +169,20 @@ export default class ConvertRules extends ChessRules {
   }
 
   isLastMove(move) {
-    return (
-      super.isLastMove(move) ||
-      move.vanish.length <= 1 ||
-      move.vanish[1].c != move.vanish[0].c ||
-      move.appear.length == 2 //castle!
-    );
+    return !move.converted;
   }
 
   postPlay(move) {
     super.postPlay(move);
-    if (!this.isLastMove(move)) {
+    if (!!move.converted) {
       this.lastMoveEnd.push({
         x: move.end.x,
         y: move.end.y,
-        p: move.vanish[1].p //TODO: check this
+        p: move.converted
       });
     }
-  }
-
-};
-
-// TODO: wrong rules! mismatch Convert (taking opponent pieces) and chaining (tend to be this) taking own units (with normal initial position).
-// Initial Convert:
-
-import { ChessRules, PiPo, Move } from "@/base_rules";
-import { randInt } from "@/utils/alea";
-
-export class ConvertRules extends ChessRules {
-
-  static get HasEnpassant() {
-    return false;
-  }
-
-  setOtherVariables(fen) {
-    super.setOtherVariables(fen);
-    // Stack of "last move" only for intermediate chaining
-    this.lastMoveEnd = [null];
-  }
-
-  static GenRandInitFen(options) {
-    const baseFen = ChessRules.GenRandInitFen(options);
-    return (
-      baseFen.substr(0, 8) +
-      "/8/pppppppp/8/8/PPPPPPPP/8/" +
-      baseFen.substr(35, 17)
-    );
-  }
-
-  getBasicMove([sx, sy], [ex, ey], tr) {
-    const L = this.lastMoveEnd.length;
-    const lm = this.lastMoveEnd[L-1];
-    const piece = (!!lm ? lm.p : null);
-    const c = this.turn;
-    if (this.board[ex][ey] == V.EMPTY) {
-      if (!!piece && !tr) tr = { c: c, p: piece }
-      let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
-      if (!!piece) mv.vanish.pop();
-      return mv;
-    }
-    // Capture: initial, or inside a chain
-    const initPiece = (piece || this.getPiece(sx, sy));
-    const oppCol = V.GetOppCol(c);
-    const oppPiece = this.getPiece(ex, ey);
-    let mv = new Move({
-      start: { x: sx, y: sy },
-      end: { x: ex, y: ey },
-      appear: [
-        new PiPo({
-          x: ex,
-          y: ey,
-          c: c,
-          p: (!!tr ? tr.p : initPiece)
-        })
-      ],
-      vanish: [
-        new PiPo({
-          x: ex,
-          y: ey,
-          c: oppCol,
-          p: oppPiece
-        })
-      ]
-    });
-    if (!piece) {
-      // Initial capture
-      mv.vanish.unshift(
-        new PiPo({
-          x: sx,
-          y: sy,
-          c: c,
-          p: initPiece
-        })
-      );
-    }
-    // TODO: This "converted" indication isn't needed in fact,
-    // because it can be deduced from the move itself.
-    mv.end.converted = oppPiece;
-    return mv;
-  }
-
-  getPotentialMovesFrom([x, y], asA) {
-    const L = this.lastMoveEnd.length;
-    if (!!this.lastMoveEnd[L-1]) {
-      if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
-        // A capture was played: wrong square
-        return [];
-      asA = this.lastMoveEnd[L-1].p;
-    }
-    switch (asA || this.getPiece(x, y)) {
-      case V.PAWN: return super.getPotentialPawnMoves([x, y]);
-      case V.ROOK: return super.getPotentialRookMoves([x, y]);
-      case V.KNIGHT: return super.getPotentialKnightMoves([x, y]);
-      case V.BISHOP: return super.getPotentialBishopMoves([x, y]);
-      case V.QUEEN: return super.getPotentialQueenMoves([x, y]);
-      case V.KING: return super.getPotentialKingMoves([x, y]);
-    }
-    return [];
-  }
-
-  getPossibleMovesFrom(sq) {
-    const L = this.lastMoveEnd.length;
-    let asA = undefined;
-    if (!!this.lastMoveEnd[L-1]) {
-      if (
-        sq[0] != this.lastMoveEnd[L-1].x ||
-        sq[1] != this.lastMoveEnd[L-1].y
-      ) {
-        return [];
-      }
-      asA = this.lastMoveEnd[L-1].p;
-    }
-    return this.filterValid(this.getPotentialMovesFrom(sq, asA));
-  }
-
-  isAttacked_aux([x, y], color, explored) {
-    if (explored.some(sq => sq[0] == x && sq[1] == y))
-      // Start of an infinite loop: exit
-      return false;
-    explored.push([x, y]);
-    if (super.isAttacked([x, y], color)) return true;
-    // Maybe indirect "chaining" attack:
-    const myColor = this.turn
-    let res = false;
-    let toCheck = []; //check all but king (no need)
-    // Pawns:
-    const shiftToPawn = (myColor == 'w' ? -1 : 1);
-    for (let yShift of [-1, 1]) {
-      const [i, j] = [x + shiftToPawn, y + yShift];
-      if (
-        V.OnBoard(i, j) &&
-        this.board[i][j] != V.EMPTY &&
-        // NOTE: no need to check color (no enemy pawn can take directly)
-        this.getPiece(i, j) == V.PAWN
-      ) {
-        toCheck.push([i, j]);
-      }
-    }
-    // Knights:
-    V.steps[V.KNIGHT].forEach(s => {
-      const [i, j] = [x + s[0], y + s[1]];
-      if (
-        V.OnBoard(i, j) &&
-        this.board[i][j] != V.EMPTY &&
-        this.getPiece(i, j) == V.KNIGHT
-      ) {
-        toCheck.push([i, j]);
-      }
-    });
-    // Sliders:
-    V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(s => {
-      let [i, j] = [x + s[0], y + s[1]];
-      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
-        i += s[0];
-        j += s[1];
-      }
-      if (!V.OnBoard(i, j)) return;
-      const piece = this.getPiece(i, j);
-      if (
-        piece == V.QUEEN ||
-        (piece == V.ROOK && (s[0] == 0 || s[1] == 0)) ||
-        (piece == V.BISHOP && (s[0] != 0 && s[1] != 0))
-      ) {
-        toCheck.push([i, j]);
-      }
-    });
-    for (let ij of toCheck) {
-      if (this.isAttacked_aux(ij, color, explored)) return true;
-    }
-    return false;
-  }
-
-  isAttacked([x, y], color) {
-    let explored = [];
-    return this.isAttacked_aux([x, y], color, explored);
-  }
-
-  filterValid(moves) {
-    // No "checks" (except to forbid castle)
-    return moves;
-  }
-
-  getCheckSquares() {
-    return [];
-  }
-
-  prePlay(move) {
-    const c = this.turn;
-    // Extra conditions to avoid tracking converted kings:
-    if (
-      move.appear[0].p == V.KING &&
-      move.vanish.length >= 1 &&
-      move.vanish[0].p == V.KING
-    ) {
-      this.kingPos[c][0] = move.appear[0].x;
-      this.kingPos[c][1] = move.appear[0].y;
-    }
-  }
-
-  play(move) {
-    this.prePlay(move);
-    const c = this.turn;
-    move.flags = JSON.stringify(this.aggregateFlags());
-    V.PlayOnBoard(this.board, move);
-    if (!move.end.converted) {
-      // Not a capture: change turn
-      this.turn = V.GetOppCol(this.turn);
-      this.movesCount++;
-      this.lastMoveEnd.push(null);
-    }
-    else {
-      this.lastMoveEnd.push(
-        Object.assign({}, move.end, { p: move.end.converted })
-      );
-    }
-    super.updateCastleFlags(move, move.appear[0].p, c);
-  }
-
-  undo(move) {
-    this.disaggregateFlags(JSON.parse(move.flags));
-    this.lastMoveEnd.pop();
-    V.UndoOnBoard(this.board, move);
-    if (!move.end.converted) {
-      this.turn = V.GetOppCol(this.turn);
-      this.movesCount--;
-    }
-    this.postUndo(move);
-  }
-
-  postUndo(move) {
-    const c = this.getColor(move.start.x, move.start.y);
-    if (
-      move.appear[0].p == V.KING &&
-      move.vanish.length >= 1 &&
-      move.vanish[0].p == V.KING
-    ) {
-      this.kingPos[c] = [move.start.x, move.start.y];
-    }
-  }
-
-  getCurrentScore() {
-    const color = this.turn;
-    const kp = this.kingPos[color];
-    if (this.getColor(kp[0], kp[1]) != color)
-      return (color == "w" ? "0-1" : "1-0");
-    if (!super.atLeastOneMove()) return "1/2";
-    return "*";
-  }
-
-  getComputerMove() {
-    let initMoves = this.getAllValidMoves();
-    if (initMoves.length == 0) return null;
-    // Loop until valid move is found (no blocked pawn conversion...)
-    while (true) {
-      let moves = JSON.parse(JSON.stringify(initMoves));
-      let mvArray = [];
-      let mv = null;
-      // Just play random moves (for now at least. TODO?)
-      while (moves.length > 0) {
-        mv = moves[randInt(moves.length)];
-        mvArray.push(mv);
-        this.play(mv);
-        if (!!mv.end.converted)
-          // A piece was just converted
-          moves = this.getPotentialMovesFrom([mv.end.x, mv.end.y]);
-        else break;
-      }
-      for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
-      if (!mv.end.converted) return (mvArray.length > 1 ? mvArray : mvArray[0]);
-    }
-    return null; //never reached
-  }
-
-  getNotation(move) {
-    if (move.appear.length == 2 && move.appear[0].p == V.KING)
-      return (move.end.y < move.start.y ? "0-0-0" : "0-0");
-    const c = this.turn;
-    const L = this.lastMoveEnd.length;
-    const lm = this.lastMoveEnd[L-1];
-    const piece = (!lm ? move.appear[0].p : lm.p);
-    // Basic move notation:
-    let notation = piece.toUpperCase();
-    if (
-      this.board[move.end.x][move.end.y] != V.EMPTY ||
-      (piece == V.PAWN && move.start.y != move.end.y)
-    ) {
-      notation += "x";
-    }
-    const finalSquare = V.CoordsToSquare(move.end);
-    notation += finalSquare;
-
-    // Add potential promotion indications:
-    const firstLastRank = (c == 'w' ? [7, 0] : [0, 7]);
-    if (move.end.x == firstLastRank[1] && piece == V.PAWN)
-      notation += "=" + move.appear[0].p.toUpperCase();
-    return notation;
+    else
+      this.lastMoveEnd = [];
   }
 
 };
diff --git a/variants/Convert/rules.html b/variants/Convert/rules.html
index f27cd96..5221fab 100644
--- a/variants/Convert/rules.html
+++ b/variants/Convert/rules.html
@@ -1,7 +1,6 @@
 <p>
-  You can "capture" your own pieces, and then move them from the capturing
-  square in the same turn, with potential chaining if the captured unit
-  makes a self-capture too.
+  Enemy units are converted to your color after capture (not removed from board).
+  They must be moved from the destination square (potentially capturing too).
 </p>
 
 <p class="author">Benjamin Auder (2021).</p>