X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FSynchrone.js;h=ad14e0517073917ea17697241eb221c3c6aba952;hb=2c5d7b20742b802d9c47916915c1114bcfc9a9c3;hp=e47941bf2d9cdf31f38ac28b4d42dba02512b238;hpb=7ebc0408a76b4a966273190a2ade49e0f97099be;p=vchess.git diff --git a/client/src/variants/Synchrone.js b/client/src/variants/Synchrone.js index e47941bf..ad14e051 100644 --- a/client/src/variants/Synchrone.js +++ b/client/src/variants/Synchrone.js @@ -1,11 +1,505 @@ import { ChessRules } from "@/base_rules"; +import { randInt } from "@/utils/alea"; export class SynchroneRules extends ChessRules { - // TODO: getNotation retourne "?" si turn == "w" - // ==> byrows disparait, juste "showAll" et "None". - // - // play: si turn == "w", enregistrer le coup (whiteMove), - // mais ne rien faire ==> résolution après le coup noir. - // - // ==> un coup sur deux (coups blancs) est "vide" du point de vue de l'exécution. + static get CanAnalyze() { + return false; + } + + static get ShowMoves() { + return "byrow"; + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 5) Check whiteMove + if ( + ( + fenParsed.turn == "b" && + // NOTE: do not check really JSON stringified move... + (!fenParsed.whiteMove || fenParsed.whiteMove == "-") + ) + || + (fenParsed.turn == "w" && fenParsed.whiteMove != "-") + ) { + return false; + } + return true; + } + + static IsGoodEnpassant(enpassant) { + const epArray = enpassant.split(","); + if (![2, 3].includes(epArray.length)) return false; + epArray.forEach(epsq => { + if (epsq != "-") { + const ep = V.SquareToCoords(epsq); + if (isNaN(ep.x) || !V.OnBoard(ep)) return false; + } + }); + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { whiteMove: fenParts[5] } + ); + } + + static GenRandInitFen(randomness) { + return ChessRules.GenRandInitFen(randomness).slice(0, -1) + "-,- -"; + } + + getFen() { + return super.getFen() + " " + this.getWhitemoveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getWhitemoveFen(); + } + + setOtherVariables(fen) { + const parsedFen = V.ParseFen(fen); + this.setFlags(parsedFen.flags); + const epArray = parsedFen.enpassant.split(","); + this.epSquares = []; + epArray.forEach(epsq => this.epSquares.push(this.getEpSquare(epsq))); + super.scanKings(fen); + // Also init whiteMove + this.whiteMove = + parsedFen.whiteMove != "-" + ? JSON.parse(parsedFen.whiteMove) + : null; + } + + // After undo(): no need to re-set INIT_COL_KING + scanKings() { + this.kingPos = { w: [-1, -1], b: [-1, -1] }; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.getPiece(i, j) == V.KING) + this.kingPos[this.getColor(i, j)] = [i, j]; + } + } + } + + getEnpassantFen() { + const L = this.epSquares.length; + let res = ""; + const start = L - 2 - (this.turn == 'b' ? 1 : 0); + for (let i=start; i < L; i++) { + if (!this.epSquares[i]) res += "-,"; + else res += V.CoordsToSquare(this.epSquares[i]) + ","; + } + return res.slice(0, -1); + } + + getWhitemoveFen() { + if (!this.whiteMove) return "-"; + return JSON.stringify({ + start: this.whiteMove.start, + end: this.whiteMove.end, + appear: this.whiteMove.appear, + vanish: this.whiteMove.vanish + }); + } + + getPossibleMovesFrom([x, y]) { + return ( + this.filterValid(super.getPotentialMovesFrom([x, y])) + // Augment with potential recaptures: + .concat(this.getRecaptures()) + ); + } + + // Aux function used to find opponent and self captures + getCaptures(x, y, color) { + const sliderAttack = (xx, yy, allowedSteps) => { + const deltaX = xx - x, + absDeltaX = Math.abs(deltaX); + const deltaY = yy - y, + absDeltaY = Math.abs(deltaY); + const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ]; + if ( + // Check that the step is a priori valid: + (absDeltaX != absDeltaY && deltaX != 0 && deltaY != 0) || + allowedSteps.every(st => st[0] != step[0] || st[1] != step[1]) + ) { + return null; + } + let sq = [ x + step[0], y + step[1] ]; + while (sq[0] != xx || sq[1] != yy) { + // NOTE: no need to check OnBoard in this special case + if (this.board[sq[0]][sq[1]] != V.EMPTY) return null; + sq[0] += step[0]; + sq[1] += step[1]; + } + return this.getBasicMove([xx, yy], [x, y]); + }; + // Can I take on the square [x, y] ? + // If yes, return the (list of) capturing move(s) + let moves = []; + for (let i=0; i<8; i++) { + for (let j=0; j<8; j++) { + if (this.getColor(i, j) == color) { + switch (this.getPiece(i, j)) { + case V.PAWN: { + // Pushed pawns move as enemy pawns + const shift = (color == 'w' ? 1 : -1); + if (x + shift == i && Math.abs(y - j) == 1) + moves.push(this.getBasicMove([i, j], [x, y])); + break; + } + case V.KNIGHT: { + const deltaX = Math.abs(i - x); + const deltaY = Math.abs(j - y); + if ( + deltaX + deltaY == 3 && + [1, 2].includes(deltaX) && + [1, 2].includes(deltaY) + ) { + moves.push(this.getBasicMove([i, j], [x, y])); + } + break; + } + case V.KING: + if (Math.abs(i - x) <= 1 && Math.abs(j - y) <= 1) + moves.push(this.getBasicMove([i, j], [x, y])); + break; + case V.ROOK: { + const mv = sliderAttack(i, j, V.steps[V.ROOK]); + if (!!mv) moves.push(mv); + break; + } + case V.BISHOP: { + const mv = sliderAttack(i, j, V.steps[V.BISHOP]); + if (!!mv) moves.push(mv); + break; + } + case V.QUEEN: { + const mv = sliderAttack( + i, j, V.steps[V.ROOK].concat(V.steps[V.BISHOP])); + if (!!mv) moves.push(mv); + break; + } + } + } + } + } + return this.filterValid(moves); + } + + getRecaptures() { + // 1) Generate all opponent's capturing moves + let oppCaptureMoves = []; + const color = this.turn; + const oppCol = V.GetOppCol(color); + for (let i=0; i<8; i++) { + for (let j=0; j<8; j++) { + if ( + this.getColor(i, j) == color && + // Do not consider king captures: self-captures of king are forbidden + this.getPiece(i, j) != V.KING + ) { + Array.prototype.push.apply( + oppCaptureMoves, + this.getCaptures(i, j, oppCol) + ); + } + } + } + // 2) Play each opponent's capture, and see if back-captures are possible: + // Lookup table to quickly decide if a move is already in list: + let moveSet = {}; + let moves = []; + oppCaptureMoves.forEach(m => { + // If another opponent capture with same endpoint already processed, skip + const mHash = "m" + m.end.x + m.end.y; + if (!moveSet[mHash]) { + moveSet[mHash] = true; + // Just make enemy piece disappear, to clear potential path: + const justDisappear = { + appear: [], + vanish: [m.vanish[0]] + }; + V.PlayOnBoard(this.board, justDisappear); + // Can I take on [m.end.x, m.end.y] ? If yes, add to list: + this.getCaptures(m.end.x, m.end.y, color) + .forEach(cm => moves.push(cm)); + V.UndoOnBoard(this.board, justDisappear); + } + }); + return moves; + } + + getAllValidMoves() { + // Return possible moves + potential recaptures + return super.getAllValidMoves().concat(this.getRecaptures()); + } + + filterValid(moves) { + if (moves.length == 0) return []; + // filterValid can be called when it's "not our turn": + const color = moves[0].vanish[0].c; + return moves.filter(m => { + const piece = m.vanish[0].p; + if (piece == V.KING) { + this.kingPos[color][0] = m.appear[0].x; + this.kingPos[color][1] = m.appear[0].y; + } + V.PlayOnBoard(this.board, m); + let res = !this.underCheck(color); + V.UndoOnBoard(this.board, m); + if (piece == V.KING) this.kingPos[color] = [m.start.x, m.start.y]; + return res; + }); + } + + atLeastOneMove(color) { + const curTurn = this.turn; + this.turn = color; + const res = super.atLeastOneMove(); + this.turn = curTurn; + return res; + } + + // White and black (partial) moves were played: merge + resolveSynchroneMove(move) { + const m1 = this.whiteMove; + const m2 = move; + // For PlayOnBoard (no need for start / end, irrelevant) + let smove = { + appear: [], + vanish: [ + m1.vanish[0], + m2.vanish[0] + ] + }; + if ((m1.end.x != m2.end.x) || (m1.end.y != m2.end.y)) { + // Easy case: two independant moves (which may (self-)capture) + smove.appear.push(m1.appear[0]); + smove.appear.push(m2.appear[0]); + // "Captured" pieces may have moved: + if (m1.appear.length == 2) { + // Castle + smove.appear.push(m1.appear[1]); + smove.vanish.push(m1.vanish[1]); + } else if ( + m1.vanish.length == 2 && + ( + m1.vanish[1].x != m2.start.x || + m1.vanish[1].y != m2.start.y + ) + ) { + smove.vanish.push(m1.vanish[1]); + } + if (m2.appear.length == 2) { + // Castle + smove.appear.push(m2.appear[1]); + smove.vanish.push(m2.vanish[1]); + } else if ( + m2.vanish.length == 2 && + ( + m2.vanish[1].x != m1.start.x || + m2.vanish[1].y != m1.start.y + ) + ) { + smove.vanish.push(m2.vanish[1]); + } + } else { + // Collision: + if (m1.vanish.length == 1 && m2.vanish.length == 1) { + // Easy case: both disappear except if one is a king + const p1 = m1.vanish[0].p; + const p2 = m2.vanish[0].p; + if ([p1, p2].includes(V.KING)) { + smove.appear.push({ + x: m1.end.x, + y: m1.end.y, + p: V.KING, + c: (p1 == V.KING ? 'w' : 'b') + }); + } + } else { + // One move is a self-capture and the other a normal capture: + // only the self-capture appears + const selfCaptureMove = + m1.vanish[1].c == m1.vanish[0].c + ? m1 + : m2; + smove.appear.push({ + x: m1.end.x, + y: m1.end.y, + p: selfCaptureMove.appear[0].p, + c: selfCaptureMove.vanish[0].c + }); + smove.vanish.push({ + x: m1.end.x, + y: m1.end.y, + p: selfCaptureMove.vanish[1].p, + c: selfCaptureMove.vanish[0].c + }); + } + } + return smove; + } + + play(move) { + move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo) + this.epSquares.push(this.getEpSquare(move)); + // Do not play on board (would reveal the move...) + this.turn = V.GetOppCol(this.turn); + this.movesCount++; + this.postPlay(move); + } + + updateCastleFlags(move) { + const firstRank = { 'w': V.size.x - 1, 'b': 0 }; + move.appear.concat(move.vanish).forEach(av => { + for (let c of ['w', 'b']) { + if (av.x == firstRank[c] && this.castleFlags[c].includes(av.y)) { + const flagIdx = (av.y == this.castleFlags[c][0] ? 0 : 1); + this.castleFlags[c][flagIdx] = 8; + } + } + }); + } + + postPlay(move) { + if (this.turn == 'b') { + // NOTE: whiteMove is used read-only, so no need to copy + this.whiteMove = move; + return; + } + + // A full turn just ended: + const smove = this.resolveSynchroneMove(move); + V.PlayOnBoard(this.board, smove); + move.whiteMove = this.whiteMove; //for undo + this.whiteMove = null; + + // Update king position + flags + let kingAppear = { 'w': false, 'b': false }; + for (let i=0; i= 0 && this.underCheck('w')) + res.push(JSON.parse(JSON.stringify(this.kingPos['w']))); + if (this.kingPos['b'][0] >= 0 && this.underCheck('b')) + res.push(JSON.parse(JSON.stringify(this.kingPos['b']))); + if (color == 'b') this.play(lastMove); + return res; + } + + getCurrentScore() { + if (this.turn == 'b') + // Turn (white + black) not over yet + return "*"; + // Was a king captured? + if (this.kingPos['w'][0] < 0) return "0-1"; + if (this.kingPos['b'][0] < 0) return "1-0"; + const whiteCanMove = this.atLeastOneMove('w'); + const blackCanMove = this.atLeastOneMove('b'); + if (whiteCanMove && blackCanMove) return "*"; + // Game over + const whiteInCheck = this.underCheck('w'); + const blackInCheck = this.underCheck('b'); + if ( + (whiteCanMove && !this.underCheck('b')) || + (blackCanMove && !this.underCheck('w')) + ) { + return "1/2"; + } + // Checkmate: could be mutual + if (!whiteCanMove && !blackCanMove) return "1/2"; + return (whiteCanMove ? "1-0" : "0-1"); + } + + getComputerMove() { + const maxeval = V.INFINITY; + const color = this.turn; + let moves = this.getAllValidMoves(); + if (moves.length == 0) + // TODO: this situation should not happen + return null; + + if (Math.random() < 0.5) + // Return a random move + return moves[randInt(moves.length)]; + + // Rank moves at depth 1: + // try to capture something (not re-capturing) + moves.forEach(m => { + V.PlayOnBoard(this.board, m); + m.eval = this.evalPosition(); + V.UndoOnBoard(this.board, m); + }); + moves.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++) + candidates.push(i); + return moves[candidates[randInt(candidates.length)]]; + } + + getNotation(move) { + if (move.appear.length == 2 && move.appear[0].p == V.KING) + // Castle + return move.end.y < move.start.y ? "0-0-0" : "0-0"; + // Basic system: piece + init + dest square + return ( + move.vanish[0].p.toUpperCase() + + V.CoordsToSquare(move.start) + + V.CoordsToSquare(move.end) + ); + } };