update
[xogo.git] / base_rules.js
index 07084bf..30e9ee5 100644 (file)
@@ -29,16 +29,18 @@ export default class ChessRules {
           {label: "Asymmetric random", value: 2}
         ]
       }],
-      check: [
+      input: [
         {
           label: "Capture king",
-          defaut: false,
-          variable: "taking"
+          variable: "taking",
+          type: "checkbox",
+          defaut: false
         },
         {
           label: "Falling pawn",
-          defaut: false,
-          variable: "pawnfall"
+          variable: "pawnfall",
+          type: "checkbox",
+          defaut: false
         }
       ],
       // Game modifiers (using "elementary variants"). Default: false
@@ -98,7 +100,7 @@ export default class ChessRules {
       this.options["teleport"] && this.subTurnTeleport == 2 &&
       this.board[coords.x][coords.y] == ""
     ) {
-      return new Move({
+      let res = new Move({
         start: {x: this.captured.x, y: this.captured.y},
         appear: [
           new PiPo({
@@ -108,9 +110,10 @@ export default class ChessRules {
             p: this.captured.p
           })
         ],
-        vanish: [],
-        drag: {c: this.captured.c, p: this.captured.p}
+        vanish: []
       });
+      res.drag = {c: this.captured.c, p: this.captured.p};
+      return res;
     }
     return null;
   }
@@ -181,7 +184,7 @@ export default class ChessRules {
 
     else {
       // Randomize
-      let pieces = { w: new Array(8), b: new Array(8) };
+      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"]) {
@@ -289,19 +292,20 @@ export default class ChessRules {
     return fen;
   }
 
+  static FenEmptySquares(count) {
+    // if more than 9 consecutive free spaces, break the integer,
+    // otherwise FEN parsing will fail.
+    if (count <= 9)
+      return count;
+    // Most boards of size < 18:
+    if (count <= 18)
+      return "9" + (count - 9);
+    // Except Gomoku:
+    return "99" + (count - 18);
+  }
+
   // Position part of the FEN string
   getPosition() {
-    const format = (count) => {
-      // if more than 9 consecutive free spaces, break the integer,
-      // otherwise FEN parsing will fail.
-      if (count <= 9)
-        return count;
-      // Most boards of size < 18:
-      if (count <= 18)
-        return "9" + (count - 9);
-      // Except Gomoku:
-      return "99" + (count - 18);
-    };
     let position = "";
     for (let i = 0; i < this.size.y; i++) {
       let emptyCount = 0;
@@ -311,7 +315,7 @@ export default class ChessRules {
         else {
           if (emptyCount > 0) {
             // Add empty squares in-between
-            position += format(emptyCount);
+            position += C.FenEmptySquares(emptyCount);
             emptyCount = 0;
           }
           position += this.board2fen(this.board[i][j]);
@@ -319,7 +323,7 @@ export default class ChessRules {
       }
       if (emptyCount > 0)
         // "Flush remainder"
-        position += format(emptyCount);
+        position += C.FenEmptySquares(emptyCount);
       if (i < this.size.y - 1)
         position += "/"; //separate rows
     }
@@ -368,8 +372,16 @@ export default class ChessRules {
   //////////////////
   // INITIALIZATION
 
-  constructor(o) {
+  constructor(o, genFenOnly) {
     this.options = o.options;
+    // Fill missing options (always the case if random challenge)
+    (V.Options.select || []).concat(V.Options.input || []).forEach(opt => {
+      if (this.options[opt.variable] === undefined)
+        this.options[opt.variable] = opt.defaut;
+    });
+    if (genFenOnly)
+      // This object will be used only for initial FEN generation
+      return;
     this.playerColor = o.color;
     this.afterPlay = o.afterPlay; //trigger some actions after playing a move
 
@@ -455,7 +467,7 @@ export default class ChessRules {
     const steps = this.pieces(this.playerColor)["p"].attack[0].steps;
     for (let step of steps) {
       const x = this.epSquare.x - step[0],
-            y = this.computeY(this.epSquare.y - step[1]);
+            y = this.getY(this.epSquare.y - step[1]);
       if (
         this.onBoard(x, y) &&
         this.getColor(x, y) == this.playerColor &&
@@ -526,7 +538,6 @@ export default class ChessRules {
     this.initMouseEvents();
     const chessboard =
       document.getElementById(this.containerId).querySelector(".chessboard");
-    new ResizeObserver(this.rescale).observe(chessboard);
   }
 
   re_drawBoardElements() {
@@ -549,7 +560,7 @@ export default class ChessRules {
       cbHeight = Math.min(window.innerHeight, 767);
       cbWidth = cbHeight * this.size.ratio;
     }
-    if (this.reserve) {
+    if (this.hasReserve) {
       const sqSize = cbWidth / this.size.y;
       // NOTE: allocate space for reserves (up/down) even if they are empty
       // Cannot use getReserveSquareSize() here, but sqSize is an upper bound.
@@ -581,8 +592,7 @@ export default class ChessRules {
     let board = `
       <svg
         viewBox="0 0 80 80"
-        class="chessboard_SVG">
-      <g>`;
+        class="chessboard_SVG">`;
     for (let i=0; i < this.size.x; i++) {
       for (let j=0; j < this.size.y; j++) {
         const ii = (flipped ? this.size.x - 1 - i : i);
@@ -591,16 +601,18 @@ export default class ChessRules {
         if (this.enlightened && !this.enlightened[ii][jj])
           classes += " in-shadow";
         // NOTE: x / y reversed because coordinates system is reversed.
-        board += `<rect
-          class="${classes}"
-          id="${this.coordsToId({x: ii, y: jj})}"
-          width="10"
-          height="10"
-          x="${10*j}"
-          y="${10*i}" />`;
+        board += `
+          <rect
+            class="${classes}"
+            id="${this.coordsToId({x: ii, y: jj})}"
+            width="10"
+            height="10"
+            x="${10*j}"
+            y="${10*i}"
+          />`;
       }
     }
-    board += "</g></svg>";
+    board += "</svg>";
     return board;
   }
 
@@ -648,7 +660,7 @@ export default class ChessRules {
         }
       }
     }
-    if (this.reserve)
+    if (this.hasReserve)
       this.re_drawReserve(['w', 'b'], r);
   }
 
@@ -764,44 +776,48 @@ export default class ChessRules {
     }
   }
 
-  // After resize event: no need to destroy/recreate pieces
-  rescale() {
-    const container = document.getElementById(this.containerId);
-    if (!container)
-      return; //useful at initial loading
-    let chessboard = container.querySelector(".chessboard");
+  // Resize board: no need to destroy/recreate pieces
+  rescale(mode) {
+    let chessboard =
+      document.getElementById(this.containerId).querySelector(".chessboard");
     const r = chessboard.getBoundingClientRect();
-    const newRatio = r.width / r.height;
-    let newWidth = r.width,
-        newHeight = r.height;
-    if (newRatio > this.size.ratio) {
-      newWidth = r.height * this.size.ratio;
-      chessboard.style.width = newWidth + "px";
-    }
-    else if (newRatio < this.size.ratio) {
-      newHeight = r.width / this.size.ratio;
-      chessboard.style.height = newHeight + "px";
-    }
+    const multFact = (mode == "up" ? 1.05 : 0.95);
+    let [newWidth, newHeight] = [multFact * r.width, multFact * r.height];
+    // Stay in window:
+    if (newWidth > window.innerWidth) {
+      newWidth = window.innerWidth;
+      newHeight = newWidth / this.size.ratio;
+    }
+    if (newHeight > window.innerHeight) {
+      newHeight = window.innerHeight;
+      newWidth = newHeight * this.size.ratio;
+    }
+    chessboard.style.width = newWidth + "px";
+    chessboard.style.height = newHeight + "px";
     const newX = (window.innerWidth - newWidth) / 2;
     chessboard.style.left = newX + "px";
     const newY = (window.innerHeight - newHeight) / 2;
     chessboard.style.top = newY + "px";
     const newR = {x: newX, y: newY, width: newWidth, height: newHeight};
     const pieceWidth = this.getPieceWidth(newWidth);
-    for (let i=0; i < this.size.x; i++) {
-      for (let j=0; j < this.size.y; j++) {
-        if (this.g_pieces[i][j]) {
-          // NOTE: could also use CSS transform "scale"
-          this.g_pieces[i][j].style.width = pieceWidth + "px";
-          this.g_pieces[i][j].style.height = pieceWidth + "px";
-          const [ip, jp] = this.getPixelPosition(i, j, newR);
-          // Translate coordinates to use chessboard as reference:
-          this.g_pieces[i][j].style.transform =
-            `translate(${ip - newX}px,${jp - newY}px)`;
+    // NOTE: next "if" for variants which use squares filling
+    // instead of "physical", moving pieces
+    if (this.g_pieces) {
+      for (let i=0; i < this.size.x; i++) {
+        for (let j=0; j < this.size.y; j++) {
+          if (this.g_pieces[i][j]) {
+            // NOTE: could also use CSS transform "scale"
+            this.g_pieces[i][j].style.width = pieceWidth + "px";
+            this.g_pieces[i][j].style.height = pieceWidth + "px";
+            const [ip, jp] = this.getPixelPosition(i, j, newR);
+            // Translate coordinates to use chessboard as reference:
+            this.g_pieces[i][j].style.transform =
+              `translate(${ip - newX}px,${jp - newY}px)`;
+          }
         }
       }
     }
-    if (this.reserve)
+    if (this.hasReserve)
       this.rescaleReserve(newR);
   }
 
@@ -932,11 +948,6 @@ export default class ChessRules {
     };
 
     const mouseup = (e) => {
-      const newR = chessboard.getBoundingClientRect();
-      if (newR.width != r.width || newR.height != r.height) {
-        this.rescale();
-        return;
-      }
       if (!start)
         return;
       const [x, y] = [start.x, start.y];
@@ -965,6 +976,8 @@ export default class ChessRules {
       document.addEventListener("mousedown", mousedown);
       document.addEventListener("mousemove", mousemove);
       document.addEventListener("mouseup", mouseup);
+      document.addEventListener("wheel",
+        (e) => this.rescale(e.deltaY < 0 ? "up" : "down"));
     }
     if ('ontouchstart' in window) {
       // https://stackoverflow.com/a/42509310/12660887
@@ -1159,7 +1172,7 @@ export default class ChessRules {
   // MOVES GENERATION
 
   // For Cylinder: get Y coordinate
-  computeY(y) {
+  getY(y) {
     if (!this.options["cylinder"])
       return y;
     let res = y % this.size.y;
@@ -1180,13 +1193,13 @@ export default class ChessRules {
           const attacks = specs.attack || specs.moves;
           for (let a of attacks) {
             outerLoop: for (let step of a.steps) {
-              let [ii, jj] = [i + step[0], this.computeY(j + step[1])];
+              let [ii, jj] = [i + step[0], this.getY(j + step[1])];
               let stepCounter = 1;
               while (this.onBoard(ii, jj) && this.board[ii][jj] == "") {
                 if (a.range <= stepCounter++)
                   continue outerLoop;
                 ii += step[0];
-                jj = this.computeY(jj + step[1]);
+                jj = this.getY(jj + step[1]);
               }
               if (
                 this.onBoard(ii, jj) &&
@@ -1207,7 +1220,7 @@ export default class ChessRules {
 
   getDropMovesFrom([c, p]) {
     // NOTE: by design, this.reserve[c][p] >= 1 on user click
-    // (but not necessarily otherwise)
+    // (but not necessarily otherwise: atLeastOneMove() etc)
     if (this.reserve[c][p] == 0)
       return [];
     let moves = [];
@@ -1323,7 +1336,7 @@ export default class ChessRules {
         ];
         for (let step of steps) {
           let x = m.end.x + step[0];
-          let y = this.computeY(m.end.y + step[1]);
+          let y = this.getY(m.end.y + step[1]);
           if (
             this.onBoard(x, y) &&
             this.board[x][y] != "" &&
@@ -1455,7 +1468,7 @@ export default class ChessRules {
           if (a.range <= stepCounter++)
             continue outerLoop;
           i += step[0];
-          j = this.computeY(j + step[1]);
+          j = this.getY(j + step[1]);
         }
         if (
           this.onBoard(i, j) &&
@@ -1474,23 +1487,49 @@ export default class ChessRules {
     const color = this.getColor(x, y);
     const stepSpec = this.pieces(color, x, y)[piece];
     let moves = [];
-    let explored = {}; //for Cylinder mode
+    // Next 3 for Cylinder mode:
+    let explored = {};
+    let segments = [];
+    let segStart = [];
+
+    const addMove = (start, end) => {
+      let newMove = this.getBasicMove(start, end);
+      if (segments.length > 0) {
+        newMove.segments = JSON.parse(JSON.stringify(segments));
+        newMove.segments.push([[segStart[0], segStart[1]], [end[0], end[1]]]);
+      }
+      moves.push(newMove);
+    };
 
     const findAddMoves = (type, stepArray) => {
       for (let s of stepArray) {
-        // TODO: if jump in y (computeY, Cylinder), move.segments
         outerLoop: for (let step of s.steps) {
-          let [i, j] = [x + step[0], this.computeY(y + step[1])];
-          let stepCounter = 1;
-          while (this.onBoard(i, j) && this.board[i][j] == "") {
-            if (type != "attack" && !explored[i + "." + j]) {
+          segments = [];
+          segStart = [x, y];
+          let [i, j] = [x, y];
+          let stepCounter = 0;
+          while (
+            this.onBoard(i, j) &&
+            (this.board[i][j] == "" || (i == x && j == y))
+          ) {
+            if (
+              type != "attack" &&
+              !explored[i + "." + j] &&
+              (i != x || j != y)
+            ) {
               explored[i + "." + j] = true;
-              moves.push(this.getBasicMove([x, y], [i, j]));
+              addMove([x, y], [i, j]);
             }
             if (s.range <= stepCounter++)
               continue outerLoop;
+            const oldIJ = [i, j];
             i += step[0];
-            j = this.computeY(j + step[1]);
+            j = this.getY(j + step[1]);
+            if (Math.abs(j - oldIJ[1]) > 1) {
+              // Boundary between segments (cylinder mode)
+              segments.push([[segStart[0], segStart[1]], oldIJ]);
+              segStart = [i, j];
+            }
           }
           if (!this.onBoard(i, j))
             continue;
@@ -1511,7 +1550,7 @@ export default class ChessRules {
             )
           ) {
             explored[i + "." + j] = true;
-            moves.push(this.getBasicMove([x, y], [i, j]));
+            addMove([x, y], [i, j]);
           }
         }
       }
@@ -1551,7 +1590,7 @@ export default class ChessRules {
               if (!C.CompatibleStep([i, j], [x, y], s, a.range))
                 continue;
               // Finally verify that nothing stand in-between
-              let [ii, jj] = [i + s[0], this.computeY(j + s[1])];
+              let [ii, jj] = [i + s[0], this.getY(j + s[1])];
               let stepCounter = 1;
               while (
                 this.onBoard(ii, jj) &&
@@ -1559,7 +1598,7 @@ export default class ChessRules {
                 (ii != x || jj != y) //condition to attack empty squares too
               ) {
                 ii += s[0];
-                jj = this.computeY(jj + s[1]);
+                jj = this.getY(jj + s[1]);
               }
               if (ii == x && jj == y) {
                 if (args.zen)
@@ -1706,7 +1745,7 @@ export default class ChessRules {
     if (
       !!this.epSquare &&
       this.epSquare.x == x + shiftX &&
-      Math.abs(this.computeY(this.epSquare.y - y)) == 1 &&
+      Math.abs(this.getY(this.epSquare.y - y)) == 1 &&
       this.getColor(x, this.epSquare.y) == oppCol //Doublemove guard...
     ) {
       const [epx, epy] = [this.epSquare.x, this.epSquare.y];
@@ -2112,9 +2151,7 @@ export default class ChessRules {
 
   playVisual(move, r) {
     move.vanish.forEach(v => {
-      // TODO: next "if" shouldn't be required
-      if (this.g_pieces[v.x][v.y])
-        this.g_pieces[v.x][v.y].remove();
+      this.g_pieces[v.x][v.y].remove();
       this.g_pieces[v.x][v.y] = null;
     });
     let chessboard =
@@ -2161,10 +2198,6 @@ export default class ChessRules {
       return;
     }
     let initPiece = this.getDomPiece(move.start.x, move.start.y);
-    if (!initPiece) { //TODO this shouldn't be required
-      callback();
-      return;
-    }
     // NOTE: cloning generally not required, but light enough, and simpler
     let movingPiece = initPiece.cloneNode();
     initPiece.style.opacity = "0";
@@ -2204,7 +2237,7 @@ export default class ChessRules {
       // TODO: unclear why we need this new delay below:
       setTimeout(() => {
         movingPiece.style.transitionDuration = duration + "s";
-        // movingPiece is child of container: no need to adjust cordinates
+        // movingPiece is child of container: no need to adjust coordinates
         movingPiece.style.transform = `translate(${arr[0]}px, ${arr[1]}px)`;
         setTimeout(cb, duration * 1000);
       }, 50);