Clorange: almost functional draft (promotions, capture non-violent...)
[xogo.git] / base_rules.js
index a8dad15..20ffbee 100644 (file)
@@ -122,6 +122,11 @@ export default class ChessRules {
     return false;
   }
 
+  // Some variants do not flip board as black
+  get flippedBoard() {
+    return (this.playerColor == 'b');
+  }
+
   // Some variants use click infos:
   doClick(coords) {
     if (typeof coords.x != "number")
@@ -403,14 +408,14 @@ export default class ChessRules {
   }
 
   // Some additional variables from FEN (variant dependant)
-  setOtherVariables(fenParsed) {
+  setOtherVariables(fenParsed, pieceArray) {
     // Set flags and enpassant:
     if (this.hasFlags)
       this.setFlags(fenParsed.flags);
     if (this.hasEnpassant)
       this.epSquare = this.getEpSquare(fenParsed.enpassant);
     if (this.hasReserve && !this.isDiagram)
-      this.initReserves(fenParsed.reserve);
+      this.initReserves(fenParsed.reserve, pieceArray);
     if (this.options["crazyhouse"])
       this.initIspawn(fenParsed.ispawn);
     if (this.options["teleport"]) {
@@ -566,7 +571,7 @@ export default class ChessRules {
 
   // Get SVG board (background, no pieces)
   getSvgChessboard() {
-    const flipped = (this.playerColor == 'b');
+    const flipped = this.flippedBoard;
     let board = `
       <svg
         viewBox="0 0 ${10*this.size.y} ${10*this.size.x}"
@@ -830,7 +835,7 @@ export default class ChessRules {
     }
     else {
       const sqSize = r.width / this.size.y;
-      const flipped = (this.playerColor == 'b');
+      const flipped = this.flippedBoard;
       x = (flipped ? this.size.y - 1 - j : j) * sqSize;
       y = (flipped ? this.size.x - 1 - i : i) * sqSize;
     }
@@ -1067,7 +1072,7 @@ export default class ChessRules {
     // TODO: (0, 0) is wrong, would need to place an attacker here...
     const steps = this.pieces(this.playerColor, 0, 0)["p"].attack[0].steps;
     for (let step of steps) {
-      const x = this.epSquare.x - step[0],
+      const x = this.epSquare.x - step[0], //NOTE: epSquare.x not on edge
             y = this.getY(this.epSquare.y - step[1]);
       if (
         this.onBoard(x, y) &&
@@ -1178,18 +1183,19 @@ export default class ChessRules {
   getPawnShift(color) {
     return (color == "w" ? -1 : 1);
   }
+  isPawnInitRank(x, color) {
+    return (color == 'w' && x >= 6) || (color == 'b' && x <= 1);
+  }
 
   pieces(color, x, y) {
     const pawnShift = this.getPawnShift(color);
-    // NOTE: jump 2 squares from first rank (pawns can be here sometimes)
-    const initRank = ((color == 'w' && x >= 6) || (color == 'b' && x <= 1));
     return {
       'p': {
         "class": "pawn",
         moves: [
           {
             steps: [[pawnShift, 0]],
-            range: (initRank ? 2 : 1)
+            range: (this.isPawnInitRank(x, color) ? 2 : 1)
           }
         ],
         attack: [
@@ -1290,6 +1296,17 @@ export default class ChessRules {
       res += this.size.y;
     return res;
   }
+  // Circular?
+  getX(x) {
+    return x; //generally, no
+  }
+
+  increment([x, y], step) {
+    return [
+      this.getX(x + step[0]),
+      this.getY(y + step[1])
+    ];
+  }
 
   getSegments(curSeg, segStart, segEnd) {
     if (curSeg.length == 0)
@@ -1321,6 +1338,11 @@ export default class ChessRules {
     return this.getColor(x1, y1) !== this.getColor(x2, y2);
   }
 
+  // Teleport & Recycle. Assumption: color(x1,y1) == color(x2,y2)
+  canSelfTake([x1, y1], [x2, y2]) {
+    return !this.isKing(x2, y2);
+  }
+
   canStepOver(i, j, p) {
     // In some variants, objects on boards don't stop movement (Chakart)
     return this.board[i][j] == "";
@@ -1350,13 +1372,12 @@ export default class ChessRules {
     const attacks = stepSpec.both.concat(stepSpec.attack);
     for (let a of attacks) {
       outerLoop: for (let step of a.steps) {
-        let [i, j] = [x + step[0], y + step[1]];
-        let stepCounter = 1;
+        let [i, j] = this.increment([x, y], step);
+        let stepCounter = 0;
         while (this.onBoard(i, j) && this.board[i][j] == "") {
           if (a.range <= stepCounter++)
             continue outerLoop;
-          i += step[0];
-          j = this.getY(j + step[1]);
+          [i, j] = this.increment([i, j], step);
         }
         if (
           this.onBoard(i, j) &&
@@ -1392,7 +1413,6 @@ export default class ChessRules {
                 {
                   attackOnly: true,
                   one: true,
-                  segments: this.options["cylinder"]
                 },
                 allowed
               )
@@ -1403,10 +1423,7 @@ export default class ChessRules {
                 this.options["zen"] &&
                 this.findCapturesOn(
                   [i, j],
-                  {
-                    one: true,
-                    segments: this.options["cylinder"]
-                  },
+                  {one: true},
                   allowed
                 )
               )
@@ -1483,6 +1500,7 @@ export default class ChessRules {
   }
 
   // All possible moves from selected square
+  // TODO: generalize usage if arg "color" (e.g. Checkered)
   getPotentialMovesFrom([x, y], color) {
     if (this.subTurnTeleport == 2)
       return [];
@@ -1564,8 +1582,7 @@ export default class ChessRules {
           vanish: []
         });
         for (let step of steps) {
-          let x = m.end.x + step[0];
-          let y = this.getY(m.end.y + step[1]);
+          let [x, y] = this.increment([m.end.x, m.end.y], step);
           if (
             this.onBoard(x, y) &&
             this.board[x][y] != "" &&
@@ -1653,7 +1670,7 @@ export default class ChessRules {
         m.appear[0].p = this.pawnPromotions[0];
         for (let i=1; i<this.pawnPromotions.length; i++) {
           let newMv = JSON.parse(JSON.stringify(m));
-          newMv.appear[0].p = this.pawnSpecs.promotions[i];
+          newMv.appear[0].p = this.pawnPromotions[i];
           newMoves.push(newMv);
         }
       }
@@ -1671,7 +1688,6 @@ export default class ChessRules {
         [x, y],
         {
           attackOnly: true,
-          segments: this.options["cylinder"],
           stepSpec: stepSpec
         },
         ([i1, j1], [i2, j2]) => {
@@ -1686,7 +1702,6 @@ export default class ChessRules {
       [x, y],
       {
         moveOnly: !!stepSpec.attack || this.options["zen"],
-        segments: this.options["cylinder"],
         stepSpec: stepSpec
       }
     );
@@ -1699,11 +1714,10 @@ export default class ChessRules {
           !this.isKing(i1, j1) && this.canTake([i2, j2], [i1, j1])
       );
       // Technical step: segments (if any) are reversed
-      if (this.options["cylinder"]) {
-        zenCaptures.forEach(z => {
+      zenCaptures.forEach(z => {
+        if (!!z.segments)
           z.segments = z.segments.reverse().map(s => s.reverse())
-        });
-      }
+      });
       Array.prototype.push.apply(squares, zenCaptures);
     }
     if (
@@ -1714,17 +1728,20 @@ export default class ChessRules {
         [x, y],
         {
           attackOnly: true,
-          segments: this.options["cylinder"],
           stepSpec: stepSpec
         },
-        ([i1, j1], [i2, j2]) =>
-          this.getColor(i2, j2) == color && !this.isKing(i2, j2)
+        ([i1, j1], [i2, j2]) => {
+          return (
+            this.getColor(i2, j2) == color &&
+            this.canSelfTake([i1, j1], [i2, j2])
+          );
+        }
       );
       Array.prototype.push.apply(squares, selfCaptures);
     }
     return squares.map(s => {
       let mv = this.getBasicMove([x, y], s.sq);
-      if (this.options["cylinder"] && s.segments.length >= 2)
+      if (!!s.segments)
         mv.segments = s.segments;
       return mv;
     });
@@ -1735,23 +1752,21 @@ export default class ChessRules {
       allowed = (sq1, sq2) => this.canTake(sq1, sq2);
     const apparentPiece = this.getPiece(x, y); //how it looks
     let res = [];
-    // Next 3 for Cylinder mode: (unused if !o.segments)
+    // Next 3 for Cylinder mode or circular (useless otherwise)
     let explored = {};
     let segments = [];
     let segStart = [];
     const addSquare = ([i, j]) => {
       let elt = {sq: [i, j]};
-      if (o.segments)
-        elt.segments = this.getSegments(segments, segStart, end);
+      if (segments.length >= 1)
+        elt.segments = this.getSegments(segments, segStart, [i, j]);
       res.push(elt);
     };
     const exploreSteps = (stepArray, mode) => {
       for (let s of stepArray) {
         outerLoop: for (let step of s.steps) {
-          if (o.segments) {
-            segments = [];
-            segStart = [x, y];
-          }
+          segments = [];
+          segStart = [x, y];
           let [i, j] = [x, y];
           let stepCounter = 0;
           while (
@@ -1775,10 +1790,9 @@ export default class ChessRules {
             if (s.range <= stepCounter++)
               continue outerLoop;
             const oldIJ = [i, j];
-            i += step[0];
-            j = this.getY(j + step[1]);
-            if (o.segments && Math.abs(j - oldIJ[1]) > 1) {
-              // Boundary between segments (cylinder mode)
+            [i, j] = this.increment([i, j], step);
+            if (Math.abs(j - oldIJ[1]) > 1 || Math.abs(i - oldIJ[0]) > 1) {
+              // Boundary between segments (cylinder or circular mode)
               segments.push([[segStart[0], segStart[1]], oldIJ]);
               segStart = [i, j];
             }
@@ -1849,7 +1863,6 @@ export default class ChessRules {
                 {
                   captureTarget: [x, y],
                   captureSteps: [{steps: [s], range: a.range}],
-                  segments: o.segments
                 },
                 allowed
               );
@@ -1980,7 +1993,7 @@ export default class ChessRules {
     const oppCols = this.getOppCols(color);
     if (
       this.epSquare &&
-      this.epSquare.x == x + shiftX &&
+      this.epSquare.x == x + shiftX && //NOTE: epSquare.x not on edge
       Math.abs(this.getY(this.epSquare.y - y)) == 1 &&
       // Doublemove (and Progressive?) guards:
       this.board[this.epSquare.x][this.epSquare.y] == "" &&
@@ -2119,7 +2132,6 @@ export default class ChessRules {
           [x, y],
           {
             byCol: oppCols,
-            segments: this.options["cylinder"],
             one: true
           }
         )
@@ -2131,7 +2143,6 @@ export default class ChessRules {
           [x, y],
           {
             attackOnly: true,
-            segments: this.options["cylinder"],
             one: true
           },
           ([i1, j1], [i2, j2]) => oppCols.includes(this.getColor(i2, j2))
@@ -2159,6 +2170,35 @@ export default class ChessRules {
     return res;
   }
 
+  // cb: callback returning a boolean (false if king missing)
+  trackKingWrap(move, kingPos, cb) {
+    if (move.appear.length == 0 && move.vanish.length == 0)
+      return true;
+    const color =
+      (move.vanish.length > 0 ? move.vanish[0].c : move.appear[0].c);
+    let newKingPP = null,
+        sqIdx = 0,
+        res = true; //a priori valid
+    const oldKingPP =
+      move.vanish.find(v => this.isKing(0, 0, v.p) && v.c == color);
+    if (oldKingPP) {
+      // Search king in appear array:
+      newKingPP =
+        move.appear.find(a => this.isKing(0, 0, a.p) && a.c == color);
+      if (newKingPP) {
+        sqIdx = kingPos.findIndex(kp =>
+          kp[0] == oldKingPP.x && kp[1] == oldKingPP.y);
+        kingPos[sqIdx] = [newKingPP.x, newKingPP.y];
+      }
+      else
+        res = false; //king vanished
+    }
+    res &&= cb(kingPos);
+    if (oldKingPP && newKingPP)
+      kingPos[sqIdx] = [oldKingPP.x, oldKingPP.y];
+    return res;
+  }
+
   // 'color' arg because some variants (e.g. Refusal) check opponent moves
   filterValid(moves, color) {
     if (!color)
@@ -2167,26 +2207,9 @@ export default class ChessRules {
     let kingPos = this.searchKingPos(color);
     return moves.filter(m => {
       this.playOnBoard(m);
-      let newKingPP = null,
-          sqIdx = 0,
-          res = true; //a priori valid
-      const oldKingPP =
-        m.vanish.find(v => this.isKing(0, 0, v.p) && v.c == color);
-      if (oldKingPP) {
-        // Search king in appear array:
-        newKingPP =
-          m.appear.find(a => this.isKing(0, 0, a.p) && a.c == color);
-        if (newKingPP) {
-          sqIdx = kingPos.findIndex(kp =>
-            kp[0] == oldKingPP.x && kp[1] == oldKingPP.y);
-          kingPos[sqIdx] = [newKingPP.x, newKingPP.y];
-        }
-        else
-          res = false; //king vanished
-      }
-      res &&= !this.underCheck(kingPos, oppCols);
-      if (oldKingPP && newKingPP)
-        kingPos[sqIdx] = [oldKingPP.x, oldKingPP.y];
+      const res = this.trackKingWrap(m, kingPos, (kp) => {
+        return !this.underCheck(kp, oppCols);
+      });
       this.undoOnBoard(m);
       return res;
     });
@@ -2320,7 +2343,7 @@ export default class ChessRules {
 
   tryChangeTurn(move) {
     if (this.isLastMove(move)) {
-      this.turn = (this.turn == 'w' ? 'b' : 'w');
+      this.turn = C.GetOppTurn(this.turn);
       this.movesCount++;
       this.subTurn = 1;
     }
@@ -2362,7 +2385,7 @@ export default class ChessRules {
         if (this.board[i][j] != "" && this.getColor(i, j) == color) {
           // NOTE: in fact searching for all potential moves from i,j.
           //       I don't believe this is an issue, for now at least.
-          const moves = this.getPotentialMovesFrom([i, j]);
+          const moves = this.getPotentialMovesFrom([i, j], color);
           if (moves.some(m => this.filterValid([m]).length >= 1))
             return true;
         }
@@ -2406,7 +2429,8 @@ export default class ChessRules {
 
   playVisual(move, r) {
     move.vanish.forEach(v => {
-      this.g_pieces[v.x][v.y].remove();
+      if (this.g_pieces[v.x][v.y]) //can be null (e.g. Apocalypse)
+        this.g_pieces[v.x][v.y].remove();
       this.g_pieces[v.x][v.y] = null;
     });
     let chessboard =
@@ -2475,6 +2499,10 @@ export default class ChessRules {
 
   animateMoving(start, end, drag, segments, cb) {
     let initPiece = this.getDomPiece(start.x, start.y);
+    if (!initPiece) { //TODO: shouldn't occur!
+      cb();
+      return;
+    }
     // NOTE: cloning often not required, but light enough, and simpler
     let movingPiece = initPiece.cloneNode();
     initPiece.style.opacity = "0";