-import { ChessRules } from "@/base_rules";
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+import { ArrayFun } from "@/utils/array";
-export class YoteRules extends ChessRules {
+export class EmergoRules extends ChessRules {
- // TODO
- //If (as white) a pile W1/B1 jumps over another pile W2/B2, it lets on the intermediate square exactly W2 men, to end as W1/(B1+B2).
- //In the first case in the video, W1=1, B1=0, W2=0, B2=1 ==> 1/1 and finally 1/2 with nothing on intermediate squares since W2 is always 0.
- //In the second case, W1=1, B1=0, W2=1, B2=1 ==> 1 man left on intermediate square, end as 1/1.
- //...I think it's that (?). Not very well explained either on Wikipedia or mindsports.nl :/
- //Found this link: http://www.iggamecenter.com/info/en/emergo.html - so it's all clear now ! I'll add the game soon.
- //Btw, I'm not a big fan of this naming "men" for pieces, but, won't contradict the author on that 
+ // Simple encoding: A to L = 1 to 12, from left to right, if white controls.
+ // Lowercase if black controls.
+ // Single piece (no prisoners): A@ to L@ (+ lowercase)
+
+ static get HasFlags() {
+ return false;
+ }
+
+ static get HasEnpassant() {
+ return false;
+ }
+
+ static get DarkBottomRight() {
+ return true;
+ }
+
+ // board element == file name:
+ static board2fen(b) {
+ return b;
+ }
+ static fen2board(f) {
+ return f;
+ }
+
+ static IsGoodPosition(position) {
+ if (position.length == 0) return false;
+ const rows = position.split("/");
+ if (rows.length != V.size.x) return false;
+ for (let row of rows) {
+ let sumElts = 0;
+ for (let i = 0; i < row.length; i++) {
+ // Add only 0.5 per symbol because 2 per piece
+ if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5;
+ else {
+ const num = parseInt(row[i], 10);
+ if (isNaN(num) || num <= 0) return false;
+ sumElts += num;
+ }
+ }
+ if (sumElts != V.size.y) return false;
+ }
+ return true;
+ }
+
+ static GetBoard(position) {
+ const rows = position.split("/");
+ let board = ArrayFun.init(V.size.x, V.size.y, "");
+ for (let i = 0; i < rows.length; i++) {
+ let j = 0;
+ for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
+ const character = rows[i][indexInRow];
+ const num = parseInt(character, 10);
+ // If num is a number, just shift j:
+ if (!isNaN(num)) j += num;
+ else
+ // Something at position i,j
+ board[i][j++] = V.fen2board(character + rows[i][++indexInRow]);
+ }
+ }
+ return board;
+ }
+
+ getPpath(b) {
+ return "Emergo/" + b;
+ }
+
+ getColor(x, y) {
+ if (x >= V.size.x) return x == V.size.x ? "w" : "b";
+ if (this.board[x][y].charCodeAt(0) < 97) return 'w';
+ return 'b';
+ }
+
+ getPiece() {
+ return V.PAWN; //unused
+ }
+
+ static IsGoodFen(fen) {
+ if (!ChessRules.IsGoodFen(fen)) return false;
+ const fenParsed = V.ParseFen(fen);
+ // 3) Check reserves
+ if (
+ !fenParsed.reserve ||
+ !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/)
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ static ParseFen(fen) {
+ const fenParts = fen.split(" ");
+ return Object.assign(
+ ChessRules.ParseFen(fen),
+ { reserve: fenParts[3] }
+ );
+ }
+
+ static get size() {
+ return { x: 9, y: 9 };
+ }
+
+ static GenRandInitFen(randomness) {
+ return "9/9/9/9/9/9/9/9/9 w 0 12,12";
+ }
+
+ getFen() {
+ return super.getFen() + " " + this.getReserveFen();
+ }
+
+ getFenForRepeat() {
+ return super.getFenForRepeat() + "_" + this.getReserveFen();
+ }
+
+ getReserveFen() {
+ return (
+ (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," +
+ (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN])
+ );
+ }
+
+ getReservePpath(index, color) {
+ return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@';
+ }
+
+ static get RESERVE_PIECES() {
+ return [V.PAWN]; //only array length matters
+ }
+
+ setOtherVariables(fen) {
+ const reserve =
+ V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10));
+ this.reserve = {
+ w: { [V.PAWN]: reserve[0] },
+ b: { [V.PAWN]: reserve[1] }
+ };
+ // Local stack of captures during a turn (squares + directions)
+ this.captures = [ [] ];
+ }
+
+ atLeastOneCaptureFrom([x, y], color) {
+ for (let s of V.steps[V.BISHOP]) {
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) != color &&
+ this.board[i + s[0]][j + s[1]] == V.EMPTY
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ atLeastOneCapture(color) {
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ if (L > 0) return this.atLeastOneCaptureFrom(captures[L-1].square, color);
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j=0; j< V.size.y; j++) {
+ if (
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == color &&
+ this.atLeastOneCaptureFrom([i, j], color)
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ maxLengthIndices(caps) {
+ let maxLength = 0;
+ let res = [];
+ for (let i = 0; i < caps.length; i++) {
+ if (caps[i].length > maxLength) {
+ res = [i];
+ maxLength = caps[i].length;
+ }
+ else if (caps[i].length == maxLength) res.push(i);
+ }
+ return res;
+ };
+
+ getLongestCapturesFrom([x, y], color, locSteps) {
+ //
+ // TODO: debug here, from
+ // 9/9/2a@1a@4/5A@3/9/3aa1A@3/9/9/8A@ w 10 8,9
+ // White to move, double capture.
+ //
+ let res = [];
+ const L = locSteps.length;
+ const lastStep = (L > 0 ? locSteps[L-1] : null);
+ for (let s of V.steps[V.BISHOP]) {
+ if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ this.board[i + s[0]][j + s[1]] == V.EMPTY &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) != color
+ ) {
+ const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
+ locSteps.push(s);
+ V.PlayOnBoard(this.board, move);
+ const sRes = this.getLongestCapturesFrom(
+ [i + s[0], j + s[1]], color, locSteps);
+ res.push({
+ step: s,
+ length: 1 + (sRes.length == 0 ? 0 : sRes[0].length)
+ });
+ locSteps.pop();
+ V.UndoOnBoard(this.board, move);
+ }
+ }
+ return this.maxLengthIndices(res).map(i => res[i]);
+ }
+
+ getAllLongestCaptures(color) {
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ if (L > 0) {
+ let locSteps = [];
+ const caps = Object.assign(
+ { square: captures[L-1].square },
+ this.getLongestCapturesFrom(captures[L-1].square, color, locSteps)
+ );
+ return this.maxLengthIndices(caps).map(i => caps[i]);
+ }
+ let caps = [];
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j=0; j < V.size.y; j++) {
+ if (
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == color
+ ) {
+ let locSteps = [];
+ let res = this.getLongestCapturesFrom([i, j], color, locSteps);
+ Array.prototype.push.apply(
+ caps,
+ res.map(r => Object.assign({ square: [i, j] }, r))
+ );
+ }
+ }
+ }
+
+console.log(caps);
+
+ return this.maxLengthIndices(caps).map(i => caps[i]);
+ }
+
+ getBasicMove([x1, y1], [x2, y2], capt) {
+ const cp1 = this.board[x1][y1];
+ if (!capt) {
+ return new Move({
+ appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ],
+ vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ]
+ });
+ }
+ // Compute resulting types based on jumped + jumping pieces
+ const cpCapt = this.board[capt[0]][capt[1]];
+ const newAtCapt = cpCapt.charCodeAt(0) - 1;
+ const newAtDest =
+ cp1[1] == '@'
+ ? (cp1.charCodeAt(0) < 97 ? 65 : 97)
+ : (cp1.charCodeAt(1) + 1);
+ const color = this.turn;
+ let mv = new Move({
+ appear: [
+ new PiPo({
+ x: x2,
+ y: y2,
+ c: cp1[0],
+ p: String.fromCharCode(newAtDest)
+ })
+ ],
+ vanish: [
+ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }),
+ new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] })
+ ]
+ });
+ if ([64, 96].includes(newAtCapt)) {
+ // Enemy units vanish from capturing square
+ if (cpCapt.charAt(1) != '@') {
+ // Out units remain:
+ mv.appear.push(
+ new PiPo({
+ x: capt[0],
+ y: capt[1],
+ c: cpCapt[0],
+ p: '@'
+ })
+ );
+ }
+ }
+ else {
+ mv.appear.push(
+ new PiPo({
+ x: capt[0],
+ y: capt[1],
+ c: String.fromCharCode(newAtCapt),
+ p: cpCapt[1]
+ })
+ );
+ }
+ return mv;
+ }
+
+ getReserveMoves(x) {
+ const color = this.turn;
+ if (!this.reserve[color] || this.atLeastOneCapture(color)) return [];
+ let moves = [];
+ const shadowPiece =
+ this.reserve[V.GetOppCol(color)] == null
+ ? this.reserve[color][V.PAWN] - 1
+ : 0;
+ const appearColor = String.fromCharCode(
+ (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece);
+ const addMove = ([i, j]) => {
+ moves.push(
+ new Move({
+ appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ],
+ vanish: [],
+ start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 }
+ })
+ );
+ };
+ const oppCol = V.GetOppCol(color);
+ const opponentCanCapture = this.atLeastOneCapture(oppCol);
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = i % 2; j < V.size.y; j += 2) {
+ if (
+ this.board[i][j] == V.EMPTY &&
+ // prevent playing on central square at move 1:
+ (this.movesCount >= 1 || i != 4 || j != 4)
+ ) {
+ if (opponentCanCapture) addMove([i, j]);
+ else {
+ let canAddMove = true;
+ for (let s of V.steps[V.BISHOP]) {
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ V.OnBoard(i - s[0], j - s[1]) &&
+ this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+ this.board[i - s[0]][j - s[1]] == V.EMPTY &&
+ this.getColor(i + s[0], j + s[1]) == oppCol
+ ) {
+ canAddMove = false;
+ break;
+ }
+ }
+ if (canAddMove) addMove([i, j]);
+ }
+ }
+ }
+ }
+ return moves;
+ }
+
+ getPotentialMovesFrom([x, y], longestCaptures) {
+ if (x >= V.size.x) {
+ if (longestCaptures.length == 0) return this.getReserveMoves(x);
+ return [];
+ }
+ const color = this.turn;
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ let moves = [];
+ if (longestCaptures.length > 0) {
+ if (
+ L > 0 &&
+ (x != captures[L-1].square[0] || y != captures[L-1].square[1])
+ ) {
+ return [];
+ }
+ longestCaptures.forEach(lc => {
+ if (lc.square[0] == x && lc.square[1] == y) {
+ const s = lc.step;
+ const [i, j] = [x + s[0], y + s[1]];
+ moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]));
+ }
+ });
+ return moves;
+ }
+ // Just search simple moves:
+ for (let s of V.steps[V.BISHOP]) {
+ const [i, j] = [x + s[0], y + s[1]];
+ if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
+ moves.push(this.getBasicMove([x, y], [i, j]));
+ }
+ return moves;
+ }
+
+ getAllValidMoves() {
+ const color = this.turn;
+ const longestCaptures = this.getAllLongestCaptures(color);
+ let potentialMoves = [];
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = 0; j < V.size.y; j++) {
+ if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+ Array.prototype.push.apply(
+ potentialMoves,
+ this.getPotentialMovesFrom([i, j], longestCaptures)
+ );
+ }
+ }
+ }
+ // Add reserve moves
+ potentialMoves = potentialMoves.concat(
+ this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1))
+ );
+ return potentialMoves;
+ }
+
+ getPossibleMovesFrom([x, y]) {
+ const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y));
+ return this.getPotentialMovesFrom([x, y], longestCaptures);
+ }
+
+ filterValid(moves) {
+ return moves;
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ play(move) {
+ const color = this.turn;
+ move.turn = color; //for undo
+ V.PlayOnBoard(this.board, move);
+ if (move.vanish.length == 2) {
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ captures.push({
+ square: [move.start.x, move.start.y],
+ 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;
+ }
+ else if (move.vanish == 0) {
+ if (--this.reserve[color][V.PAWN] == 0) this.reserve[color] = null;
+ }
+ if (!move.notTheEnd) {
+ this.turn = V.GetOppCol(color);
+ this.movesCount++;
+ this.captures.push([]);
+ }
+ }
+
+ undo(move) {
+ V.UndoOnBoard(this.board, move);
+ if (!move.notTheEnd) {
+ this.turn = move.turn;
+ this.movesCount--;
+ this.captures.pop();
+ }
+ if (move.vanish.length == 0) {
+ const color = (move.appear[0].c == 'A' ? 'w' : 'b');
+ if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 1 };
+ else this.reserve[color][V.PAWN]++;
+ }
+ else if (move.vanish.length == 2) {
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ captures.pop();
+ }
+ }
+
+ atLeastOneMove() {
+ if (this.atLeastOneCapture()) return true;
+ const color = this.turn;
+ for (let i = 0; i < V.size.x; i++) {
+ for (let j = 0; j < V.size.y; j++) {
+ if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+ const moves = this.getPotentialMovesFrom([i, j], []);
+ if (moves.length > 0) return true;
+ }
+ }
+ }
+ const reserveMoves =
+ this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
+ return (reserveMoves.length > 0);
+ }
+
+ getCurrentScore() {
+ const color = this.turn;
+ // If no pieces on board + reserve, I lose
+ if (
+ !this.reserve[color] &&
+ this.board.every(b => {
+ return b.every(cell => {
+ return (cell == "" || cell[0] != color);
+ });
+ })
+ ) {
+ return (color == 'w' ? "0-1" : "1-0");
+ }
+ if (!this.atLeastOneMove()) return "1/2";
+ return "*";
+ }
+
+ getComputerMove() {
+ // Random mover for now (TODO)
+ const color = this.turn;
+ let mvArray = [];
+ let mv = null;
+ while (this.turn == color) {
+ const moves = this.getAllValidMoves();
+ mv = moves[randInt(moves.length)];
+ mvArray.push(mv);
+ this.play(mv);
+ }
+ for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+ return (mvArray.length > 1 ? mvArray : mvArray[0]);
+ }
+
+ getNotation(move) {
+ if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end);
+ return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
+ }
};