Almost fixed Fanorona main
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 23 Jun 2026 21:10:53 +0000 (23:10 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 23 Jun 2026 21:10:53 +0000 (23:10 +0200)
js/base_rules.js
js/variants.js
pieces/Fanorona/arrow_behind.svg [new file with mode: 0644]
pieces/Fanorona/arrow_front.svg [new file with mode: 0644]
variants/Fanorona/class.js
variants/Fanorona/style.css
variants/Weiqi/class.js
variants/_OnGrid/class.js [new file with mode: 0644]

index 532932b..a6c2c59 100644 (file)
@@ -865,11 +865,15 @@ export default class ChessRules {
       y = (this.playerColor == i ? y = r.height + 5 : - 5 - rsqSize);
     }
     else {
       y = (this.playerColor == i ? y = r.height + 5 : - 5 - rsqSize);
     }
     else {
+      const delta = Math.abs(this.size.x - this.size.y);
       const sqSize = r.width / Math.max(this.size.x, this.size.y);
       const flipped = this.flippedBoard;
       const sqSize = r.width / Math.max(this.size.x, this.size.y);
       const flipped = this.flippedBoard;
-      x = (flipped ? this.size.y - 1 - j : j) * sqSize +
-          Math.abs(this.size.x - this.size.y) * sqSize / 2;
+      x = (flipped ? this.size.y - 1 - j : j) * sqSize;
+      if (this.size.x > this.size.y)
+        x += delta * sqSize / 2;
       y = (flipped ? this.size.x - 1 - i : i) * sqSize;
       y = (flipped ? this.size.x - 1 - i : i) * sqSize;
+      if (this.size.y > this.size.x)
+        y += delta * sqSize / 2;
     }
     return [r.x + x, r.y + y];
   }
     }
     return [r.x + x, r.y + y];
   }
index 7315dac..abdac56 100644 (file)
@@ -54,7 +54,7 @@ const variants = [
   {name: 'Enpassant', desc: 'Capture en passant', disp: 'En-passant'},
   {name: 'Evolution', desc: 'Faster development'},
   {name: 'Extinction', desc: 'Capture all of a kind'},
   {name: 'Enpassant', desc: 'Capture en passant', disp: 'En-passant'},
   {name: 'Evolution', desc: 'Faster development'},
   {name: 'Extinction', desc: 'Capture all of a kind'},
-//  {name: 'Fanorona', desc: 'Malagasy Draughts'},
+  {name: 'Fanorona', desc: 'Malagasy Draughts'},
 //  {name: 'Football', desc: 'Score a goal'},
 //  {name: 'Forward', desc: 'Moving forward'},
 //  {name: 'Freecapture', desc: 'Capture both colors', disp: 'Free Capture'},
 //  {name: 'Football', desc: 'Score a goal'},
 //  {name: 'Forward', desc: 'Moving forward'},
 //  {name: 'Freecapture', desc: 'Capture both colors', disp: 'Free Capture'},
diff --git a/pieces/Fanorona/arrow_behind.svg b/pieces/Fanorona/arrow_behind.svg
new file mode 100644 (file)
index 0000000..7f12c46
--- /dev/null
@@ -0,0 +1,19 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
+  <g fill="black">
+    <!-- boule -->
+    <circle cx="50" cy="82" r="14"/>
+
+    <!-- corps -->
+    <rect x="42" y="12" width="16" height="58" rx="4"/>
+
+    <!-- pointe -->
+    <path d="
+      M50 6
+      L20 36
+      A6 6 0 0 0 28 44
+      L50 22
+      L72 44
+      A6 6 0 0 0 80 36
+      Z"/>
+  </g>
+</svg>
diff --git a/pieces/Fanorona/arrow_front.svg b/pieces/Fanorona/arrow_front.svg
new file mode 100644 (file)
index 0000000..834739c
--- /dev/null
@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
+  <g fill="black">
+    <!-- boule -->
+    <circle cx="50" cy="18" r="14"/>
+
+    <!-- corps -->
+    <rect x="42" y="30" width="16" height="58" rx="4"/>
+
+    <!-- ailes -->
+    <path d="
+      M50 24
+      L20 54
+      A6 6 0 0 0 28 62
+      L50 40
+      Z"/>
+
+    <path d="
+      M50 24
+      L80 54
+      A6 6 0 0 1 72 62
+      L50 40
+      Z"/>
+  </g>
+</svg>
index d9a9cdd..4e71efd 100644 (file)
@@ -1,8 +1,8 @@
 import ChessRules from "/js/base_rules.js";
 import ChessRules from "/js/base_rules.js";
-import WeiqiRules from "/variants/Weiqi/class.js";
-// TODO: PiPo
+import AbstractOnGridRules from "/variants/_OnGrid/class.js";
+import PiPo from "/utils/PiPo.js";
 
 
-export default class FanoronaRules extends ChessRules {
+export default class FanoronaRules extends AbstractOnGridRules {
 
   static get Options() {
     return {};
 
   static get Options() {
     return {};
@@ -21,25 +21,21 @@ export default class FanoronaRules extends ChessRules {
 
   genRandInitBaseFen() {
     return {
 
   genRandInitBaseFen() {
     return {
-      fen: "sssssssss/sssssssss/sSsS1sSsS/SSSSSSSSS/SSSSSSSSS w 0",
+      fen: "sssssssss/sssssssss/sSsS1sSsS/SSSSSSSSS/SSSSSSSSS",
       o: {}
     };
   }
 
   setOtherVariables(fen) {
       o: {}
     };
   }
 
   setOtherVariables(fen) {
+    super.setOtherVariables(fen);
     // Local stack of captures during a turn (squares + directions)
     // Local stack of captures during a turn (squares + directions)
-    this.captures = [ [] ]; //TODO
+    this.captures = [];
   }
 
   get size() {
     return { x: 5, y: 9 };
   }
 
   }
 
   get size() {
     return { x: 5, y: 9 };
   }
 
-  getSvgChessboard() {
-    return WeiqiRules.SvgChessboard_share(
-      this.playerColor, this.size, this.coordsToId);
-  }
-
   getPiece() {
     return 's';
   }
   getPiece() {
     return 's';
   }
@@ -48,11 +44,9 @@ export default class FanoronaRules extends ChessRules {
     if (piece == 's') //stone
       return { "class": "stone" };
     // Arrow
     if (piece == 's') //stone
       return { "class": "stone" };
     // Arrow
-    if (piece.charCodeAt(0) >= 105) {
-      return {
-        "class": "arrow-" + (piece.charCodeAt(0) >= 105 ? "base" : "point")
-      };
-    }
+    return {
+      "class": "arrow-" + (piece.charCodeAt(0) >= 105 ? "behind" : "front")
+    };
   }
 
   // a,b,c,d,e,f,g,h : dot on point, N to NO
   }
 
   // a,b,c,d,e,f,g,h : dot on point, N to NO
@@ -68,46 +62,58 @@ export default class FanoronaRules extends ChessRules {
 
   // Draw arrow
   setPieceBackground(domPiece, piece) {
 
   // Draw arrow
   setPieceBackground(domPiece, piece) {
-    if (piece == 'p')
+    if (piece == 's')
       return;
     domPiece.style.setProperty('--rotate-by', V.ArrowToAngle(piece));
   }
 
       return;
     domPiece.style.setProperty('--rotate-by', V.ArrowToAngle(piece));
   }
 
-  // After moving, add stones captured in "step" direction from new location
-  // [x, y] to mv.vanish (if any captured stone!)
-  addCapture([x, y], step, move) {
-    let [i, j] = [x + step[0], y + step[1]];
-    const oppCol = V.GetOppCol(move.vanish[0].c);
-    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);
-  }
-
-
-  // TODO from here
-
-
-  getPotentialMovesFrom([x, y]) {
-    const L0 = this.captures.length;
-    const captures = this.captures[L0 - 1];
-    const L = captures.length;
+  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) {
     if (L > 0) {
-      var c = captures[L-1];
+      const c = this.captures[L-1];
       if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
       if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
-        return [];
+        return (!justCapt ? [] : false);
     }
     }
-    const oppCol = V.GetOppCol(this.turn);
-    let steps = V.steps[V.ROOK];
-    if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+    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) {
     let moves = [];
     for (let s of steps) {
-      if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) {
+      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({
         // Add a move to say "I'm done capturing"
         moves.push(
           new Move({
@@ -120,73 +126,48 @@ export default class FanoronaRules extends ChessRules {
         continue;
       }
       let [i, j] = [x + s[0], y + s[1]];
         continue;
       }
       let [i, j] = [x + s[0], y + s[1]];
-      if (captures.some(c => c.square.x == i && c.square.y == j)) continue;
-      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
-        // The move is potentially allowed. Might lead to 2 different captures ---------> TODO
+      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]);
         let mv = super.getBasicMove([x, y], [i, j]);
-        const capt = this.addCapture([i, j], s, mv);
+        const capt = addCapture([i, j], s, mv);
         if (capt) {
         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);
           moves.push(mv);
-          mv = super.getBasicMove([x, y], [i, j]);
         }
         }
-        const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv);
-        if (capt_bw) moves.push(mv);
         // Captures take priority (if available)
         // Captures take priority (if available)
-        if (!capt && !capt_bw && L == 0) moves.push(mv);
+        if (!justCapt && !capt && !capt_bw && L == 0)
+          moves.push(mv);
       }
     }
       }
     }
-    return moves;
+    return (!justCapt ? moves : false);
   }
 
   atLeastOneCapture() {
   }
 
   atLeastOneCapture() {
-    const color = this.turn;
-    const oppCol = V.GetOppCol(color);
-    const L0 = this.captures.length;
-    const captures = this.captures[L0 - 1];
-    const L = captures.length;
+    const oppCol = C.GetOppTurn(this.turn);
+    const L = this.captures.length;
+    // Called after at least one capture, so L > 0
     if (L > 0) {
     if (L > 0) {
-      // If some adjacent enemy stone, with free space to capture it,
-      // toward a square not already visited, through a different step
-      // from last one: then yes.
-      const c = captures[L-1];
-      const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]];
-      let steps = V.steps[V.ROOK];
-      if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
-      // TODO: half of the steps explored are redundant
-      for (let s of steps) {
-        if (s[0] == c.step[0] && s[1] == c.step[1]) continue;
-        const [i, j] = [x + s[0], y + s[1]];
-        if (
-          !V.OnBoard(i, j) ||
-          this.board[i][j] != V.EMPTY ||
-          captures.some(c => c.square.x == i && c.square.y == j)
-        ) {
-          continue;
-        }
-        if (
-          V.OnBoard(i + s[0], j + s[1]) &&
-          this.board[i + s[0]][j + s[1]] != V.EMPTY &&
-          this.getColor(i + s[0], j + s[1]) == oppCol
-        ) {
-          return true;
-        }
-        if (
-          V.OnBoard(x - s[0], y - s[1]) &&
-          this.board[x - s[0]][y - s[1]] != V.EMPTY &&
-          this.getColor(x - s[0], y - s[1]) == oppCol
-        ) {
-          return true;
-        }
-      }
-      return false;
+      const c = this.captures[L-1];
+      return this.getPotentialMovesFrom([c.square.x, c.square.y], true);
     }
     }
-    for (let i = 0; i < V.size.x; i++) {
-      for (let j = 0; j < V.size.y; j++) {
+    for (let i = 0; i < this.size.x; i++) {
+      for (let j = 0; j < this.size.y; j++) {
         if (
         if (
-          this.board[i][j] != V.EMPTY &&
-          this.getColor(i, j) == color &&
-          // TODO: this could be more efficient
-          this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2)
+          this.board[i][j] != "" &&
+          this.getColor(i, j) == this.turn &&
+          this.getPotentialMovesFrom([i, j], true)
         ) {
           return true;
         }
         ) {
           return true;
         }
@@ -195,33 +176,14 @@ export default class FanoronaRules extends ChessRules {
     return false;
   }
 
     return false;
   }
 
-  static KeepCaptures(moves) {
-    return moves.filter(m => m.vanish.length >= 2);
-  }
-
-  getPossibleMovesFrom(sq) {
-    let moves = this.getPotentialMovesFrom(sq);
-    const L0 = this.captures.length;
-    const captures = this.captures[L0 - 1];
-    if (captures.length > 0) return this.getPotentialMovesFrom(sq);
-    const captureMoves = V.KeepCaptures(moves);
-    if (captureMoves.length > 0) return captureMoves;
-    if (this.atLeastOneCapture()) return [];
-    return moves;
-  }
-
   filterValid(moves) {
     return moves;
   }
 
   play(move) {
   filterValid(moves) {
     return moves;
   }
 
   play(move) {
-    const color = this.turn;
-    move.turn = color; //for undo
-    V.PlayOnBoard(this.board, move);
+    this.playOnBoard(move);
     if (move.vanish.length >= 2) {
     if (move.vanish.length >= 2) {
-      const L0 = this.captures.length;
-      let captures = this.captures[L0 - 1];
-      captures.push({
+      this.captures.push({
         square: move.start,
         step: [move.end.x - move.start.x, move.end.y - move.start.y]
       });
         square: move.start,
         step: [move.end.x - move.start.x, move.end.y - move.start.y]
       });
@@ -230,23 +192,22 @@ export default class FanoronaRules extends ChessRules {
         move.notTheEnd = true;
     }
     if (!move.notTheEnd) {
         move.notTheEnd = true;
     }
     if (!move.notTheEnd) {
-      this.turn = V.GetOppCol(color);
+      this.turn = C.GetOppTurn(this.turn);
       this.movesCount++;
       this.movesCount++;
-      this.captures.push([]);
+      this.captures = [];
     }
   }
 
   getCurrentScore() {
     }
   }
 
   getCurrentScore() {
-    const color = this.turn;
     // If no stones on board, I lose
     if (
       this.board.every(b => {
         return b.every(cell => {
     // If no stones on board, I lose
     if (
       this.board.every(b => {
         return b.every(cell => {
-          return (cell == "" || cell[0] != color);
+          return (cell == "" || cell[0] != this.turn);
         });
       })
     ) {
         });
       })
     ) {
-      return (color == 'w' ? "0-1" : "1-0");
+      return (this.turn == 'w' ? "0-1" : "1-0");
     }
     return "*";
   }
     }
     return "*";
   }
index 287477f..29e9c78 100644 (file)
@@ -9,12 +9,12 @@ piece.black.stone {
   background-image: url('/pieces/Weiqi/white_stone.svg');
 }
 
   background-image: url('/pieces/Weiqi/white_stone.svg');
 }
 
-.arrow-point {
-  background-image: url('/pieces/Fanorona/arrow_point.svg');
+.arrow-front {
+  background-image: url('/pieces/Fanorona/arrow_front.svg');
   rotate: var(--rotate-by);
 }
 
   rotate: var(--rotate-by);
 }
 
-.arrow-base {
-  background-image: url('/pieces/Fanorona/arrow_base.svg');
+.arrow-behind {
+  background-image: url('/pieces/Fanorona/arrow_behind.svg');
   rotate: var(--rotate-by);
 }
   rotate: var(--rotate-by);
 }
index 735c0f0..2ccfd0a 100644 (file)
@@ -1,9 +1,10 @@
 import ChessRules from "/js/base_rules.js";
 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";
 import {ArrayFun} from "/utils/array.js";
 
 import Move from "/utils/Move.js";
 import PiPo from "/utils/PiPo.js";
 import {ArrayFun} from "/utils/array.js";
 
-export default class WeiqiRules extends ChessRules {
+export default class WeiqiRules extends AbstractOnGridRules {
 
   static get Options() {
     return {
 
   static get Options() {
     return {
@@ -38,48 +39,6 @@ export default class WeiqiRules extends ChessRules {
     return false;
   }
 
     return false;
   }
 
-  static SvgChessboard_share(color, bsize, coordsToId) {
-    const flipped = (color == 'b');
-    let board = `
-      <svg
-        viewBox="0 0 ${10*(bsize.y)} ${10*(bsize.x)}"
-        class="chessboard_SVG">`;
-    for (let i=0; i < bsize.x; i++) {
-      for (let j=0; j < bsize.y; j++) {
-        const ii = (flipped ? bsize.x - 1 - i : i);
-        const jj = (flipped ? bsize.y - 1 - j : j);
-        board += `
-          <rect
-            id="${coordsToId({x: ii, y: jj})}"
-            width="10"
-            height="10"
-            x="${10*j}"
-            y="${10*i}"
-            fill="transparent"
-          />`;
-      }
-    }
-    // Add lines to delimitate "squares"
-    for (let i = 0; i < bsize.x; i++) {
-      const y = i * 10 + 5, maxX = bsize.y * 10 - 5;
-      board += `
-        <line x1="5" y1="${y}" x2="${maxX}" y2="${y}"
-              stroke="black" stroke-width="0.2"/>`;
-    }
-    for (let i = 0; i < bsize.x; i++) {
-      const x = i * 10 + 5, maxY = bsize.x * 10 - 5;
-      board += `
-        <line x1="${x}" y1="5" x2="${x}" y2="${maxY}"
-              stroke="black" stroke-width="0.2"/>`;
-    }
-    board += "</svg>";
-    return board;
-  }
-
-  getSvgChessboard() {
-    return V.SvgChessboard_share(this.playerColor, this.size, this.coordsToId);
-  }
-
   get size() {
     return {
       x: this.options["bsize"],
   get size() {
     return {
       x: this.options["bsize"],
@@ -135,6 +94,10 @@ export default class WeiqiRules extends ChessRules {
     return {"class": classe_s};
   }
 
     return {"class": classe_s};
   }
 
+  canIplay(x, y) {
+    return (this.playerColor == this.turn && this.board[x][y] == "");
+  }
+
   doClick(coords) {
     const [x, y] = [coords.x, coords.y];
     if (this.board[x][y] != "" || this.turn != this.playerColor)
   doClick(coords) {
     const [x, y] = [coords.x, coords.y];
     if (this.board[x][y] != "" || this.turn != this.playerColor)
diff --git a/variants/_OnGrid/class.js b/variants/_OnGrid/class.js
new file mode 100644 (file)
index 0000000..f4f1d2d
--- /dev/null
@@ -0,0 +1,45 @@
+import ChessRules from "/js/base_rules.js";
+
+export default class AbstractOnGridRules extends ChessRules {
+
+  getSvgChessboard() {
+    const flipped = (this.playerColor == 'b');
+    let board = `
+      <svg
+        viewBox="0 0 ${10*(this.size.y)} ${10*(this.size.x)}"
+        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);
+        const jj = (flipped ? this.size.y - 1 - j : j);
+        board += `
+          <rect
+            id="${this.coordsToId({x: ii, y: jj})}"
+            width="10"
+            height="10"
+            x="${10*j}"
+            y="${10*i}"
+            fill="transparent"
+          />`;
+      }
+    }
+    // Add lines to delimitate "squares"
+    for (let i = 0; i < this.size.x; i++) {
+      const y = i * 10 + 5,
+            maxX = this.size.y * 10 - 5;
+      board += `
+        <line x1="5" y1="${y}" x2="${maxX}" y2="${y}"
+              stroke="black" stroke-width="0.2"/>`;
+    }
+    for (let i = 0; i < this.size.y; i++) {
+      const x = i * 10 + 5,
+            maxY = this.size.x * 10 - 5;
+      board += `
+        <line x1="${x}" y1="5" x2="${x}" y2="${maxY}"
+              stroke="black" stroke-width="0.2"/>`;
+    }
+    board += "</svg>";
+    return board;
+  }
+
+};