X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FWormhole.js;h=1f3ecd2a09423db30b2eecaaf34bec9a00a841c2;hb=e50a802531b99829c533f22ecd21e359e7e1e049;hp=964c5e409c9dbe2e0ad05d11de70fa8f94117352;hpb=c3a86f018aba40e3926e3672c7cea87acc6d1e25;p=vchess.git diff --git a/client/src/variants/Wormhole.js b/client/src/variants/Wormhole.js index 964c5e40..1f3ecd2a 100644 --- a/client/src/variants/Wormhole.js +++ b/client/src/variants/Wormhole.js @@ -1,10 +1,6 @@ -import { ChessRules, PiPo, Move } from "@/base_rules"; -import { ArrayFun } from "@/utils/array"; -import { randInt } from "@/utils/alea"; +import { ChessRules } from "@/base_rules"; -// TODO: - -export const VariantRules = class HiddenRules extends ChessRules { +export class WormholeRules extends ChessRules { static get HasFlags() { return false; } @@ -13,321 +9,322 @@ export const VariantRules = class HiddenRules extends ChessRules { return false; } - // Analyse in Hidden mode makes no sense - static get CanAnalyze() { - return false; - } - - // Moves are revealed only when game ends - static get ShowMoves() { - return "none"; - } - - static get HIDDEN_DECODE() { - return { - s: "p", - t: "q", - u: "r", - c: "b", - o: "n", - l: "k" - }; - } - static get HIDDEN_CODE() { - return { - p: "s", - q: "t", - r: "u", - b: "c", - n: "o", - k: "l" - }; + static get HOLE() { + return "xx"; } - // Turn a hidden piece or revealed piece into revealed piece: - static Decode(p) { - if (Object.keys(V.HIDDEN_DECODE).includes(p)) - return V.HIDDEN_DECODE[p]; - return p; + static board2fen(b) { + if (b[0] == 'x') return 'x'; + return ChessRules.board2fen(b); } - static get PIECES() { - return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE)); + static fen2board(f) { + if (f == 'x') return V.HOLE; + return ChessRules.fen2board(f); } - // Pieces can be hidden :) - getPiece(i, j) { - const piece = this.board[i][j].charAt(1); - if (Object.keys(V.HIDDEN_DECODE).includes(piece)) - return V.HIDDEN_DECODE[piece]; - return piece; + getPpath(b) { + if (b[0] == 'x') return "Wormhole/hole"; + return b; } - // Scan board for kings positions (no castling) - scanKingsRooks(fen) { - this.kingPos = { w: [-1, -1], b: [-1, -1] }; - const fenRows = V.ParseFen(fen).position.split("/"); - for (let i = 0; i < fenRows.length; i++) { - let k = 0; //column index on board - for (let j = 0; j < fenRows[i].length; j++) { - switch (fenRows[i].charAt(j)) { - case "k": - case "l": - this.kingPos["b"] = [i, k]; - break; - case "K": - case "L": - this.kingPos["w"] = [i, k]; - break; - default: { - const num = parseInt(fenRows[i].charAt(j)); - if (!isNaN(num)) k += num - 1; - } + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + let kings = { "k": 0, "K": 0 }; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (['K','k'].includes(row[i])) kings[row[i]]++; + if (['x'].concat(V.PIECES).includes(row[i].toLowerCase())) sumElts++; + else { + const num = parseInt(row[i], 10); + if (isNaN(num)) return false; + sumElts += num; } - k++; } + if (sumElts != V.size.y) return false; } + if (Object.values(kings).some(v => v != 1)) return false; + return true; } - getPpath(b, color, score) { - if (Object.keys(V.HIDDEN_DECODE).includes(b[1])) { - // Supposed to be hidden. - if (score == "*" && (!color || color != b[0])) - return "Hidden/" + b[0] + "p"; - // Else: condition OK to show the piece - return b[0] + V.HIDDEN_DECODE[b[1]]; + getSquareAfter(square, movement) { + let shift1, shift2; + if (Array.isArray(movement[0])) { + // A knight + shift1 = movement[0]; + shift2 = movement[1]; + } else { + shift1 = movement; + shift2 = null; } - // The piece is already not supposed to be hidden: - return b; + const tryMove = (init, shift) => { + let step = [ + shift[0] / Math.abs(shift[0]) || 0, + shift[1] / Math.abs(shift[1]) || 0, + ]; + const nbSteps = Math.max(Math.abs(shift[0]), Math.abs(shift[1])); + let stepsAchieved = 0; + let sq = [init[0] + step[0], init[1] + step[1]]; + while (V.OnBoard(sq[0],sq[1])) { + if (this.board[sq[0]][sq[1]] != V.HOLE) + stepsAchieved++; + if (stepsAchieved < nbSteps) { + sq[0] += step[0]; + sq[1] += step[1]; + } + else break; + } + if (stepsAchieved < nbSteps) + // The move is impossible + return null; + return sq; + }; + // First, apply shift1 + let dest = tryMove(square, shift1); + if (dest && shift2) + // A knight: apply second shift + dest = tryMove(dest, shift2); + return dest; } - getBasicMove([sx, sy], [ex, ey], tr) { - if ( - tr && - Object.keys(V.HIDDEN_DECODE).includes(this.board[sx][sy].charAt(1)) - ) { - // The transformed piece is a priori hidden - tr.p = V.HIDDEN_CODE[tr.p]; - } - let mv = new Move({ - appear: [ - new PiPo({ - x: ex, - y: ey, - c: tr ? tr.c : this.getColor(sx, sy), - p: tr ? tr.p : this.board[sx][sy].charAt(1) - }) + // NOTE (TODO?): some extra work done in some function because informations + // on one step should ease the computation for a step in the same direction. + static get steps() { + return { + r: [ + [-1, 0], + [1, 0], + [0, -1], + [0, 1], + [-2, 0], + [2, 0], + [0, -2], + [0, 2] + ], + // Decompose knight movements into one step orthogonal + one diagonal + n: [ + [[0, -1], [-1, -1]], + [[0, -1], [1, -1]], + [[-1, 0], [-1,-1]], + [[-1, 0], [-1, 1]], + [[0, 1], [-1, 1]], + [[0, 1], [1, 1]], + [[1, 0], [1, -1]], + [[1, 0], [1, 1]] + ], + b: [ + [-1, -1], + [-1, 1], + [1, -1], + [1, 1], + [-2, -2], + [-2, 2], + [2, -2], + [2, 2] ], - vanish: [ - new PiPo({ - x: sx, - y: sy, - c: this.getColor(sx, sy), - p: this.board[sx][sy].charAt(1) - }) + k: [ + [-1, 0], + [1, 0], + [0, -1], + [0, 1], + [-1, -1], + [-1, 1], + [1, -1], + [1, 1] ] - }); + }; + } + + getJumpMoves([x, y], steps) { + let moves = []; + for (let step of steps) { + const sq = this.getSquareAfter([x,y], step); + if (sq && + ( + this.board[sq[0]][sq[1]] == V.EMPTY || + this.canTake([x, y], sq) + ) + ) { + moves.push(this.getBasicMove([x, y], sq)); + } + } + return moves; + } - // 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) - }) - ); - // Pieces are revealed when they capture - mv.appear[0].p = V.Decode(mv.appear[0].p); + // What are the pawn moves from square x,y ? + getPotentialPawnMoves([x, y]) { + const color = this.turn; + let moves = []; + const [sizeX, sizeY] = [V.size.x, V.size.y]; + const shiftX = color == "w" ? -1 : 1; + const startRank = color == "w" ? sizeX - 2 : 1; + const lastRank = color == "w" ? 0 : sizeX - 1; + + const sq1 = this.getSquareAfter([x,y], [shiftX,0]); + if (sq1 && this.board[sq1[0]][y] == V.EMPTY) { + // One square forward (cannot be a promotion) + moves.push(this.getBasicMove([x, y], [sq1[0], y])); + if (x == startRank) { + // If two squares after is available, then move is possible + const sq2 = this.getSquareAfter([x,y], [2*shiftX,0]); + if (sq2 && this.board[sq2[0]][y] == V.EMPTY) + // Two squares jump + moves.push(this.getBasicMove([x, y], [sq2[0], y])); + } + } + // Captures + for (let shiftY of [-1, 1]) { + const sq = this.getSquareAfter([x,y], [shiftX,shiftY]); + if ( + !!sq && + this.board[sq[0]][sq[1]] != V.EMPTY && + this.canTake([x, y], [sq[0], sq[1]]) + ) { + const finalPieces = sq[0] == lastRank + ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] + : [V.PAWN]; + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [sq[0], sq[1]], { + c: color, + p: piece + }) + ); + } + } } - return mv; + return moves; } - // What are the king moves from square x,y ? - getPotentialKingMoves(sq) { - // No castling: - return this.getSlideNJumpMoves( - sq, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), - "oneStep" - ); + getPotentialRookMoves(sq) { + return this.getJumpMoves(sq, V.steps[V.ROOK]); } - filterValid(moves) { - return moves; + getPotentialKnightMoves(sq) { + return this.getJumpMoves(sq, V.steps[V.KNIGHT]); } - static GenRandInitFen() { - let pieces = { w: new Array(8), b: new Array(8) }; - // Shuffle pieces + pawns on two first ranks - for (let c of ["w", "b"]) { - let positions = ArrayFun.range(16); + getPotentialBishopMoves(sq) { + return this.getJumpMoves(sq, V.steps[V.BISHOP]); + } - // Get random squares for bishops - let randIndex = 2 * randInt(8); - const bishop1Pos = positions[randIndex]; - // The second bishop must be on a square of different color - let randIndex_tmp = 2 * randInt(8) + 1; - const bishop2Pos = positions[randIndex_tmp]; - // Remove chosen squares - positions.splice(Math.max(randIndex, randIndex_tmp), 1); - positions.splice(Math.min(randIndex, randIndex_tmp), 1); + getPotentialQueenMoves(sq) { + return this.getJumpMoves( + sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]) + ); + } - // Get random squares for knights - randIndex = randInt(14); - const knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = randInt(13); - const knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); + getPotentialKingMoves(sq) { + return this.getJumpMoves(sq, V.steps[V.KING]); + } - // Get random squares for rooks - randIndex = randInt(12); - const rook1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = randInt(11); - const rook2Pos = positions[randIndex]; - positions.splice(randIndex, 1); + isAttackedByJump([x, y], color, piece, steps) { + for (let step of steps) { + const sq = this.getSquareAfter([x,y], step); + if ( + sq && + this.getPiece(sq[0], sq[1]) == piece && + this.getColor(sq[0], sq[1]) == color + ) { + return true; + } + } + return false; + } - // Get random square for queen - randIndex = randInt(10); - const queenPos = positions[randIndex]; - positions.splice(randIndex, 1); + isAttackedByPawn([x, y], color) { + const pawnShift = (color == "w" ? 1 : -1); + for (let i of [-1, 1]) { + const sq = this.getSquareAfter([x,y], [pawnShift,i]); + if ( + sq && + this.getPiece(sq[0], sq[1]) == V.PAWN && + this.getColor(sq[0], sq[1]) == color + ) { + return true; + } + } + return false; + } - // Get random square for king - randIndex = randInt(9); - const kingPos = positions[randIndex]; - positions.splice(randIndex, 1); + isAttackedByRook(sq, color) { + return this.isAttackedByJump(sq, color, V.ROOK, V.steps[V.ROOK]); + } - // Pawns position are all remaining slots: - for (let p of positions) - pieces[c][p] = "s"; + isAttackedByKnight(sq, color) { + // NOTE: knight attack is not symmetric in this variant: + // steps order need to be reversed. + return this.isAttackedByJump( + sq, + color, + V.KNIGHT, + V.steps[V.KNIGHT].map(s => s.reverse()) + ); + } - // Finally put the shuffled pieces in the board array - pieces[c][rook1Pos] = "u"; - pieces[c][knight1Pos] = "o"; - pieces[c][bishop1Pos] = "c"; - pieces[c][queenPos] = "t"; - pieces[c][kingPos] = "l"; - pieces[c][bishop2Pos] = "c"; - pieces[c][knight2Pos] = "o"; - pieces[c][rook2Pos] = "u"; - } - let upFen = pieces["b"].join(""); - upFen = upFen.substr(0,8) + "/" + upFen.substr(8).split("").reverse().join(""); - let downFen = pieces["b"].join("").toUpperCase(); - downFen = downFen.substr(0,8) + "/" + downFen.substr(8).split("").reverse().join(""); - return upFen + "/8/8/8/8/" + downFen + " w 0"; + isAttackedByBishop(sq, color) { + return this.isAttackedByJump(sq, color, V.BISHOP, V.steps[V.BISHOP]); } - getCheckSquares() { - return []; + isAttackedByQueen(sq, color) { + return this.isAttackedByJump( + sq, + color, + V.QUEEN, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]) + ); } - updateVariables(move) { - super.updateVariables(move); - if ( - move.vanish.length >= 2 && - [V.KING,V.HIDDEN_CODE[V.KING]].includes(move.vanish[1].p) - ) { - // We took opponent king - this.kingPos[this.turn] = [-1, -1]; - } + isAttackedByKing(sq, color) { + return this.isAttackedByJump(sq, color, V.KING, V.steps[V.KING]); } - unupdateVariables(move) { - super.unupdateVariables(move); - const c = move.vanish[0].c; - const oppCol = V.GetOppCol(c); - if (this.kingPos[oppCol][0] < 0) - // Last move took opponent's king: - this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y]; + // NOTE: altering move in getBasicMove doesn't work and wouldn't be logical. + // This is a side-effect on board generated by the move. + static PlayOnBoard(board, move) { + board[move.vanish[0].x][move.vanish[0].y] = V.HOLE; + for (let psq of move.appear) board[psq.x][psq.y] = psq.c + psq.p; } getCurrentScore() { - const color = this.turn; - const kp = this.kingPos[color]; - if (kp[0] < 0) - // King disappeared - return color == "w" ? "0-1" : "1-0"; - // Assume that stalemate is impossible: - return "*"; + if (this.atLeastOneMove()) return "*"; + // No valid move: I lose + return this.turn == "w" ? "0-1" : "1-0"; } - getComputerMove() { - const color = this.turn; - let moves = this.getAllValidMoves(); - for (let move of moves) { - move.eval = 0; //a priori... + static get SEARCH_DEPTH() { + return 2; + } - // Can I take something ? If yes, do it with some probability - if (move.vanish.length == 2 && move.vanish[1].c != color) { - // OK this isn't a castling move - const myPieceVal = V.VALUES[move.appear[0].p]; - const hisPieceVal = Object.keys(V.HIDDEN_DECODE).includes(move.vanish[1].p) - ? undefined - : V.VALUES[move.vanish[1].p]; - if (!hisPieceVal) { - // Opponent's piece is unknown: do not take too much risk - move.eval = -myPieceVal + 1.5; //so that pawns always take + evalPosition() { + let evaluation = 0; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (![V.EMPTY,V.HOLE].includes(this.board[i][j])) { + const sign = this.getColor(i, j) == "w" ? 1 : -1; + evaluation += sign * V.VALUES[this.getPiece(i, j)]; } - // Favor captures - else if (myPieceVal <= hisPieceVal) - move.eval = hisPieceVal - myPieceVal + 1; - else { - // Taking a pawn with minor piece, - // or minor piece or pawn with a rook, - // or anything but a queen with a queen, - // or anything with a king. - move.eval = hisPieceVal - myPieceVal; - } - } else { - // If no capture, favor small step moves, - // but sometimes move the knight anyway - const penalty = V.Decode(move.vanish[0].p) != V.KNIGHT - ? Math.abs(move.end.x - move.start.x) + Math.abs(move.end.y - move.start.y) - : (Math.random() < 0.5 ? 3 : 1); - move.eval -= penalty / (V.size.x + V.size.y - 1); } - - // TODO: also favor movements toward the center? } - - moves.sort((a, b) => b.eval - a.eval); - let candidates = [0]; - for (let j = 1; j < moves.length && moves[j].eval == moves[0].eval; j++) - candidates.push(j); - return moves[candidates[randInt(candidates.length)]]; + return evaluation; } getNotation(move) { - // Translate final square - const finalSquare = V.CoordsToSquare(move.end); - 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 (move.appear.length > 0 && !["p","s"].includes(move.appear[0].p)) { - // Promotion - const appearPiece = V.Decode(move.appear[0].p); - notation += "=" + appearPiece.toUpperCase(); - } - return notation; - } - // Piece movement - return ( - piece.toUpperCase() + + // Indicate start square + dest square, because holes distort the board + let notation = + (piece != V.PAWN ? piece.toUpperCase() : "") + + V.CoordsToSquare(move.start) + (move.vanish.length > move.appear.length ? "x" : "") + - finalSquare - ); + V.CoordsToSquare(move.end); + if (piece == V.PAWN && move.appear[0].p != V.PAWN) + // Promotion + notation += "=" + move.appear[0].p.toUpperCase(); + return notation; } };