Draft Ball variant + some fixes, enhancements and code cleaning
[vchess.git] / client / src / variants / Ball.js
diff --git a/client/src/variants/Ball.js b/client/src/variants/Ball.js
new file mode 100644 (file)
index 0000000..cf50a80
--- /dev/null
@@ -0,0 +1,351 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { WildebeestRules } from "@/variants/Wildebeest";
+import { ArrayFun } from "@/utils/array";
+import { shuffle } from "@/utils/alea";
+
+export class BallRules extends ChessRules {
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { promotions: ChessRules.PawnSpecs.promotions.concat([V.WILDEBEEST]) }
+    );
+  }
+
+  static get HasFlags() {
+    return false;
+  }
+  static get HasCastle() {
+    return false;
+  }
+
+  static get WILDEBEEST() {
+    return 'w';
+  }
+
+  static get BALL() {
+    // 'b' is already taken:
+    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': 'c',
+      'q': 't',
+      'k': 'l',
+      'w': 'y'
+    };
+  }
+
+  static get HAS_BALL_DECODE() {
+    return {
+      's': 'p',
+      'u': 'r',
+      'o': 'n',
+      'c': 'b',
+      't': 'q',
+      'l': 'k',
+      'y': 'w'
+    };
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES
+      .concat([V.WILDEBEEST])
+      .concat(Object.keys(V.HAS_BALL_DECODE))
+      .concat(['a']);
+  }
+
+  static board2fen(b) {
+    if (b == V.BALL) return 'a';
+    return ChessRules.board2fen(b);
+  }
+
+  static fen2board(f) {
+    if (f == 'a') return V.BALL;
+    return ChessRules.fen2board(f);
+  }
+
+  // Check that exactly one ball is on the board
+  // + at least one piece per color.
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    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]);
+    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 (withBall.includes(lowerRi)) ballCount++;
+          sumElts++;
+        } else {
+          const num = parseInt(row[i]);
+          if (isNaN(num)) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    if (ballCount != 1 || Object.values(pieces).some(v => v == 0))
+      return false;
+    return true;
+  }
+
+  getPpath(b) {
+    let prefix = "";
+    const withPrefix =
+      Object.keys(V.HAS_BALL_DECODE)
+      .concat([V.WILDEBEEST])
+      .concat(['a']);
+    if (withPrefix.includes(b[1])) prefix = "Ball/";
+    return prefix + b;
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    // Capture enemy or pass ball to friendly pieces
+    return (
+      this.getColor(x1, y1) !== this.getColor(x2, y2) ||
+      Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1))
+    );
+  }
+
+  getCheckSquares(color) {
+    return [];
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 0)
+      return "rnbqkwbnr/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/RNBQKWBNR w 0 -";
+
+    let pieces = { w: new Array(9), b: new Array(9) };
+    for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
+      // Get random squares for every piece, totally freely
+      let positions = shuffle(ArrayFun.range(9));
+      const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q', 'w'];
+      const rem2 = positions[0] % 2;
+      if (rem2 == positions[1] % 2) {
+        // Fix bishops (on different colors)
+        for (let i=2; i<9; i++) {
+          if (positions[i] % 2 != rem2)
+            [positions[1], positions[i]] = [positions[i], positions[1]];
+        }
+      }
+      for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i];
+    }
+    return (
+      pieces["b"].join("") +
+      "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
+      pieces["w"].join("").toUpperCase() +
+      // En-passant allowed, but no flags
+      " w 0 -"
+    );
+  }
+
+  scanKings(fen) {}
+
+  static get size() {
+    return { x: 9, y: 9 };
+  }
+
+  getPiece(i, j) {
+    const p = this.board[i][j].charAt(1);
+    if (Object.keys(V.HAS_BALL_DECODE).includes(p))
+      return V.HAS_BALL_DECODE[p];
+    return p;
+  }
+
+  static get steps() {
+    return WildebeestRules.steps;
+  }
+
+  // Because of the ball, getPiece() could be wrong:
+  // use board[x][y][1] instead (always valid).
+  getBasicMove([sx, sy], [ex, ey], tr) {
+    const initColor = this.getColor(sx, sy);
+    const initPiece = this.board[sx][sy].charAt(1);
+    let mv = new Move({
+      appear: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: tr ? tr.c : initColor,
+          p: tr ? tr.p : initPiece
+        })
+      ],
+      vanish: [
+        new PiPo({
+          x: sx,
+          y: sy,
+          c: initColor,
+          p: initPiece
+        })
+      ]
+    });
+
+    // Fix "ball holding" indication in case of promotions:
+    if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece))
+      mv.appear[0].p = V.HAS_BALL_CODE[tr.p];
+
+    // The opponent piece disappears if we take it
+    if (this.board[ex][ey] != V.EMPTY) {
+      mv.vanish.push(
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: this.getColor(ex, ey),
+          p: this.board[ex][ey].charAt(1)
+        })
+      );
+    }
+
+    // Post-processing: maybe the ball was taken, or a piece + ball
+    if (mv.vanish.length == 2) {
+      if (
+        // Take the ball?
+        mv.vanish[1].c == 'a' ||
+        // Capture a ball-holding piece?
+        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) {
+        // 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];
+        mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p];
+      }
+      // Else: standard capture
+    }
+
+    return mv;
+  }
+
+  // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
+  // So base implementation is fine.
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.getPiece(x, y) == V.WILDEBEEST)
+      return this.getPotentialWildebeestMoves([x, y]);
+    return super.getPotentialMovesFrom([x, y]);
+  }
+
+  getPotentialWildebeestMoves(sq) {
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.KNIGHT].concat(V.steps[WildebeestRules.CAMEL]),
+      "oneStep"
+    );
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  // isAttacked: unused here (no checks)
+
+  postPlay() {}
+  postUndo() {}
+
+  getCurrentScore() {
+    // Turn has changed:
+    const color = V.GetOppCol(this.turn);
+    const lastRank = (color == "w" ? 0 : 8);
+    if ([3,4,5].some(
+      i => {
+        return (
+          Object.keys(V.HAS_BALL_DECODE).includes(
+            this.board[lastRank][i].charAt(1)) &&
+          this.getColor(lastRank, i) == color
+        );
+      }
+    )) {
+      // Goal scored!
+      return color == "w" ? "1-0" : "0-1";
+    }
+    if (this.atLeastOneMove()) return "*";
+    // Stalemate (quite unlikely?)
+    return "1/2";
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 5,
+      n: 3,
+      b: 3,
+      q: 9,
+      w: 7,
+      k: 5,
+      a: 0 //ball: neutral
+    };
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+  evalPosition() {
+    // Count material:
+    let evaluation = super.evalPosition();
+    if (this.board[4][4] == V.BALL)
+      // Ball not captured yet
+      return evaluation;
+    // Ponder depending on ball position
+    for (let i=0; i<9; i++) {
+      for (let j=0; j<9; j++) {
+        if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1]))
+          return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i);
+      }
+    }
+    return 0; //never reached
+  }
+
+  getNotation(move) {
+    const finalSquare = V.CoordsToSquare(move.end);
+    if (move.appear.length == 2)
+      // A pass: special notation
+      return V.CoordsToSquare(move.start) + "P" + finalSquare;
+    const piece = this.getPiece(move.start.x, move.start.y);
+    if (piece == V.PAWN) {
+      // Pawn move
+      let notation = "";
+      if (move.vanish.length > move.appear.length) {
+        // Capture
+        const startColumn = V.CoordToColumn(move.start.y);
+        notation = startColumn + "x" + finalSquare;
+      }
+      else notation = finalSquare;
+      if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
+        // Promotion
+        const promotePiece =
+          V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
+        notation += "=" + promotePiece.toUpperCase();
+      }
+      return notation;
+    }
+    // Piece movement
+    return (
+      piece.toUpperCase() +
+      (move.vanish.length > move.appear.length ? "x" : "") +
+      finalSquare
+    );
+  }
+};