X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FShogi.js;fp=client%2Fsrc%2Fvariants%2FShogi.js;h=52cfb54483061137f0cdfbb5f79e066e98160c06;hb=cd49e617866590dbc68530ad961b109cdbe1ce55;hp=0000000000000000000000000000000000000000;hpb=665eed903c4f294de82e7cb0ce4026b64fe64765;p=vchess.git diff --git a/client/src/variants/Shogi.js b/client/src/variants/Shogi.js new file mode 100644 index 00000000..52cfb544 --- /dev/null +++ b/client/src/variants/Shogi.js @@ -0,0 +1,596 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; + +export class ShogiRules extends ChessRules { + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + 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]{14,14}$/)) + return false; + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { reserve: fenParts[3] } + ); + } + + // pawns, rooks, knights, bishops and king kept from ChessRules + static get GOLD_G() { + return "g"; + } + static get SILVER_G() { + return "s"; + } + static get LANCER() { + return "l"; + } + + // Promoted pieces: + static get P_PAWN() { + return 'q'; + } + static get P_KNIGHT() { + return 'o'; + } + static get P_SILVER() { + return 't'; + } + static get P_LANCER() { + return 'm'; + } + static get P_ROOK() { + return 'd'; + } + static get P_BISHOP() { + return 'h'; + } + + static get PIECES() { + return [ + ChessRules.PAWN, + ChessRules.ROOK, + ChessRules.KNIGHT, + ChessRules.BISHOP, + ChessRules.KING, + V.GOLD_G, + V.SILVER_G, + V.LANCER, + V.P_PAWN, + V.P_KNIGHT, + V.P_SILVER, + V.P_LANCER, + V.P_ROOK, + V.P_BISHOP + ]; + } + + getPpath(b, color, score, orientation) { + // 'i' for "inversed": + const suffix = (b[0] == orientation ? "" : "i"); + return "Shogi/" + b + suffix; + } + + getPPpath(m, orientation) { + return ( + this.getPpath( + m.appear[0].c + m.appear[0].p, + null, + null, + orientation + ) + ); + } + + static GenRandInitFen() { + // No randomization for now: + return ( + "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL " + + "w 0 00000000000000" + ); + } + + getFen() { + return super.getFen() + " " + this.getReserveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getReserveFen(); + } + + getReserveFen() { + let counts = new Array(14); + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + counts[i] = this.reserve["w"][V.RESERVE_PIECES[i]]; + counts[6 + i] = this.reserve["b"][V.RESERVE_PIECES[i]]; + } + return counts.join(""); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + const fenParsed = V.ParseFen(fen); + // Also init reserves (used by the interface to show landable pieces) + this.reserve = { + w: { + [V.PAWN]: parseInt(fenParsed.reserve[0]), + [V.ROOK]: parseInt(fenParsed.reserve[1]), + [V.BISHOP]: parseInt(fenParsed.reserve[2]), + [V.GOLD_G]: parseInt(fenParsed.reserve[3]), + [V.SILVER_G]: parseInt(fenParsed.reserve[4]), + [V.KNIGHT]: parseInt(fenParsed.reserve[5]), + [V.LANCER]: parseInt(fenParsed.reserve[6]) + }, + b: { + [V.PAWN]: parseInt(fenParsed.reserve[7]), + [V.ROOK]: parseInt(fenParsed.reserve[8]), + [V.BISHOP]: parseInt(fenParsed.reserve[9]), + [V.GOLD_G]: parseInt(fenParsed.reserve[10]), + [V.SILVER_G]: parseInt(fenParsed.reserve[11]), + [V.KNIGHT]: parseInt(fenParsed.reserve[12]), + [V.LANCER]: parseInt(fenParsed.reserve[13]) + } + }; + } + + getColor(i, j) { + if (i >= V.size.x) return i == V.size.x ? "w" : "b"; + return this.board[i][j].charAt(0); + } + + getPiece(i, j) { + if (i >= V.size.x) return V.RESERVE_PIECES[j]; + return this.board[i][j].charAt(1); + } + + static get size() { + return { x: 9, y: 9}; + } + + getReservePpath(index, color, orientation) { + return ( + "Shogi/" + color + V.RESERVE_PIECES[index] + + (color != orientation ? 'i' : '') + ); + } + + // Ordering on reserve pieces + static get RESERVE_PIECES() { + return ( + [V.PAWN, V.ROOK, V.BISHOP, V.GOLD_G, V.SILVER_G, V.KNIGHT, V.LANCER] + ); + } + + getReserveMoves([x, y]) { + const color = this.turn; + const p = V.RESERVE_PIECES[y]; + if (p == V.PAWN) { + var oppCol = V.GetOppCol(color); + var allowedFiles = + [...Array(9).keys()].filter(j => + [...Array(9).keys()].every(i => { + return ( + this.board[i][j] == V.EMPTY || + this.getColor(i, j) != color || + this.getPiece(i, j) != V.PAWN + ); + }) + ) + } + if (this.reserve[color][p] == 0) return []; + let moves = []; + const forward = color == 'w' ? -1 : 1; + const lastRanks = color == 'w' ? [0, 1] : [8, 7]; + for (let i = 0; i < V.size.x; i++) { + if ( + (i == lastRanks[0] && [V.PAWN, V.KNIGHT, V.LANCER].includes(p)) || + (i == lastRanks[1] && p == V.KNIGHT) + ) { + continue; + } + for (let j = 0; j < V.size.y; j++) { + if ( + this.board[i][j] == V.EMPTY && + (p != V.PAWN || allowedFiles.includes(j)) + ) { + let mv = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: color, + p: p + }) + ], + vanish: [], + start: { x: x, y: y }, //a bit artificial... + end: { x: i, y: j } + }); + if (p == V.PAWN) { + // Do not drop on checkmate: + this.play(mv); + const res = (this.underCheck(oppCol) && !this.atLeastOneMove()); + this.undo(mv); + if (res) continue; + } + moves.push(mv); + } + } + } + return moves; + } + + getPotentialMovesFrom([x, y]) { + if (x >= V.size.x) { + // Reserves, outside of board: x == sizeX(+1) + return this.getReserveMoves([x, y]); + } + switch (this.getPiece(x, y)) { + case V.PAWN: + return this.getPotentialPawnMoves([x, y]); + case V.ROOK: + return this.getPotentialRookMoves([x, y]); + case V.KNIGHT: + return this.getPotentialKnightMoves([x, y]); + case V.BISHOP: + return this.getPotentialBishopMoves([x, y]); + case V.SILVER_G: + return this.getPotentialSilverMoves([x, y]); + case V.LANCER: + return this.getPotentialLancerMoves([x, y]); + case V.KING: + return this.getPotentialKingMoves([x, y]); + case V.P_ROOK: + return this.getPotentialDragonMoves([x, y]); + case V.P_BISHOP: + return this.getPotentialHorseMoves([x, y]); + case V.GOLD_G: + case V.P_PAWN: + case V.P_SILVER: + case V.P_KNIGHT: + case V.P_LANCER: + return this.getPotentialGoldMoves([x, y]); + } + return []; //never reached + } + + // Modified to take promotions into account + getSlideNJumpMoves([x, y], steps, options) { + const color = this.turn; + const oneStep = options.oneStep; + const forcePromoteOnLastRank = options.force; + const promoteInto = options.promote; + const lastRanks = (color == 'w' ? [0, 1, 2] : [9, 8, 7]); + let moves = []; + outerLoop: for (let step of steps) { + let i = x + step[0]; + let j = y + step[1]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + if (i != lastRanks[0] || !forcePromoteOnLastRank) + moves.push(this.getBasicMove([x, y], [i, j])); + if (!!promoteInto && lastRanks.includes(i)) { + moves.push( + this.getBasicMove( + [x, y], [i, j], { c: color, p: promoteInto }) + ); + } + if (oneStep) continue outerLoop; + i += step[0]; + j += step[1]; + } + if (V.OnBoard(i, j) && this.canTake([x, y], [i, j])) { + if (i != lastRanks[0] || !forcePromoteOnLastRank) + moves.push(this.getBasicMove([x, y], [i, j])); + if (!!promoteInto && lastRanks.includes(i)) { + moves.push( + this.getBasicMove( + [x, y], [i, j], { c: color, p: promoteInto }) + ); + } + } + } + return moves; + } + + getPotentialGoldMoves(sq) { + const forward = (this.turn == 'w' ? -1 : 1); + return this.getSlideNJumpMoves( + sq, + V.steps[V.ROOK].concat([ [forward, 1], [forward, -1] ]), + { oneStep: true } + ); + } + + getPotentialPawnMoves(sq) { + const forward = (this.turn == 'w' ? -1 : 1); + return ( + this.getSlideNJumpMoves( + sq, + [[forward, 0]], + { + oneStep: true, + promote: V.P_PAWN, + force: true + } + ) + ); + } + + getPotentialSilverMoves(sq) { + const forward = (this.turn == 'w' ? -1 : 1); + return this.getSlideNJumpMoves( + sq, + V.steps[V.BISHOP].concat([ [forward, 0] ]), + { + oneStep: true, + promote: V.P_SILVER + } + ); + } + + getPotentialKnightMoves(sq) { + const forward = (this.turn == 'w' ? -2 : 2); + return this.getSlideNJumpMoves( + sq, + [ [forward, 1], [forward, -1] ], + { + oneStep: true, + promote: V.P_KNIGHT, + force: true + } + ); + } + + getPotentialRookMoves(sq) { + return this.getSlideNJumpMoves( + sq, V.steps[V.ROOK], { promote: V.P_ROOK }); + } + + getPotentialBishopMoves(sq) { + return this.getSlideNJumpMoves( + sq, V.steps[V.BISHOP], { promote: V.P_BISHOP }); + } + + getPotentialLancerMoves(sq) { + const forward = (this.turn == 'w' ? -1 : 1); + return this.getSlideNJumpMoves( + sq, [[forward, 0]], { promote: V.P_LANCER }); + } + + getPotentialDragonMoves(sq) { + return ( + this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], { oneStep: true })) + ); + } + + getPotentialHorseMoves(sq) { + return ( + this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.ROOK], { oneStep: true })) + ); + } + + getPotentialKingMoves(sq) { + return this.getSlideNJumpMoves( + sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + { oneStep: true } + ); + } + + isAttacked(sq, color) { + return ( + this.isAttackedByPawn(sq, color) || + this.isAttackedByRook(sq, color) || + this.isAttackedByDragon(sq, color) || + this.isAttackedByKnight(sq, color) || + this.isAttackedByBishop(sq, color) || + this.isAttackedByHorse(sq, color) || + this.isAttackedByLancer(sq, color) || + this.isAttackedBySilver(sq, color) || + this.isAttackedByGold(sq, color) || + this.isAttackedByKing(sq, color) + ); + } + + isAttackedByGold([x, y], color) { + const shift = (color == 'w' ? 1 : -1); + for (let step of V.steps[V.ROOK].concat([[shift, 1], [shift, -1]])) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + [V.GOLD_G, V.P_PAWN, V.P_SILVER, V.P_KNIGHT, V.P_LANCER] + .includes(this.getPiece(i, j)) + ) { + return true; + } + } + return false; + } + + isAttackedBySilver([x, y], color) { + const shift = (color == 'w' ? 1 : -1); + for (let step of V.steps[V.BISHOP].concat([[shift, 0]])) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.getPiece(i, j) == V.SILVER_G + ) { + return true; + } + } + return false; + } + + isAttackedByPawn([x, y], color) { + const shift = (color == 'w' ? 1 : -1); + const [i, j] = [x + shift, y]; + return ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.getPiece(i, j) == V.PAWN + ); + } + + isAttackedByKnight(sq, color) { + const forward = (color == 'w' ? 2 : -2); + return this.isAttackedBySlideNJump( + sq, color, V.KNIGHT, [[forward, 1], [forward, -1]], "oneStep"); + } + + isAttackedByLancer(sq, color) { + const forward = (color == 'w' ? 1 : -1); + return this.isAttackedBySlideNJump(sq, color, V.LANCER, [[forward, 0]]); + } + + isAttackedByDragon(sq, color) { + return ( + this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.ROOK]) || + this.isAttackedBySlideNJump( + sq, color, V.DRAGON, V.steps[V.BISHOP], "oneStep") + ); + } + + isAttackedByHorse(sq, color) { + return ( + this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.BISHOP]) || + this.isAttackedBySlideNJump( + sq, color, V.DRAGON, V.steps[V.ROOK], "oneStep") + ); + } + + getAllValidMoves() { + let moves = super.getAllPotentialMoves(); + const color = this.turn; + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + moves = moves.concat( + this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i]) + ); + } + return this.filterValid(moves); + } + + atLeastOneMove() { + if (!super.atLeastOneMove()) { + // Search one reserve move + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + let moves = this.filterValid( + this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i]) + ); + if (moves.length > 0) return true; + } + return false; + } + return true; + } + + static get P_CORRESPONDANCES() { + return { + q: 'p', + o: 'n', + t: 's', + m: 'l', + d: 'r', + h: 'b' + }; + } + + static MayDecode(piece) { + if (Object.keys(V.P_CORRESPONDANCES).includes(piece)) + return V.P_CORRESPONDANCES[piece]; + return piece; + } + + postPlay(move) { + super.postPlay(move); + const color = move.appear[0].c; + if (move.vanish.length == 0) + // Drop unpromoted piece: + this.reserve[color][move.appear[0].p]--; + else if (move.vanish.length == 2) + // May capture a promoted piece: + this.reserve[color][V.MayDecode(move.vanish[1].p)]++; + } + + postUndo(move) { + super.postUndo(move); + const color = this.turn; + if (move.vanish.length == 0) + this.reserve[color][move.appear[0].p]++; + else if (move.vanish.length == 2) + this.reserve[color][V.MayDecode(move.vanish[1].p)]--; + } + + static get SEARCH_DEPTH() { + return 2; + } + + static get VALUES() { + // TODO: very arbitrary and wrong + return { + p: 1, + q: 3, + r: 5, + d: 6, + n: 2, + o: 3, + b: 3, + h: 4, + s: 3, + t: 3, + l: 2, + m: 3, + g: 3, + k: 1000, + } + } + + evalPosition() { + let evaluation = super.evalPosition(); + // Add reserves: + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + const p = V.RESERVE_PIECES[i]; + evaluation += this.reserve["w"][p] * V.VALUES[p]; + evaluation -= this.reserve["b"][p] * V.VALUES[p]; + } + return evaluation; + } + + getNotation(move) { + const finalSquare = V.CoordsToSquare(move.end); + if (move.vanish.length == 0) { + // Rebirth: + const piece = move.appear[0].p.toUpperCase(); + return (piece != 'P' ? piece : "") + "@" + finalSquare; + } + const piece = move.vanish[0].p.toUpperCase(); + return ( + (piece != 'P' || move.vanish.length == 2 ? piece : "") + + (move.vanish.length == 2 ? "x" : "") + + finalSquare + + ( + move.appear[0].p != move.vanish[0].p + ? "=" + move.appear[0].p.toUpperCase() + : "" + ) + ); + } +};