X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FDynamo.js;h=f7d602405cffd8080d999795d52bba08b9bc58e9;hb=8c267d0c700050831a10d0934df1d1606aa0d654;hp=25954d57952e54cce8af1d6e5044ab63e2c26e90;hpb=b2655276e322972b5c052d8c81db78e8cd6c5e2e;p=vchess.git diff --git a/client/src/variants/Dynamo.js b/client/src/variants/Dynamo.js index 25954d57..f7d60240 100644 --- a/client/src/variants/Dynamo.js +++ b/client/src/variants/Dynamo.js @@ -1,4 +1,5 @@ import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; export class DynamoRules extends ChessRules { // TODO: later, allow to push out pawns on a and h files @@ -19,27 +20,30 @@ export class DynamoRules extends ChessRules { const amove = V.ParseFen(fen).amove; if (amove != "-") { const amoveParts = amove.split("/"); - let amove = { + let move = { // No need for start & end appear: [], vanish: [] }; [0, 1].map(i => { - amoveParts[i].split(".").forEach(av => { - // Format is "bpe3" - const xy = V.SquareToCoords(av.substr(2)); - move[i == 0 ? "appear" : "vanish"].push( - new PiPo({ - x: xy.x, - y: xy.y, - c: av[0], - p: av[1] - }) - ); - }); + if (amoveParts[i] != "-") { + amoveParts[i].split(".").forEach(av => { + // Format is "bpe3" + const xy = V.SquareToCoords(av.substr(2)); + move[i == 0 ? "appear" : "vanish"].push( + new PiPo({ + x: xy.x, + y: xy.y, + c: av[0], + p: av[1] + }) + ); + }); + } }); this.amoves.push(move); } + this.subTurn = 1; // Stack "first moves" (on subTurn 1) to merge and check opposite moves this.firstMove = []; } @@ -54,9 +58,19 @@ export class DynamoRules extends ChessRules { static IsGoodFen(fen) { if (!ChessRules.IsGoodFen(fen)) return false; const fenParts = fen.split(" "); - if (fenParts.length != 6) return false; - if (fenParts[5] != "-" && !fenParts[5].match(/^([a-h][1-8]){2}$/)) - return false; + if (fenParts.length != 5) return false; + if (fenParts[4] != "-") { + // TODO: a single regexp instead. + // Format is [bpa2[.wpd3]] || '-'/[bbc3[.wrd5]] || '-' + const amoveParts = fenParts[4].split("/"); + if (amoveParts.length != 2) return false; + for (let part of amoveParts) { + if (part != "-") { + for (let psq of part.split(".")) + if (!psq.match(/^[a-r]{3}[1-8]$/)) return false; + } + } + } return true; } @@ -70,9 +84,11 @@ export class DynamoRules extends ChessRules { getAmoveFen() { const L = this.amoves.length; + if (L == 0) return "-"; return ( ["appear","vanish"].map( mpart => { + if (mpart.length == 0) return "-"; return ( this.amoves[L-1][mpart].map( av => { @@ -100,6 +116,7 @@ export class DynamoRules extends ChessRules { const color = this.getColor(x, y); const piece = this.getPiece(x, y); const lastRank = (color == 'w' ? 0 : 7); + let counter = 1; while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { if (i == lastRank && piece == V.PAWN) { // Promotion by push or pull @@ -109,13 +126,16 @@ export class DynamoRules extends ChessRules { }); } else moves.push(super.getBasicMove([x, y], [i, j])); + if (++counter > nbSteps) break; + i += dx; + j += dy; } if (!V.OnBoard(i, j) && piece != V.KING) { // Add special "exit" move, by "taking king" moves.push( new Move({ start: { x: x, y: y }, - end: JSON.parse(JSON.stringify(this.kingPos[color])), + end: { x: this.kingPos[color][0], y: this.kingPos[color][1] }, appear: [], vanish: [{ x: x, y: y, c: color, p: piece }] }) @@ -137,45 +157,53 @@ export class DynamoRules extends ChessRules { return [dx / divisor, dy / divisor]; } - // There is something on x2,y2, maybe our color, pushed/pulled - static IsAprioriValidMove([x1, y1], [x2, y2]) { + // There was something on x2,y2, maybe our color, pushed/pulled. + // Also, the pushed/pulled piece must exit the board. + isAprioriValidExit([x1, y1], [x2, y2], color2) { const color1 = this.getColor(x1, y1); - const color2 = this.getColor(x2, y2); const pawnShift = (color1 == 'w' ? -1 : 1); - const pawnStartRank = (color1 == 'w' ? 6 : 1); + const lastRank = (color1 == 'w' ? 0 : 7); const deltaX = Math.abs(x1 - x2); const deltaY = Math.abs(y1 - y2); + const checkSlider = () => { + const dir = this.getNormalizedDirection([x2 - x1, y2 - y1]); + let [i, j] = [x1 + dir[0], y1 + dir[1]]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += dir[0]; + j += dir[1]; + } + return !V.OnBoard(i, j); + }; switch (this.getPiece(x1, y1)) { case V.PAWN: return ( + x1 + pawnShift == x2 && ( - color1 == color2 && - y1 == y2 && - ( - x1 + pawnShift == x2 || - x1 == pawnStartRank && x1 + 2 * pawnShift == x2 - ) - ) - || - ( - color1 != color2 && - deltaY == 1 && - x1 + pawnShift == x2 + (color1 == color2 && x2 == lastRank && y1 == y2) || + (color1 != color2 && deltaY == 1 && !V.OnBoard(x2, 2 * y2 - y1)) ) ); case V.ROOK: - return (x1 == x2 || y1 == y2); - case V.KNIGHT: { - return (deltaX + deltaY == 3 && (deltaX == 1 || deltaY == 1)); - } + if (x1 != x2 && y1 != y2) return false; + return checkSlider(); + case V.KNIGHT: + return ( + deltaX + deltaY == 3 && + (deltaX == 1 || deltaY == 1) && + !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) + ); case V.BISHOP: - return (deltaX == deltaY); + if (deltaX != deltaY) return false; + return checkSlider(); case V.QUEEN: + if (deltaX != 0 && deltaY != 0 && deltaX != deltaY) return false; + return checkSlider(); + case V.KING: return ( - (deltaX == 0 || deltaY == 0 || deltaX == deltaY) + deltaX <= 1 && + deltaY <= 1 && + !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) ); - case V.KING: - return (deltaX <= 1 && deltaY <= 1); } return false; } @@ -186,11 +214,6 @@ export class DynamoRules extends ChessRules { getPotentialMovesFrom([x, y]) { const color = this.turn; if (this.subTurn == 1) { - // Free to play any move: - const moves = super.getPotentialMovesFrom([x, y]) - // Structure to avoid adding moves twice (can be action & move) - let hashMoves = {}; - moves.forEach(m => { hashMoves[getMoveHash(m)] = true; }); const getMoveHash = (m) => { return V.CoordsToSquare(m.start) + V.CoordsToSquare(m.end); }; @@ -198,24 +221,32 @@ export class DynamoRules extends ChessRules { const newMoves = this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps) .filter(m => !movesHash[getMoveHash(m)]); - newMoves.forEach(m => { hashMoves[getMoveHash(m)] = true; }); + newMoves.forEach(m => { movesHash[getMoveHash(m)] = true; }); Array.prototype.push.apply(moves, newMoves); }; + // Free to play any move (if piece of my color): + const moves = + this.getColor(x, y) == color + ? super.getPotentialMovesFrom([x, y]) + : []; const pawnShift = (color == 'w' ? -1 : 1); const pawnStartRank = (color == 'w' ? 6 : 1); + // Structure to avoid adding moves twice (can be action & move) + let movesHash = {}; + moves.forEach(m => { movesHash[getMoveHash(m)] = true; }); // [x, y] is pushed by 'color' for (let step of V.steps[V.KNIGHT]) { const [i, j] = [x + step[0], y + step[1]]; - if (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) { - // Only can move away from a knight (can pull but must move first) - Array.prototype.push.apply( - moves, - this.getMovesInDirection([x, y], [-step[0], -step[1]], 1) - .filter(m => !movesHash[getMoveHash(m)]) - ); + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.getPiece(i, j) == V.KNIGHT + ) { + addMoves(step, 1); } } - for (let step in V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { let [i, j] = [x + step[0], y + step[1]]; while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { i += step[0]; @@ -231,7 +262,11 @@ export class DynamoRules extends ChessRules { // Can a priori go both ways, except with pawns switch (this.getPiece(i, j)) { case V.PAWN: - if (deltaX <= 2 && deltaY <= 1) { + if ( + (x - i) / deltaX == pawnShift && + deltaX <= 2 && + deltaY <= 1 + ) { const pColor = this.getColor(x, y); if (pColor == color && deltaY == 0) { // Pushed forward @@ -246,10 +281,6 @@ export class DynamoRules extends ChessRules { case V.ROOK: if (deltaX == 0 || deltaY == 0) addMoves(step); break; - case V.KNIGHT: - if (deltaX + deltaY == 3 && (deltaX == 1 || deltaY == 1)) - addMoves(step, 1); - break; case V.BISHOP: if (deltaX == deltaY) addMoves(step); break; @@ -277,10 +308,13 @@ export class DynamoRules extends ChessRules { if (fm.appear.length == 0) { // Piece at subTurn 1 just exited the board. // Can I be a piece which caused the exit? - this.undo(fm); - const moveOk = V.IsAprioriValidMove([x, y], [fm.start.x, fm.start.y]); - this.play(fm); - if (moveOk) { + if ( + this.isAprioriValidExit( + [x, y], + [fm.start.x, fm.start.y], + fm.vanish[0].c + ) + ) { // Seems so: const dir = this.getNormalizedDirection( [fm.start.x - x, fm.start.y - y]); @@ -292,9 +326,20 @@ export class DynamoRules extends ChessRules { [fm.end.x - fm.start.x, fm.end.y - fm.start.y]); const dir = this.getNormalizedDirection( [fm.start.x - x, fm.start.y - y]); - // Normalized directions should match: - if (dir[0] == dirM[0] && dir[1] == dirM[1]) - return this.getMovesInDirection([x, y], dir); + // Normalized directions should match + if (dir[0] == dirM[0] && dir[1] == dirM[1]) { + // And nothing should stand between [x, y] and the square fm.start + let [i, j] = [x + dir[0], y + dir[1]]; + while ( + (i != fm.start.x || j != fm.start.y) && + this.board[i][j] == V.EMPTY + ) { + i += dir[0]; + j += dir[1]; + } + if (i == fm.start.x && j == fm.start.y) + return this.getMovesInDirection([x, y], dir); + } } return []; } @@ -345,7 +390,7 @@ export class DynamoRules extends ChessRules { let res = this.underCheck(color); if (res) { const moves2 = this.getAllPotentialMoves(); - for (m2 of moves2) { + for (let m2 of moves2) { this.play(m2); const res2 = this.underCheck(color); this.undo(m2); @@ -386,7 +431,19 @@ export class DynamoRules extends ChessRules { this.getPiece(rx, ry) == piece && this.getColor(rx, ry) == color ) { - // Now step in the other direction: if end of the world, then attacked + // Continue some steps in the same direction (pull) + rx += step[0]; + ry += step[1]; + while ( + V.OnBoard(rx, ry) && + this.board[rx][ry] == V.EMPTY && + !oneStep + ) { + rx += step[0]; + ry += step[1]; + } + if (!V.OnBoard(rx, ry)) return true; + // Step in the other direction (push) rx = x - step[0]; ry = y - step[1]; while ( @@ -405,7 +462,7 @@ export class DynamoRules extends ChessRules { isAttackedByPawn([x, y], color) { const lastRank = (color == 'w' ? 0 : 7); - if (y != lastRank) + if (x != lastRank) // The king can be pushed out by a pawn only on last rank return false; const pawnShift = (color == "w" ? 1 : -1); @@ -434,10 +491,12 @@ export class DynamoRules extends ChessRules { // then return an empty move, allowing to "pass" subTurn2 if ( this.subTurn == 2 && - this.board[square.x][square.y] == V.EMPTY && + this.board[square[0]][square[1]] == V.EMPTY && !this.underCheck(this.turn) ) { return { + start: { x: -1, y: -1 }, + end: { x: -1, y: -1 }, appear: [], vanish: [] }; @@ -449,6 +508,8 @@ export class DynamoRules extends ChessRules { move.flags = JSON.stringify(this.aggregateFlags()); V.PlayOnBoard(this.board, move); if (this.subTurn == 2) { + const L = this.firstMove.length; + this.amoves.push(this.getAmove(this.firstMove[L-1], move)); this.turn = V.GetOppCol(this.turn); this.movesCount++; } @@ -457,15 +518,20 @@ export class DynamoRules extends ChessRules { this.postPlay(move); } - updateCastleFlags(move, piece) { - const c = V.GetOppCol(this.turn); - const firstRank = (c == "w" ? V.size.x - 1 : 0); - // Update castling flags - if (piece == V.KING) this.castleFlags[c] = [V.size.y, V.size.y]; + postPlay(move) { + if (move.start.x < 0) return; + for (let a of move.appear) + if (a.p == V.KING) this.kingPos[a.c] = [a.x, a.y]; + this.updateCastleFlags(move); + } + + updateCastleFlags(move) { + const firstRank = { 'w': V.size.x - 1, 'b': 0 }; for (let v of move.vanish) { - if (v.x == firstRank && this.castleFlags[c].includes(v.y)) { - const flagIdx = (v.y == this.castleFlags[c][0] ? 0 : 1); - this.castleFlags[c][flagIdx] = V.size.y; + if (v.p == V.KING) this.castleFlags[v.c] = [V.size.y, V.size.y]; + else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) { + const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1); + this.castleFlags[v.c][flagIdx] = V.size.y; } } } @@ -481,4 +547,70 @@ export class DynamoRules extends ChessRules { this.subTurn = 3 - this.subTurn; this.postUndo(move); } + + postUndo(move) { + // (Potentially) Reset king position + for (let v of move.vanish) + if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y]; + } + + getComputerMove() { + let moves = this.getAllValidMoves(); + if (moves.length == 0) return null; + // "Search" at depth 1 for now + const maxeval = V.INFINITY; + const color = this.turn; + const emptyMove = { + start: { x: -1, y: -1 }, + end: { x: -1, y: -1 }, + appear: [], + vanish: [] + }; + moves.forEach(m => { + this.play(m); + m.eval = (color == "w" ? -1 : 1) * maxeval; + const moves2 = this.getAllValidMoves().concat([emptyMove]); + m.next = moves2[0]; + moves2.forEach(m2 => { + this.play(m2); + const score = this.getCurrentScore(); + let mvEval = 0; + if (score != "1/2") { + if (score != "*") mvEval = (score == "1-0" ? 1 : -1) * maxeval; + else mvEval = this.evalPosition(); + } + if ( + (color == 'w' && mvEval > m.eval) || + (color == 'b' && mvEval < m.eval) + ) { + m.eval = mvEval; + m.next = m2; + } + this.undo(m2); + }); + this.undo(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); + const mIdx = candidates[randInt(candidates.length)]; + const move2 = moves[mIdx].next; + delete moves[mIdx]["next"]; + return [moves[mIdx], move2]; + } + + getNotation(move) { + if (move.start.x < 0) + // A second move is always required, but may be empty + return "-"; + const initialSquare = V.CoordsToSquare(move.start); + const finalSquare = V.CoordsToSquare(move.end); + if (move.appear.length == 0) + // Pushed or pulled out of the board + return initialSquare + "R"; + return move.appear[0].p.toUpperCase() + initialSquare + finalSquare; + } };