Fix FenUtil.setupPieces and Antiking variants
[xogo.git] / base_rules.js
index 0b7d6ed..7dc134d 100644 (file)
@@ -1,5 +1,6 @@
-import { Random } from "/utils/alea.js";
-import { ArrayFun } from "/utils/array.js";
+import {Random} from "/utils/alea.js";
+import {ArrayFun} from "/utils/array.js";
+import {FenUtil} from "/utils/setupPieces.js";
 import PiPo from "/utils/PiPo.js";
 import Move from "/utils/Move.js";
 
@@ -208,74 +209,27 @@ export default class ChessRules {
     baseFen.o = Object.assign({init: true}, baseFen.o);
     const parts = this.getPartFen(baseFen.o);
     return (
-      baseFen.fen +
+      baseFen.fen + " w 0" +
       (Object.keys(parts).length > 0 ? (" " + JSON.stringify(parts)) : "")
     );
   }
 
   // Setup the initial random-or-not (asymmetric-or-not) position
   genRandInitBaseFen() {
-    let fen, flags = "0707";
-    if (!this.options.randomness)
-      // Deterministic:
-      fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0";
-
-    else {
-      // Randomize
-      let pieces = {w: new Array(8), b: new Array(8)};
-      flags = "";
-      // Shuffle pieces on first (and last rank if randomness == 2)
-      for (let c of ["w", "b"]) {
-        if (c == 'b' && this.options.randomness == 1) {
-          pieces['b'] = pieces['w'];
-          flags += flags;
-          break;
-        }
-        let positions = ArrayFun.range(8);
-        // Get random squares for bishops
-        let randIndex = 2 * Random.randInt(4);
-        const bishop1Pos = positions[randIndex];
-        // The second bishop must be on a square of different color
-        let randIndex_tmp = 2 * Random.randInt(4) + 1;
-        const bishop2Pos = positions[randIndex_tmp];
-        // Remove chosen squares
-        positions.splice(Math.max(randIndex, randIndex_tmp), 1);
-        positions.splice(Math.min(randIndex, randIndex_tmp), 1);
-        // Get random squares for knights
-        randIndex = Random.randInt(6);
-        const knight1Pos = positions[randIndex];
-        positions.splice(randIndex, 1);
-        randIndex = Random.randInt(5);
-        const knight2Pos = positions[randIndex];
-        positions.splice(randIndex, 1);
-        // Get random square for queen
-        randIndex = Random.randInt(4);
-        const queenPos = positions[randIndex];
-        positions.splice(randIndex, 1);
-        // Rooks and king positions are now fixed,
-        // because of the ordering rook-king-rook
-        const rook1Pos = positions[0];
-        const kingPos = positions[1];
-        const rook2Pos = positions[2];
-        // Finally put the shuffled pieces in the board array
-        pieces[c][rook1Pos] = "r";
-        pieces[c][knight1Pos] = "n";
-        pieces[c][bishop1Pos] = "b";
-        pieces[c][queenPos] = "q";
-        pieces[c][kingPos] = "k";
-        pieces[c][bishop2Pos] = "b";
-        pieces[c][knight2Pos] = "n";
-        pieces[c][rook2Pos] = "r";
-        flags += rook1Pos.toString() + rook2Pos.toString();
+    const s = FenUtil.setupPieces(
+      ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'],
+      {
+        randomness: this.options["randomness"],
+        between: {p1: 'k', p2: 'r'},
+        diffCol: ['b'],
+        flags: ['r']
       }
-      fen = (
-        pieces["b"].join("") +
-        "/pppppppp/8/8/8/8/PPPPPPPP/" +
-        pieces["w"].join("").toUpperCase() +
-        " w 0"
-      );
-    }
-    return { fen: fen, o: {flags: flags} };
+    );
+    return {
+      fen: s.b.join("") + "/pppppppp/8/8/8/8/PPPPPPPP/" +
+           s.w.join("").toUpperCase(),
+      o: {flags: s.flags}
+    };
   }
 
   // "Parse" FEN: just return untransformed string data
@@ -405,9 +359,6 @@ export default class ChessRules {
       if (this.options[opt.variable] === undefined)
         this.options[opt.variable] = opt.defaut;
     });
-    if (o.genFenOnly)
-      // This object will be used only for initial FEN generation
-      return;
 
     // Some variables
     this.playerColor = o.color;
@@ -477,17 +428,15 @@ export default class ChessRules {
   }
 
   // ordering as in pieces() p,r,n,b,q,k
-  initReserves(reserveStr) {
+  initReserves(reserveStr, pieceArray) {
+    if (!pieceArray)
+      pieceArray = ['p', 'r', 'n', 'b', 'q', 'k'];
     const counts = reserveStr.split("").map(c => parseInt(c, 36));
-    this.reserve = { w: {}, b: {} };
-    const pieceName = ['p', 'r', 'n', 'b', 'q', 'k'];
-    const L = pieceName.length;
-    for (let i of ArrayFun.range(2 * L)) {
-      if (i < L)
-        this.reserve['w'][pieceName[i]] = counts[i];
-      else
-        this.reserve['b'][pieceName[i-L]] = counts[i];
-    }
+    const L = pieceArray.length;
+    this.reserve = {
+      w: ArrayFun.toObject(pieceArray, counts.slice(0, L)),
+      b: ArrayFun.toObject(pieceArray, counts.slice(L, 2 * L))
+    };
   }
 
   initIspawn(ispawnStr) {
@@ -528,20 +477,16 @@ export default class ChessRules {
       (oldV,newV) => oldV + (this.reserve[c][newV] > 0 ? 1 : 0), 0);
   }
 
-  static AddClass_es(piece, class_es) {
+  static AddClass_es(elt, class_es) {
     if (!Array.isArray(class_es))
       class_es = [class_es];
-    class_es.forEach(cl => {
-      piece.classList.add(cl);
-    });
+    class_es.forEach(cl => elt.classList.add(cl));
   }
 
-  static RemoveClass_es(piece, class_es) {
+  static RemoveClass_es(elt, class_es) {
     if (!Array.isArray(class_es))
       class_es = [class_es];
-    class_es.forEach(cl => {
-      piece.classList.remove(cl);
-    });
+    class_es.forEach(cl => elt.classList.remove(cl));
   }
 
   // Generally light square bottom-right
@@ -612,7 +557,7 @@ export default class ChessRules {
     chessboard.style.top = spaceTop + "px";
     // Give sizes instead of recomputing them,
     // because chessboard might not be drawn yet.
-    this.setupPieces({
+    this.setupVisualPieces({
       width: cbWidth,
       height: cbHeight,
       x: spaceLeft,
@@ -629,6 +574,8 @@ export default class ChessRules {
         class="chessboard_SVG">`;
     for (let i=0; i < this.size.x; i++) {
       for (let j=0; j < this.size.y; j++) {
+        if (!this.onBoard(i, j))
+          continue;
         const ii = (flipped ? this.size.x - 1 - i : i);
         const jj = (flipped ? this.size.y - 1 - j : j);
         let classes = this.getSquareColorClass(ii, jj);
@@ -650,7 +597,7 @@ export default class ChessRules {
     return board;
   }
 
-  setupPieces(r) {
+  setupVisualPieces(r) {
     let chessboard =
       document.getElementById(this.containerId).querySelector(".chessboard");
     if (!r)
@@ -784,8 +731,8 @@ export default class ChessRules {
       piece = "k"; //capturing cannibal king: back to king form
     const oldCount = this.reserve[color][piece];
     this.reserve[color][piece] = count;
-    // Redrawing is much easier if count==0
-    if ([oldCount, count].includes(0))
+    // Redrawing is much easier if count==0 (or undefined)
+    if ([oldCount, count].some(item => !item))
       this.re_drawReserve([color]);
     else {
       const numId = this.getReserveNumId(color, piece);
@@ -1018,11 +965,10 @@ export default class ChessRules {
     // TODO: onpointerdown/move/up ? See reveal.js /controllers/touch.js
   }
 
+  // NOTE: not called if isDiagram
   removeListeners() {
     let container = document.getElementById(this.containerId);
     this.windowResizeObs.unobserve(container);
-    if (this.isDiagram)
-      return; //no listeners in this case
     if ('onmousedown' in window) {
       this.mouseListeners.forEach(ml => {
         document.removeEventListener(ml.type, ml.listener);
@@ -1247,13 +1193,13 @@ export default class ChessRules {
       },
       'r': {
         "class": "rook",
-        moves: [
+        both: [
           {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
         ]
       },
       'n': {
         "class": "knight",
-        moves: [
+        both: [
           {
             steps: [
               [1, 2], [1, -2], [-1, 2], [-1, -2],
@@ -1265,13 +1211,13 @@ export default class ChessRules {
       },
       'b': {
         "class": "bishop",
-        moves: [
+        both: [
           {steps: [[1, 1], [1, -1], [-1, 1], [-1, -1]]}
         ]
       },
       'q': {
         "class": "queen",
-        moves: [
+        both: [
           {
             steps: [
               [0, 1], [0, -1], [1, 0], [-1, 0],
@@ -1282,7 +1228,7 @@ export default class ChessRules {
       },
       'k': {
         "class": "king",
-        moves: [
+        both: [
           {
             steps: [
               [0, 1], [0, -1], [1, 0], [-1, 0],
@@ -1346,7 +1292,20 @@ export default class ChessRules {
   }
 
   getStepSpec(color, x, y, piece) {
-    return this.pieces(color, x, y)[piece || this.getPieceType(x, y)];
+    let pieceType = piece;
+    let allSpecs = this.pieces(color, x, y);
+    if (!piece)
+      pieceType = this.getPieceType(x, y);
+    else if (allSpecs[piece].moveas)
+      pieceType = allSpecs[piece].moveas;
+    let res = allSpecs[pieceType];
+    if (!res["both"])
+      res.both = [];
+    if (!res["moves"])
+      res.moves = [];
+    if (!res["attack"])
+      res.attack = [];
+    return res;
   }
 
   // Can thing on square1 capture thing on square2?
@@ -1380,7 +1339,7 @@ export default class ChessRules {
     const oppCol = C.GetOppCol(color);
     const piece = this.getPieceType(x, y);
     const stepSpec = this.getStepSpec(color, x, y, piece);
-    const attacks = stepSpec.attack || stepSpec.moves;
+    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]];
@@ -1494,7 +1453,7 @@ export default class ChessRules {
     let moves = [];
     for (let i=0; i<this.size.x; i++) {
       for (let j=0; j<this.size.y; j++) {
-        if (this.canDrop([c, p], [i, j])) {
+        if (this.onBoard(i, j) && this.canDrop([c, p], [i, j])) {
           let mv = new Move({
             start: {x: c, y: p},
             end: {x: i, y: j},
@@ -1779,7 +1738,7 @@ export default class ChessRules {
         elt.segments = this.getSegments(segments, segStart, end);
       res.push(elt);
     };
-    const exploreSteps = (stepArray) => {
+    const exploreSteps = (stepArray, mode) => {
       for (let s of stepArray) {
         outerLoop: for (let step of s.steps) {
           if (o.segments) {
@@ -1798,9 +1757,9 @@ export default class ChessRules {
                 !o.captureTarget ||
                 (o.captureTarget[0] == i && o.captureTarget[1] == j)
               ) {
-                if (o.one && !o.attackOnly)
+                if (o.one && mode != "attack")
                   return true;
-                if (!o.attackOnly)
+                if (mode != "attack")
                   addSquare(!o.captureTarget ? [i, j] : [x, y]);
                 if (o.captureTarget)
                   return res[0];
@@ -1823,9 +1782,9 @@ export default class ChessRules {
           if (!explored[i + "." + j]) {
             explored[i + "." + j] = true;
             if (allowed([x, y], [i, j])) {
-              if (o.one && !o.moveOnly)
+              if (o.one && mode != "moves")
                 return true;
-              if (!o.moveOnly)
+              if (mode != "moves")
                 addSquare(!o.captureTarget ? [i, j] : [x, y]);
               if (
                 o.captureTarget &&
@@ -1840,17 +1799,15 @@ export default class ChessRules {
       return undefined; //default, but let's explicit it
     };
     if (o.captureTarget)
-      return exploreSteps(o.captureSteps)
+      return exploreSteps(o.captureSteps, "attack");
     else {
       const stepSpec =
         o.stepSpec || this.getStepSpec(this.getColor(x, y), x, y);
       let outOne = false;
-      if (!o.attackOnly || !stepSpec.attack)
-        outOne = exploreSteps(stepSpec.moves);
-      if (!outOne && !o.moveOnly && !!stepSpec.attack) {
-        o.attackOnly = true; //ok because o is always a temporary object
-        outOne = exploreSteps(stepSpec.attack);
-      }
+      if (!o.attackOnly)
+        outOne = exploreSteps(stepSpec.both.concat(stepSpec.moves), "moves");
+      if (!outOne && !o.moveOnly)
+        outOne = exploreSteps(stepSpec.both.concat(stepSpec.attack), "attack");
       return (o.one ? outOne : res);
     }
   }
@@ -1873,7 +1830,7 @@ export default class ChessRules {
           if (this.canStepOver(x, y, apparentPiece))
             continue;
           const stepSpec = this.getStepSpec(colIJ, i, j);
-          const attacks = stepSpec.attack || stepSpec.moves;
+          const attacks = stepSpec.attack.concat(stepSpec.both);
           for (let a of attacks) {
             for (let s of a.steps) {
               // Quick check: if step isn't compatible, don't even try
@@ -2075,7 +2032,7 @@ export default class ChessRules {
           // will be executed in filterValid() later.
           (
             i != finalSquares[castleSide][0] &&
-            this.underCheck([x, i], oppCol)
+            this.underCheck([[x, i]], oppCol)
           )
           ||
           (
@@ -2182,8 +2139,6 @@ export default class ChessRules {
   underCheck(square_s, oppCol) {
     if (this.options["taking"] || this.options["dark"])
       return false;
-    if (!Array.isArray(square_s[0]))
-      square_s = [square_s];
     return square_s.some(sq => this.underAttack(sq, oppCol));
   }
 
@@ -2345,14 +2300,13 @@ export default class ChessRules {
   }
 
   postPlay(move) {
-    const color = this.turn;
     if (this.options["dark"])
       this.updateEnlightened();
     if (this.options["teleport"]) {
       if (
         this.subTurnTeleport == 1 &&
         move.vanish.length > move.appear.length &&
-        move.vanish[1].c == color
+        move.vanish[1].c == this.turn
       ) {
         const v = move.vanish[move.vanish.length - 1];
         this.captured = {x: v.x, y: v.y, c: v.c, p: v.p};
@@ -2362,8 +2316,12 @@ export default class ChessRules {
       this.subTurnTeleport = 1;
       this.captured = null;
     }
+    this.tryChangeTurn(move);
+  }
+
+  tryChangeTurn(move) {
     if (this.isLastMove(move)) {
-      this.turn = C.GetOppCol(color);
+      this.turn = C.GetOppCol(this.turn);
       this.movesCount++;
       this.subTurn = 1;
     }
@@ -2422,7 +2380,11 @@ export default class ChessRules {
   }
 
   // What is the score ? (Interesting if game is over)
-  getCurrentScore(move) {
+  getCurrentScore(move_s) {
+    const move = move_s[move_s.length - 1];
+    // Shortcut in case the score was computed before:
+    if (move.result)
+      return move.result;
     const color = this.turn;
     const oppCol = C.GetOppCol(color);
     const kingPos = {
@@ -2477,32 +2439,37 @@ export default class ChessRules {
   buildMoveStack(move, r) {
     this.moveStack.push(move);
     this.computeNextMove(move);
-    this.play(move);
-    const newTurn = this.turn;
-    if (this.moveStack.length == 1 && !this.hideMoves)
-      this.playVisual(move, r);
-    if (move.next) {
-      this.gameState = {
-        fen: this.getFen(),
-        board: JSON.parse(JSON.stringify(this.board)) //easier
-      };
-      this.buildMoveStack(move.next, r);
-    }
-    else {
-      if (this.moveStack.length == 1) {
-        // Usual case (one normal move)
-        this.afterPlay(this.moveStack, newTurn, {send: true, res: true});
-        this.moveStack = []
+    const then = () => {
+      const newTurn = this.turn;
+      if (this.moveStack.length == 1 && !this.hideMoves)
+        this.playVisual(move, r);
+      if (move.next) {
+        this.gameState = {
+          fen: this.getFen(),
+          board: JSON.parse(JSON.stringify(this.board)) //easier
+        };
+        this.buildMoveStack(move.next, r);
       }
       else {
-        this.afterPlay(this.moveStack, newTurn, {send: true, res: false});
-        this.re_initFromFen(this.gameState.fen, this.gameState.board);
-        this.playReceivedMove(this.moveStack.slice(1), () => {
-          this.afterPlay(this.moveStack, newTurn, {send: false, res: true});
-          this.moveStack = []
-        });
+        if (this.moveStack.length == 1) {
+          // Usual case (one normal move)
+          this.afterPlay(this.moveStack, newTurn, {send: true, res: true});
+          this.moveStack = [];
+        }
+        else {
+          this.afterPlay(this.moveStack, newTurn, {send: true, res: false});
+          this.re_initFromFen(this.gameState.fen, this.gameState.board);
+          this.playReceivedMove(this.moveStack.slice(1), () => {
+            this.afterPlay(this.moveStack, newTurn, {send: false, res: true});
+            this.moveStack = [];
+          });
+        }
       }
-    }
+    };
+    // If hiding moves, then they are revealed in play() with callback
+    this.play(move, this.hideMoves ? then : null);
+    if (!this.hideMoves)
+      then();
   }
 
   // Implemented in variants using (automatic) moveStack
@@ -2633,8 +2600,9 @@ export default class ChessRules {
 
   launchAnimation(moves, container, callback) {
     if (this.hideMoves) {
-      moves.forEach(m => this.play(m));
-      callback();
+      for (let i=0; i<moves.length; i++)
+        // If hiding moves, they are revealed into play():
+        this.play(moves[i], i == moves.length - 1 ? callback : () => {});
       return;
     }
     const r = container.querySelector(".chessboard").getBoundingClientRect();
@@ -2673,6 +2641,7 @@ export default class ChessRules {
     let container = document.getElementById(this.containerId);
     if (document.hidden) {
       document.onvisibilitychange = () => {
+        // TODO here: page reload ?! (some issues if tab changed...)
         document.onvisibilitychange = undefined;
         checkDisplayThenAnimate(700);
       };