X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FSynchrone.js;h=372bc1eea2c306f7ba30ad347c96e4817da72e34;hp=e47941bf2d9cdf31f38ac28b4d42dba02512b238;hb=d54f6261c9e30f4eabb402ad301dd5c5e40fb656;hpb=7e476ce4b299c0a35529b80c730a452992453cc6 diff --git a/client/src/variants/Synchrone.js b/client/src/variants/Synchrone.js index e47941bf..372bc1ee 100644 --- a/client/src/variants/Synchrone.js +++ b/client/src/variants/Synchrone.js @@ -1,11 +1,468 @@ +// TODO: debug, and forbid self-capture of king. + 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 true; //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 == "w" && + // NOTE: do not check really JSON stringified move... + (!fenParsed.whiteMove || fenParsed.whiteMove == "-") + ) + || + (fenParsed.turn == "b" && 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 + }); + } + + // NOTE: lazy unefficient implementation (for now. TODO?) + getPossibleMovesFrom([x, y]) { + const moves = this.getAllValidMoves(); + return moves.filter(m => { + return m.start.x == x && m.start.y == y; + }); + } + + getCaptures(x, y) { + const color = this.turn; + 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); + } + + getAllValidMoves() { + const color = this.turn; + // 0) Generate our possible moves + let myMoves = super.getAllValidMoves(); + // Lookup table to quickly decide if a move is already in list: + let moveSet = {}; + const getMoveHash = (move) => { + return ( + "m" + move.start.x + move.start.y + + move.end.x + move.end.y + + // Also use m.appear[0].p for pawn promotions + move.appear[0].p + ); + }; + myMoves.forEach(m => moveSet[getMoveHash(m)] = true); + // 1) Generate all opponent's moves + this.turn = V.GetOppCol(color); + const oppMoves = super.getAllValidMoves(); + this.turn = color; + // 2) Play each opponent's move, and see if captures are possible: + // --> capturing moving unit only (otherwise some issues) + oppMoves.forEach(m => { + V.PlayOnBoard(this.board, m); + // Can I take on [m.end.x, m.end.y] ? + // If yes and not already in list, add it (without the capturing part) + let capturingMoves = this.getCaptures(m.end.x, m.end.y); + capturingMoves.forEach(cm => { + const cmHash = getMoveHash(cm); + if (!moveSet[cmHash]) { + // The captured unit hasn't moved yet, so temporarily cancel capture + cm.vanish.pop(); + // If m is itself a capturing move: then replace by self-capture + if (m.vanish.length == 2) cm.vanish.push(m.vanish[1]); + myMoves.push(cm); + moveSet[cmHash] = true; + } + }); + V.UndoOnBoard(this.board, m); + }); + return myMoves; + } + + 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.vanish.length == 2 && + ( + m2.end.x != m1.vanish[1].x || + m2.end.y != m1.vanish[1].y + ) + ) { + smove.vanish.push(m1.vanish[1]); + } + if ( + m2.vanish.length == 2 && + ( + m1.end.x != m2.vanish[1].x || + m1.end.y != m2.vanish[1].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 + console.log(m1); + console.log(m2); + 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 + }); + } + } + 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); + this.whiteMove = null; + + // Update king position + flags + let kingAppear = { 'w': false, 'b': false }; + for (let i=0; i { + 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)]]; + } };