Add some TODOs related to faster Fanorona moves generation
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 13 Oct 2026 07:46:18 +0000 (09:46 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 13 Oct 2026 07:46:18 +0000 (09:46 +0200)
.gitignore
TODO
js/base_rules.js
oldF.js [deleted file]
variants/Fanorona/class.js

index 3189358..938d608 100644 (file)
@@ -8,3 +8,4 @@
 /.pid
 /js/parameters.js
 /dist/
+/.unversioned/
diff --git a/TODO b/TODO
index 81f9dfb..656eaf8 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,3 +1,6 @@
-https://fr.wikipedia.org/wiki/Unlur
+clarify moveStack logic around line 2600 baseRules.js
+implement in Fanorona getPotentialMove_s() function
 
+## New variants :
+https://fr.wikipedia.org/wiki/Unlur
 DuckChess + crazyduck --> move it at random ? Capturing ? Hmm.
index d82dc07..dd93b81 100644 (file)
@@ -127,6 +127,11 @@ export default class ChessRules {
     return (this.playerColor == 'b');
   }
 
+  // Some variants do not precompute moves
+  get twoSquareMove() {
+    return false;
+  }
+
   // Some variants use click infos:
   doClick(coords) {
     if (typeof coords.x != "number")
@@ -396,6 +401,7 @@ export default class ChessRules {
     this.turn = fenParsed.turn;
     this.movesCount = parseInt(fenParsed.movesCount, 10);
     this.setOtherVariables(fenParsed);
+    this.re_setGameVariables();
   }
 
   // Turn position fen into double array ["wb","wp","bk",...]
@@ -466,6 +472,13 @@ export default class ChessRules {
       this.ispawn = {};
   }
 
+  // Called before 1st move, and after receiving each opponent move.
+  re_setGameVariables() {
+    // Useful for example when captures are mandatory:
+    if (this.options["capture"])
+      this.canCapture = this.atLeastOneCapture(this.turn);
+  }
+
   ////////////////
   // VISUAL UTILS
 
@@ -908,7 +921,8 @@ export default class ChessRules {
     let start = null,
         r = null,
         startPiece, curPiece = null,
-        pieceWidth;
+        pieceWidth,
+        potentialMoves;
     const mousedown = (e) => {
       // Disable zoom on smartphones:
       if (e.touches && e.touches.length > 1)
@@ -944,6 +958,8 @@ export default class ChessRules {
             startPiece.style.opacity = "0.4";
             chessboard.style.cursor = "none";
           }
+          if (!this.twoSquareMove)
+            potentialMoves = this.getPotentialMovesFrom([x, y]);
         }
       }
     };
@@ -971,8 +987,12 @@ export default class ChessRules {
       const cd =
         (landingElt ? this.idToCoords(landingElt.id) : undefined);
       if (cd) {
-        // NOTE: clearly suboptimal, but much easier, and not a big deal.
-        const potentialMoves = this.getPotentialMove_s([x, y], [cd.x, cd.y]);
+        if (this.twoSquareMove)
+          potentialMoves = this.getPotentialMove_s([x, y], [cd.x, cd.y]);
+        else {
+          potentialMoves =
+            potentialMoves.filter(m => m.end.x == cd.x && m.end.y == cd.y);
+        }
         const moves = this.filterValid(potentialMoves);
         if (moves.length >= 2)
           this.showChoices(moves, r);
@@ -1583,12 +1603,17 @@ export default class ChessRules {
     return moves;
   }
 
+  // Redefined in variants not computing everything on mousedown
+  getPotentialMove_s([x1, y1], [x2, y2]) {
+    return [];
+  }
+
   postProcessPotentialMoves(moves) {
     if (moves.length == 0)
       return [];
     const color = this.getColor(moves[0].start.x, moves[0].start.y);
     const oppCols = this.getOppCols(color);
-    if (this.options["capture"] && this.atLeastOneCapture(color))
+    if (this.options["capture"] && this.canCapture)
       moves = this.capturePostProcess(moves, oppCols);
     if (this.options["atomic"])
       moves = this.atomicPostProcess(moves, color, oppCols);
@@ -2556,7 +2581,7 @@ export default class ChessRules {
   buildMoveStack(move, r) {
     this.moveStack.push(move);
     this.computeNextMove(move);
-    const then = () => {
+    const actions = () => {
       const newTurn = this.turn;
       if (this.moveStack.length == 1 && !this.hideMoves)
         this.playVisual(move, r);
@@ -2584,9 +2609,9 @@ export default class ChessRules {
       }
     };
     // If hiding moves, then they are revealed in play() with callback
-    this.play(move, this.hideMoves ? then : null);
+    this.play(move, this.hideMoves ? actions : null);
     if (!this.hideMoves)
-      then();
+      actions();
   }
 
   // Implemented in variants using (automatic) moveStack
diff --git a/oldF.js b/oldF.js
deleted file mode 100644 (file)
index 4e71efd..0000000
--- a/oldF.js
+++ /dev/null
@@ -1,215 +0,0 @@
-import ChessRules from "/js/base_rules.js";
-import AbstractOnGridRules from "/variants/_OnGrid/class.js";
-import PiPo from "/utils/PiPo.js";
-
-export default class FanoronaRules extends AbstractOnGridRules {
-
-  static get Options() {
-    return {};
-  }
-
-  get hasFlags() {
-    return false;
-  }
-
-  get hasEnpassant() {
-    return false;
-  }
-  static get HasKing() {
-    return false;
-  }
-
-  genRandInitBaseFen() {
-    return {
-      fen: "sssssssss/sssssssss/sSsS1sSsS/SSSSSSSSS/SSSSSSSSS",
-      o: {}
-    };
-  }
-
-  setOtherVariables(fen) {
-    super.setOtherVariables(fen);
-    // Local stack of captures during a turn (squares + directions)
-    this.captures = [];
-  }
-
-  get size() {
-    return { x: 5, y: 9 };
-  }
-
-  getPiece() {
-    return 's';
-  }
-
-  pieceDef(piece, color, x, y) {
-    if (piece == 's') //stone
-      return { "class": "stone" };
-    // Arrow
-    return {
-      "class": "arrow-" + (piece.charCodeAt(0) >= 105 ? "behind" : "front")
-    };
-  }
-
-  // a,b,c,d,e,f,g,h : dot on point, N to NO
-  // i,j,k,l,m,n,o,p : dot on base, N to NO
-  static ArrowToAngle(arrow) {
-    let ccode = arrow.charCodeAt(0);
-    if (ccode >= 105) { //i
-      ccode -= 8;
-      arrow = String.fromCharCode(ccode);
-    }
-    return (45 * (ccode - 97)) + "deg";
-  }
-
-  // Draw arrow
-  setPieceBackground(domPiece, piece) {
-    if (piece == 's')
-      return;
-    domPiece.style.setProperty('--rotate-by', V.ArrowToAngle(piece));
-  }
-
-  getPotentialMovesFrom([x, y], justCapt) {
-    // After moving, add stones captured in "step" direction from new location
-    // [x, y] to mv.vanish (if any captured stone!)
-    const oppCol = C.GetOppTurn(this.turn);
-    const addCapture = ([x, y], step, move) => {
-      let [i, j] = [x + step[0], y + step[1]];
-      while (
-        this.onBoard(i, j) &&
-        this.board[i][j] != "" &&
-        this.getColor(i, j) == oppCol
-      ) {
-        move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: 's' }));
-        [i, j] = [i + step[0], j + step[1]];
-      }
-      return (move.vanish.length >= 2);
-    };
-    const stepToArrow = (s, forward) => {
-      const baseShift = (forward ? 0 : 8),
-            colShift = (this.playerColor=='w' ? 0 : 4);
-      const doShift = (c) => {
-        return String.fromCharCode(
-          97 + (c.charCodeAt(0) - 97 + colShift) % 8 + baseShift);
-      };
-      switch (s) {
-      case "-1_0": return doShift('a');
-      case "-1_1": return doShift('b');
-      case "0_1": return doShift('c');
-      case "1_1": return doShift('d');
-      case "1_0": return doShift('e');
-      case "1_-1": return doShift('f');
-      case "0_-1": return doShift('g');
-      case "-1_-1": return doShift('h');
-      }
-      return ''; //never reached
-    };
-    const L = this.captures.length;
-    if (L > 0) {
-      const c = this.captures[L-1];
-      if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
-        return (!justCapt ? [] : false);
-    }
-    let steps = super.pieceDef('r').both[0].steps;
-    if ((x + y) % 2 == 0)
-      steps = steps.concat(super.pieceDef('b').both[0].steps);
-    let moves = [];
-    for (let s of steps) {
-      if (!justCapt && L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) {
-        // Add a move to say "I'm done capturing"
-        moves.push(
-          new Move({
-            appear: [],
-            vanish: [],
-            start: { x: x, y: y },
-            end: { x: x - s[0], y: y - s[1] }
-          })
-        );
-        continue;
-      }
-      let [i, j] = [x + s[0], y + s[1]];
-      if (this.captures.some(c => c.square.x == i && c.square.y == j))
-        continue;
-      if (this.onBoard(i, j) && this.board[i][j] == "") {
-        // The move is possible. Might lead to 2 different captures
-        let mv = super.getBasicMove([x, y], [i, j]);
-        const capt = addCapture([i, j], s, mv);
-        if (capt) {
-          if (!!justCapt)
-            return true;
-          mv.choice = stepToArrow(s[0] + "_" + s[1], true);
-          moves.push(mv);
-          mv = super.getBasicMove([x, y], [i, j]); //cheap enough
-        }
-        const capt_bw = addCapture([x, y], [-s[0], -s[1]], mv);
-        if (capt_bw) {
-          if (!!justCapt)
-            return true;
-          mv.choice = stepToArrow(s[0] + "_" + s[1], false);
-          moves.push(mv);
-        }
-        // Captures take priority (if available)
-        if (!justCapt && !capt && !capt_bw && L == 0)
-          moves.push(mv);
-      }
-    }
-    return (!justCapt ? moves : false);
-  }
-
-  atLeastOneCapture() {
-    const oppCol = C.GetOppTurn(this.turn);
-    const L = this.captures.length;
-    // Called after at least one capture, so L > 0
-    if (L > 0) {
-      const c = this.captures[L-1];
-      return this.getPotentialMovesFrom([c.square.x, c.square.y], true);
-    }
-    for (let i = 0; i < this.size.x; i++) {
-      for (let j = 0; j < this.size.y; j++) {
-        if (
-          this.board[i][j] != "" &&
-          this.getColor(i, j) == this.turn &&
-          this.getPotentialMovesFrom([i, j], true)
-        ) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  filterValid(moves) {
-    return moves;
-  }
-
-  play(move) {
-    this.playOnBoard(move);
-    if (move.vanish.length >= 2) {
-      this.captures.push({
-        square: move.start,
-        step: [move.end.x - move.start.x, move.end.y - move.start.y]
-      });
-      if (this.atLeastOneCapture())
-        // There could be other captures (optional)
-        move.notTheEnd = true;
-    }
-    if (!move.notTheEnd) {
-      this.turn = C.GetOppTurn(this.turn);
-      this.movesCount++;
-      this.captures = [];
-    }
-  }
-
-  getCurrentScore() {
-    // If no stones on board, I lose
-    if (
-      this.board.every(b => {
-        return b.every(cell => {
-          return (cell == "" || cell[0] != this.turn);
-        });
-      })
-    ) {
-      return (this.turn == 'w' ? "0-1" : "1-0");
-    }
-    return "*";
-  }
-
-};
index 63fb5b7..1079861 100644 (file)
@@ -57,6 +57,10 @@ export default class FanoronaRules extends AbstractOnGridRules {
     this.captures = [];
   }
 
+  re_setGameVariables() {
+    this.canCapture = this.atLeastOneCapture();
+  }
+
   get size() {
     return { x: 5, y: 9 };
   }
@@ -158,7 +162,7 @@ export default class FanoronaRules extends AbstractOnGridRules {
         continue;
       }
       let [i, j] = [x + s[0], y + s[1]];
-      if (this.captures.some(c => c.square.x == i && c.square.y == j))
+      if (this.captures.some(cc => cc.square.x == i && cc.square.y == j))
         continue;
       if (this.onBoard(i, j) && this.board[i][j] == "") {
         // The move is possible. Might lead to 2 different captures
@@ -208,7 +212,7 @@ export default class FanoronaRules extends AbstractOnGridRules {
   }
 
   filterValid(moves) {
-    if (this.captures.length > 0 || !this.atLeastOneCapture())
+    if (this.captures.length > 0 || !this.canCapture)
       return moves;
     return moves.filter(m => m.vanish.length >= 2);
   }