Several small improvements + integrate options + first working draft of Cwda
[vchess.git] / client / src / variants / Ball.js
index 734a921..4768975 100644 (file)
@@ -3,6 +3,22 @@ import { ArrayFun } from "@/utils/array";
 import { shuffle } from "@/utils/alea";
 
 export class BallRules extends ChessRules {
+
+  static get Lines() {
+    return [
+      // White goal:
+      [[0, 3], [0, 6]],
+      [[0, 6], [1, 6]],
+      [[1, 6], [1, 3]],
+      [[1, 3], [0, 3]],
+      // Black goal:
+      [[9, 3], [9, 6]],
+      [[9, 6], [8, 6]],
+      [[8, 6], [8, 3]],
+      [[8, 3], [9, 3]]
+    ];
+  }
+
   static get PawnSpecs() {
     return Object.assign(
       {},
@@ -24,18 +40,12 @@ export class BallRules extends ChessRules {
     return "aa";
   }
 
-  // Special code for "something to fill space" (around goals)
-  // --> If goal is outside the board (current prototype: it's inside)
-//  static get FILL() {
-//    return "ff";
-//  }
-
   static get HAS_BALL_CODE() {
     return {
       'p': 's',
       'r': 'u',
       'n': 'o',
-      'b': 'd',
+      'b': 'c',
       'q': 't',
       'k': 'l',
       'h': 'i'
@@ -47,7 +57,7 @@ export class BallRules extends ChessRules {
       's': 'p',
       'u': 'r',
       'o': 'n',
-      'd': 'b',
+      'c': 'b',
       't': 'q',
       'l': 'k',
       'i': 'h'
@@ -71,6 +81,13 @@ export class BallRules extends ChessRules {
     return ChessRules.fen2board(f);
   }
 
+  static ParseFen(fen) {
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { pmove: fen.split(" ")[4] }
+    );
+  }
+
   // Check that exactly one ball is on the board
   // + at least one piece per color.
   static IsGoodPosition(position) {
@@ -78,18 +95,19 @@ export class BallRules extends ChessRules {
     const rows = position.split("/");
     if (rows.length != V.size.x) return false;
     let pieces = { "w": 0, "b": 0 };
-    const withBall = Object.keys(V.HAS_BALL_DECODE).concat([V.BALL]);
+    const withBall = Object.keys(V.HAS_BALL_DECODE).concat(['a']);
     let ballCount = 0;
     for (let row of rows) {
       let sumElts = 0;
       for (let i = 0; i < row.length; i++) {
         const lowerRi = row[i].toLowerCase();
         if (V.PIECES.includes(lowerRi)) {
-          if (lowerRi != V.BALL) pieces[row[i] == lowerRi ? "b" : "w"]++;
+          if (lowerRi != 'a') pieces[row[i] == lowerRi ? "b" : "w"]++;
           if (withBall.includes(lowerRi)) ballCount++;
           sumElts++;
-        } else {
-          const num = parseInt(row[i]);
+        }
+        else {
+          const num = parseInt(row[i], 10);
           if (isNaN(num)) return false;
           sumElts += num;
         }
@@ -101,35 +119,80 @@ export class BallRules extends ChessRules {
     return true;
   }
 
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParts = fen.split(" ");
+    if (fenParts.length != 5) return false;
+    if (
+      fenParts[4] != "-" &&
+      !fenParts[4].match(/^([a-i][1-9]){2,2}$/)
+    ) {
+      return false;
+    }
+    return true;
+  }
+
   getPpath(b) {
     let prefix = "";
     const withPrefix =
       Object.keys(V.HAS_BALL_DECODE)
       .concat([V.PHOENIX])
-      .concat(['a']);
+      .concat(['a', 'w']); //TODO: 'w' for backward compatibility - to remove
     if (withPrefix.includes(b[1])) prefix = "Ball/";
     return prefix + b;
   }
 
+  getPPpath(m) {
+    if (
+      m.vanish.length == 2 &&
+      m.appear.length == 2 &&
+      m.appear[0].c != m.appear[1].c
+    ) {
+      // Take ball in place (from opponent)
+      return "Ball/inplace";
+    }
+    return super.getPPpath(m);
+  }
+
   canTake([x1, y1], [x2, y2]) {
-    // Capture enemy or pass ball to friendly pieces
+    if (this.getColor(x1, y1) !== this.getColor(x2, y2)) {
+      // The piece holding the ball cannot capture:
+      return (
+        !(Object.keys(V.HAS_BALL_DECODE)
+          .includes(this.board[x1][y1].charAt(1)))
+      );
+    }
+    // Pass: possible only if one of the friendly pieces has the ball
     return (
-      this.getColor(x1, y1) !== this.getColor(x2, y2) ||
-      Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1))
+      Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1)) ||
+      Object.keys(V.HAS_BALL_DECODE).includes(this.board[x2][y2].charAt(1))
     );
   }
 
-  getCheckSquares(color) {
-    return [];
+  getFen() {
+    return super.getFen() + " " + this.getPmoveFen();
   }
 
-  static GenRandInitFen(randomness) {
-    if (randomness == 0)
-      return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 -";
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getPmoveFen();
+  }
+
+  getPmoveFen() {
+    const L = this.pmoves.length;
+    if (!this.pmoves[L-1]) return "-";
+    return (
+      V.CoordsToSquare(this.pmoves[L-1].start) +
+      V.CoordsToSquare(this.pmoves[L-1].end)
+    );
+  }
+
+  static GenRandInitFen(options) {
+    if (options.randomness == 0)
+      return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 - -";
 
     let pieces = { w: new Array(9), b: new Array(9) };
     for (let c of ["w", "b"]) {
-      if (c == 'b' && randomness == 1) {
+      if (c == 'b' && options.randomness == 1) {
         pieces['b'] = pieces['w'];
         break;
       }
@@ -142,8 +205,10 @@ export class BallRules extends ChessRules {
       if (rem2 == positions[1] % 2) {
         // Fix bishops (on different colors)
         for (let i=4; i<9; i++) {
-          if (positions[i] % 2 != rem2)
+          if (positions[i] % 2 != rem2) {
             [positions[1], positions[i]] = [positions[i], positions[1]];
+            break;
+          }
         }
       }
       rem2 = positions[2] % 2;
@@ -160,13 +225,27 @@ export class BallRules extends ChessRules {
       pieces["b"].join("") +
       "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
       pieces["w"].join("").toUpperCase() +
-      // En-passant allowed, but no flags
-      " w 0 -"
+      " w 0 - -"
     );
   }
 
   scanKings() {}
 
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const pmove = V.ParseFen(fen).pmove;
+    // Local stack of "pass moves" (no need for appear & vanish)
+    this.pmoves = [
+      pmove != "-"
+        ?
+          {
+            start: V.SquareToCoords(pmove.substr(0, 2)),
+            end: V.SquareToCoords(pmove.substr(2))
+          }
+        : null
+    ];
+  }
+
   static get size() {
     return { x: 9, y: 9 };
   }
@@ -238,16 +317,31 @@ export class BallRules extends ChessRules {
       );
     }
 
-    // Post-processing: maybe the ball was taken, or a piece + ball
+    // Post-processing: maybe the ball was taken, or a piece + ball,
+    // or maybe a pass (ball <--> piece)
     if (mv.vanish.length == 2) {
       if (
         // Take the ball?
         mv.vanish[1].c == 'a' ||
-        // Capture a ball-holding piece?
+        // Capture a ball-holding piece? If friendly one, then adjust
         Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
       ) {
         mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p];
-      } else if (mv.vanish[1].c == mv.vanish[0].c) {
+        if (mv.vanish[1].c == mv.vanish[0].c) {
+          // "Capturing" self => pass
+          mv.appear[0].x = mv.start.x;
+          mv.appear[0].y = mv.start.y;
+          mv.appear.push(
+            new PiPo({
+              x: mv.end.x,
+              y: mv.end.y,
+              p: V.HAS_BALL_DECODE[mv.vanish[1].p],
+              c: mv.vanish[0].c
+            })
+          );
+        }
+      }
+      else if (mv.vanish[1].c == mv.vanish[0].c) {
         // Pass the ball: the passing unit does not disappear
         mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
         mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
@@ -259,47 +353,124 @@ export class BallRules extends ChessRules {
     return mv;
   }
 
-  // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
+  // NOTE: if a pawn captures en-passant, he doesn't hold the ball
   // So base implementation is fine.
 
   getPotentialMovesFrom([x, y]) {
-    if (this.getPiece(x, y) == V.PHOENIX)
-      return this.getPotentialPhoenixMoves([x, y]);
-    return super.getPotentialMovesFrom([x, y]);
-  }
-
-  // "Sliders": at most 2 steps
-  getSlideNJumpMoves([x, y], steps, oneStep) {
-    let moves = [];
-    outerLoop: for (let step of steps) {
-      let i = x + step[0];
-      let j = y + step[1];
-      let stepCount = 1;
-      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
-        moves.push(this.getBasicMove([x, y], [i, j]));
-        if (oneStep || stepCount == 2) continue outerLoop;
-        i += step[0];
-        j += step[1];
-        stepCount++;
+    let moves = undefined;
+    const piece = this.getPiece(x, y);
+    if (piece == V.PHOENIX)
+      moves = this.getPotentialPhoenixMoves([x, y]);
+    else moves = super.getPotentialMovesFrom([x, y]);
+    // Add "taking ball in place" move (at most one in list)
+    for (let m of moves) {
+      if (
+        m.vanish.length == 2 &&
+        m.vanish[1].p != 'a' &&
+        m.vanish[0].c != m.vanish[1].c &&
+        Object.keys(V.HAS_BALL_DECODE).includes(m.appear[0].p)
+      ) {
+        const color = this.turn;
+        const oppCol = V.GetOppCol(color);
+        moves.push(
+          new Move({
+            appear: [
+              new PiPo({
+                x: x,
+                y: y,
+                c: color,
+                p: m.appear[0].p
+              }),
+              new PiPo({
+                x: m.vanish[1].x,
+                y: m.vanish[1].y,
+                c: oppCol,
+                p: V.HAS_BALL_DECODE[m.vanish[1].p]
+              })
+            ],
+            vanish: [
+              new PiPo({
+                x: x,
+                y: y,
+                c: color,
+                p: piece
+              }),
+              new PiPo({
+                x: m.vanish[1].x,
+                y: m.vanish[1].y,
+                c: oppCol,
+                p: m.vanish[1].p
+              })
+            ],
+            end: { x: m.end.x, y: m.end.y }
+          })
+        );
+        break;
       }
-      if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
-        moves.push(this.getBasicMove([x, y], [i, j]));
     }
     return moves;
   }
 
+  getSlideNJumpMoves(sq, steps, nbSteps) {
+    // "Sliders": at most 3 steps
+    return super.getSlideNJumpMoves(sq, steps, !nbSteps ? 3 : 1);
+  }
+
   getPotentialPhoenixMoves(sq) {
-    return this.getSlideNJumpMoves(sq, V.steps[V.PHOENIX], "oneStep");
+    return super.getSlideNJumpMoves(sq, V.steps[V.PHOENIX], 1);
+  }
+
+  getPmove(move) {
+    if (
+      move.vanish.length == 2 &&
+      move.appear.length == 2 &&
+      move.appear[0].c != move.appear[1].c
+    ) {
+      // In-place pass:
+      return {
+        start: move.start,
+        end: move.end
+      };
+    }
+    return null;
+  }
+
+  oppositePasses(m1, m2) {
+    return (
+      m1.start.x == m2.end.x &&
+      m1.start.y == m2.end.y &&
+      m1.end.x == m2.start.x &&
+      m1.end.y == m2.start.y
+    );
   }
 
   filterValid(moves) {
-    return moves;
+    const L = this.pmoves.length;
+    const lp = this.pmoves[L-1];
+    if (!lp) return moves;
+    return moves.filter(m => {
+      return (
+        m.vanish.length == 1 ||
+        m.appear.length == 1 ||
+        m.appear[0].c == m.appear[1].c ||
+        !this.oppositePasses(lp, m)
+      );
+    });
   }
 
   // isAttacked: unused here (no checks)
 
-  postPlay() {}
-  postUndo() {}
+  postPlay(move) {
+    this.pmoves.push(this.getPmove(move));
+  }
+
+  postUndo() {
+    this.pmoves.pop();
+  }
+
+  getCheckSquares() {
+    return [];
+  }
 
   getCurrentScore() {
     // Turn has changed:
@@ -384,4 +555,5 @@ export class BallRules extends ChessRules {
       finalSquare
     );
   }
+
 };