Fanorona seems ok but not fluid.. main
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 24 Jun 2026 04:15:52 +0000 (06:15 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 24 Jun 2026 04:15:52 +0000 (06:15 +0200)
oldF.js [new file with mode: 0644]
pieces/Fanorona/black_stone.svg [new file with mode: 0644]
pieces/Fanorona/white_stone.svg [new file with mode: 0644]
variants/Fanorona/class.js
variants/Fanorona/style.css

diff --git a/oldF.js b/oldF.js
new file mode 100644 (file)
index 0000000..4e71efd
--- /dev/null
+++ b/oldF.js
@@ -0,0 +1,215 @@
+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 "*";
+  }
+
+};
diff --git a/pieces/Fanorona/black_stone.svg b/pieces/Fanorona/black_stone.svg
new file mode 100644 (file)
index 0000000..b5a8dcf
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+<defs><radialGradient id="rg" cx=".3" cy=".3" r=".8">
+<stop offset="0" stop-color="#777"/>
+<stop offset=".3" stop-color="#222"/>
+<stop offset="1" stop-color="#000"/>
+</radialGradient></defs>
+<circle cx="250" cy="250" r="160" fill="url(#rg)"/>
+</svg>
diff --git a/pieces/Fanorona/white_stone.svg b/pieces/Fanorona/white_stone.svg
new file mode 100644 (file)
index 0000000..1529a41
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+<defs><radialGradient id="rg" cx=".47" cy=".49" r=".48">
+<stop offset=".7" stop-color="#FFF"/>
+<stop offset=".9" stop-color="#DDD"/>
+<stop offset="1" stop-color="#777"/>
+</radialGradient></defs>
+<circle cx="250" cy="250" r="160" fill="url(#rg)"/>
+</svg>
index 4e71efd..4dd7be1 100644 (file)
@@ -1,5 +1,6 @@
 import ChessRules from "/js/base_rules.js";
 import AbstractOnGridRules from "/variants/_OnGrid/class.js";
+import Move from "/utils/Move.js";
 import PiPo from "/utils/PiPo.js";
 
 export default class FanoronaRules extends AbstractOnGridRules {
@@ -26,6 +27,30 @@ export default class FanoronaRules extends AbstractOnGridRules {
     };
   }
 
+  getSvgChessboard() {
+    let board = super.getSvgChessboard().slice(0, -6);
+    // Add diagonals:
+    for (const i of [0, 2, 4]) {
+      const x1 = i * 10 + 5,
+            x2 = (4+i) * 10 + 5;
+      for (const [y1, y2] of [[5, 45], [45, 5]]) {
+        board += `
+          <line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
+              stroke="grey" stroke-width="0.2"/>`;
+      }
+    }
+    for (const i of [0,4]) {
+      const y2 = i * 10 + 5;
+      for (const [x1, x2] of [[5,2*10+5], [8*10+5, 6*10+5]]) {
+        board += `
+          <line x1="${x1}" y1="25" x2="${x2}" y2="${y2}"
+              stroke="grey" stroke-width="0.2"/>`;
+      }
+    }
+    board += "</svg>";
+    return board;
+  }
+
   setOtherVariables(fen) {
     super.setOtherVariables(fen);
     // Local stack of captures during a turn (squares + directions)
@@ -68,10 +93,10 @@ export default class FanoronaRules extends AbstractOnGridRules {
   }
 
   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) => {
+      // After moving, add stones captured in "step" direction from new
+      // location [x, y] to mv.vanish (if any captured stone!)
       let [i, j] = [x + step[0], y + step[1]];
       while (
         this.onBoard(i, j) &&
@@ -103,8 +128,9 @@ export default class FanoronaRules extends AbstractOnGridRules {
       return ''; //never reached
     };
     const L = this.captures.length;
+    let c;
     if (L > 0) {
-      const c = this.captures[L-1];
+      c = this.captures[L-1];
       if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
         return (!justCapt ? [] : false);
     }
@@ -115,14 +141,14 @@ export default class FanoronaRules extends AbstractOnGridRules {
     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] }
-          })
-        );
+        let mv = new Move({
+          appear: [],
+          vanish: [],
+          start: { x: x, y: y },
+          end: { x: x - s[0], y: y - s[1] }
+        });
+        mv.noAnimate = true;
+        moves.push(mv);
         continue;
       }
       let [i, j] = [x + s[0], y + s[1]];
@@ -155,12 +181,11 @@ export default class FanoronaRules extends AbstractOnGridRules {
   }
 
   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);
+      return this.getPotentialMovesFrom(
+        [c.square.x + c.step[0], c.square.y + c.step[1]], true);
     }
     for (let i = 0; i < this.size.x; i++) {
       for (let j = 0; j < this.size.y; j++) {
@@ -177,7 +202,9 @@ export default class FanoronaRules extends AbstractOnGridRules {
   }
 
   filterValid(moves) {
-    return moves;
+    if (this.captures.length > 0 || !this.atLeastOneCapture())
+      return moves;
+    return moves.filter(m => m.vanish.length >= 2);
   }
 
   play(move) {
index 29e9c78..b96fdef 100644 (file)
@@ -3,10 +3,10 @@
 }
 
 piece.white.stone {
-  background-image: url('/pieces/Weiqi/black_stone.svg');
+  background-image: url('/pieces/Fanorona/black_stone.svg');
 }
 piece.black.stone {
-  background-image: url('/pieces/Weiqi/white_stone.svg');
+  background-image: url('/pieces/Fanorona/white_stone.svg');
 }
 
 .arrow-front {