X-Git-Url: https://git.auder.net/variants/Cwda/complete_rules.html?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FDynamo.js;h=25954d57952e54cce8af1d6e5044ab63e2c26e90;hb=b2655276e322972b5c052d8c81db78e8cd6c5e2e;hp=9f4cf3ffc2878aef25f57455f49b84baf42fb347;hpb=f9385686d9434c607cdaa55e41a0425269db0815;p=vchess.git diff --git a/client/src/variants/Dynamo.js b/client/src/variants/Dynamo.js index 9f4cf3ff..25954d57 100644 --- a/client/src/variants/Dynamo.js +++ b/client/src/variants/Dynamo.js @@ -1,7 +1,7 @@ import { ChessRules, Move, PiPo } from "@/base_rules"; export class DynamoRules extends ChessRules { - // TODO: later, allow to push out pawns on a and h files? + // TODO: later, allow to push out pawns on a and h files static get HasEnpassant() { return false; } @@ -17,8 +17,7 @@ export class DynamoRules extends ChessRules { // Local stack of "action moves" this.amoves = []; const amove = V.ParseFen(fen).amove; - if (cmove == "-") this.amoves.push(null); - else { + if (amove != "-") { const amoveParts = amove.split("/"); let amove = { // No need for start & end @@ -26,7 +25,7 @@ export class DynamoRules extends ChessRules { vanish: [] }; [0, 1].map(i => { - amoveParts[0].split(".").forEach(av => { + amoveParts[i].split(".").forEach(av => { // Format is "bpe3" const xy = V.SquareToCoords(av.substr(2)); move[i == 0 ? "appear" : "vanish"].push( @@ -41,12 +40,14 @@ export class DynamoRules extends ChessRules { }); this.amoves.push(move); } + // Stack "first moves" (on subTurn 1) to merge and check opposite moves + this.firstMove = []; } static ParseFen(fen) { return Object.assign( ChessRules.ParseFen(fen), - { cmove: fen.split(" ")[4] } + { amove: fen.split(" ")[4] } ); } @@ -59,28 +60,30 @@ export class DynamoRules extends ChessRules { return true; } - getAmove(move) { - if (move.appear.length == 2 && move.vanish.length == 2) - return { appear: move.appear, vanish: move.vanish }; - return null; + getFen() { + return super.getFen() + " " + this.getAmoveFen(); } - // TODO: this.firstMove + rooks location in setOtherVariables - // only rooks location in FEN (firstMove is forgotten if quit game and come back) - doClick(square) { - // If subTurn == 2 && square is the final square of last move, - // then return an empty move - if ( - this.subTurn == 2 && - square.x == this.firstMove.end.x && - square.y == this.firstMove.end.y - ) { - return { - appear: [], - vanish: [] - }; - } - return null; + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getAmoveFen(); + } + + getAmoveFen() { + const L = this.amoves.length; + return ( + ["appear","vanish"].map( + mpart => { + return ( + this.amoves[L-1][mpart].map( + av => { + const square = V.CoordsToSquare({ x: av.x, y: av.y }); + return av.c + av.p + square; + } + ).join(".") + ); + } + ).join("/") + ); } canTake() { @@ -88,136 +91,212 @@ export class DynamoRules extends ChessRules { return false; } - // "pa" : piece (as a square) doing this push/pull action - getActionMoves([sx, sy], [ex, ey], pa) { - const color = this.getColor(sx, sy); - const lastRank = (color == 'w' ? 0 : 7); - const piece = this.getPiece(sx, sy); + // Step is right, just add (push/pull) moves in this direction + // Direction is assumed normalized. + getMovesInDirection([x, y], [dx, dy], nbSteps) { + nbSteps = nbSteps || 8; //max 8 steps anyway + let [i, j] = [x + dx, y + dy]; let moves = []; - if (ex == lastRank && piece == V.PAWN) { - // Promotion by push or pull - V.PawnSpecs.promotions.forEach(p => { - let move = super.getBasicMove([sx, sy], [ex, ey], { c: color, p: p }); - moves.push(move); - }); - } else moves.push(super.getBasicMove([sx, sy], [ex, ey])); - const actionType = - ( - Math.abs(pa[0] - sx) < Math.abs(pa[0] - ex) || - Math.abs(pa[1] - sy) < Math.abs(pa[1] - ey) - ) - ? "push" - : "pull"; - moves.forEach(m => m.action = [{ by: pa, type: actionType }]); + const color = this.getColor(x, y); + const piece = this.getPiece(x, y); + const lastRank = (color == 'w' ? 0 : 7); + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + if (i == lastRank && piece == V.PAWN) { + // Promotion by push or pull + V.PawnSpecs.promotions.forEach(p => { + let move = super.getBasicMove([x, y], [i, j], { c: color, p: p }); + moves.push(move); + }); + } + else moves.push(super.getBasicMove([x, y], [i, j])); + } + 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])), + appear: [], + vanish: [{ x: x, y: y, c: color, p: piece }] + }) + ); + } return moves; } - // TODO: if type is given, consider only actions of this type - getPactions(sq, color, type) { - const [x, y] = sq; - let moves = []; - let squares = {}; - if (!by) { - const oppCol = V.GetOppCol(color); - // Look in all directions for a "color" piece + // Normalize direction to know the step + getNormalizedDirection([dx, dy]) { + const absDir = [Math.abs(dx), Math.abs(dy)]; + let divisor = 0; + if (absDir[0] != 0 && absDir[1] != 0 && absDir[0] != absDir[1]) + // Knight + divisor = Math.min(absDir[0], absDir[1]); + else + // Standard slider (or maybe a pawn or king: same) + divisor = Math.max(absDir[0], absDir[1]); + return [dx / divisor, dy / divisor]; + } + + // There is something on x2,y2, maybe our color, pushed/pulled + static IsAprioriValidMove([x1, y1], [x2, y2]) { + 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 deltaX = Math.abs(x1 - x2); + const deltaY = Math.abs(y1 - y2); + switch (this.getPiece(x1, y1)) { + case V.PAWN: + return ( + ( + color1 == color2 && + y1 == y2 && + ( + x1 + pawnShift == x2 || + x1 == pawnStartRank && x1 + 2 * pawnShift == x2 + ) + ) + || + ( + color1 != color2 && + deltaY == 1 && + x1 + pawnShift == x2 + ) + ); + case V.ROOK: + return (x1 == x2 || y1 == y2); + case V.KNIGHT: { + return (deltaX + deltaY == 3 && (deltaX == 1 || deltaY == 1)); + } + case V.BISHOP: + return (deltaX == deltaY); + case V.QUEEN: + return ( + (deltaX == 0 || deltaY == 0 || deltaX == deltaY) + ); + case V.KING: + return (deltaX <= 1 && deltaY <= 1); + } + return false; + } + + // NOTE: for pushes, play the pushed piece first. + // for pulls: play the piece doing the action first + // NOTE: to push a piece out of the board, make it slide until its king + 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); + }; + const addMoves = (dir, nbSteps) => { + const newMoves = + this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps) + .filter(m => !movesHash[getMoveHash(m)]); + newMoves.forEach(m => { hashMoves[getMoveHash(m)] = true; }); + Array.prototype.push.apply(moves, newMoves); + }; + const pawnShift = (color == 'w' ? -1 : 1); + const pawnStartRank = (color == 'w' ? 6 : 1); + // [x, y] is pushed by 'color' for (let step of V.steps[V.KNIGHT]) { - const xx = x + step[0], - yy = y + step[1]; + 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)]) + ); + } + } + for (let step in 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]; + j += step[1]; + } if ( - V.OnBoard(xx, yy) && - this.getPiece(xx, yy) == V.KNIGHT && - this.getColor(xx, yy) == color + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color ) { - const px = x - step[0], - py = y - step[1]; - if (V.OnBoard(px, py)) { - if (this.board[px][py] == V.EMPTY) { - const hash = "s" + px + py; - if (!squares[hash]) { - squares[hash] = true; - Array.prototype.push.apply( - moves, - this.getActionMoves([x, y], [px, py], [xx, yy]) - ); - } - else { //add piece doing action + const deltaX = Math.abs(i - x); + const deltaY = Math.abs(j - y); + // Can a priori go both ways, except with pawns + switch (this.getPiece(i, j)) { + case V.PAWN: + if (deltaX <= 2 && deltaY <= 1) { + const pColor = this.getColor(x, y); + if (pColor == color && deltaY == 0) { + // Pushed forward + const maxSteps = (i == pawnStartRank && deltaX == 1 ? 2 : 1); + addMoves(step, maxSteps); + } + else if (pColor != color && deltaY == 1 && deltaX == 1) + // Pushed diagonally + addMoves(step, 1); } - } - } else { - const hash = "s" + xx + yy; - if (!squares[hash]) { - squares[hash] = true; - moves.push( - new Move({ - start: { x: x, y: y }, - end: { x: xx, y: yy }, - appear: [], - vanish: [ - new PiPo({ - x: x, - y: y, - p: this.getPiece(x, y), - c: oppCol - }) - ] - }) - ); - } + break; + 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; + case V.QUEEN: + if (deltaX == 0 || deltaY == 0 || deltaX == deltaY) + addMoves(step); + break; + case V.KING: + if (deltaX <= 1 && deltaY <= 1) addMoves(step, 1); + break; } } } - for (let step in V.steps[V.ROOK]) { - // (+ if color is ours, pawn pushes) king, rook and queen - // --> pawns special case can push from a little distance if on 2nd rank (or 1st rank) - } - for (let step in V.steps[V.BISHOP]) { - // King, bishop, queen, and possibly pawns attacks (if color is enemy) - } + return moves; } - return moves; - } - - // NOTE: to push a piece out of the board, make it slide until our piece - // (doing the action, moving or not) - // TODO: for pushes, play the pushed piece first. - // for pulls: play the piece doing the action first - getPotentialMovesFrom([x, y]) { - const color = this.turn; - if (this.getColor(x, y) != color) - // The only moves possible with enemy pieces are pulls and pushes: - return this.getPactions([x, y], color); - // Playing my pieces: either on their own, or pushed by another // If subTurn == 2 then we should have a first move, - // TODO = use it to allow some type of action - if (this.subTurn == 2) { - return ( - this.moveOnSubturn1.isAnAction - ? super.getPotentialMovesFrom([x, y]) - : this.getPactions([x, y], color, TODO_arg) - ); + // which restrict what we can play now: only in the first move direction + // NOTE: no need for knight or pawn checks, because the move will be + // naturally limited in those cases. + const L = this.firstMove.length; + const fm = this.firstMove[L-1]; + if (fm.appear.length == 2 && fm.vanish.length == 2) + // Castle: no real move playable then. + return []; + 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) { + // Seems so: + const dir = this.getNormalizedDirection( + [fm.start.x - x, fm.start.y - y]); + return this.getMovesInDirection([x, y], dir); + } } - // Both options are possible at subTurn1: normal move, or push - return ( - super.getPotentialMovesFrom([x, y]) - .concat(this.getPactions([x, y], color, "push")) - // TODO: discard moves that let the king underCheck, and no second - // move can counter check. Example: pinned queen pushes pinned pawn. - .filter(m => { - this.play(m); - const res = this.filterMoves(this.getPotentialMoves(/* TODO: args? */)).length > 0; - this.undo(m); - return res; - }) - ); - } - - // TODO: track rooks locations, should be a field in FEN, in castleflags? - // --> only useful if castleFlags is still ON - getCastleMoves(sq) { - // TODO: if rook1 isn't at its place (with castleFlags ON), set it off - // same for rook2. - let moves = super.getCastleMoves(sq); - // TODO: restore castleFlags + else { + const dirM = this.getNormalizedDirection( + [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); + } + return []; } // Does m2 un-do m1 ? (to disallow undoing actions) @@ -238,7 +317,6 @@ export class DynamoRules extends ChessRules { return true; }; return ( - !!m1 && m1.appear.length == 2 && m2.appear.length == 2 && m1.vanish.length == 2 && @@ -248,13 +326,51 @@ export class DynamoRules extends ChessRules { ); } + getAmove(move1, move2) { + // Just merge (one is action one is move, one may be empty) + return { + appear: move1.appear.concat(move2.appear), + vanish: move1.vanish.concat(move2.vanish) + } + } + filterValid(moves) { - if (moves.length == 0) return []; const color = this.turn; - return moves.filter(m => { - const L = this.amoves.length; //at least 1: init from FEN - return !this.oppositeMoves(this.amoves[L - 1], m); - }); + if (this.subTurn == 1) { + return moves.filter(m => { + // A move is valid either if it doesn't result in a check, + // or if a second move is possible to counter the check + // (not undoing a potential move + action of the opponent) + this.play(m); + let res = this.underCheck(color); + if (res) { + const moves2 = this.getAllPotentialMoves(); + for (m2 of moves2) { + this.play(m2); + const res2 = this.underCheck(color); + this.undo(m2); + if (!res2) { + res = false; + break; + } + } + } + this.undo(m); + return !res; + }); + } + const Lf = this.firstMove.length; + const La = this.amoves.length; + if (La == 0) return super.filterValid(moves); + return ( + super.filterValid( + moves.filter(m => { + // Move shouldn't undo another: + const amove = this.getAmove(this.firstMove[Lf-1], m); + return !this.oppositeMoves(this.amoves[La-1], amove); + }) + ) + ); } isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) { @@ -313,50 +429,56 @@ export class DynamoRules extends ChessRules { return super.getCurrentScore(); } + doClick(square) { + // If subTurn == 2 && square is empty && !underCheck, + // then return an empty move, allowing to "pass" subTurn2 + if ( + this.subTurn == 2 && + this.board[square.x][square.y] == V.EMPTY && + !this.underCheck(this.turn) + ) { + return { + appear: [], + vanish: [] + }; + } + return null; + } + play(move) { move.flags = JSON.stringify(this.aggregateFlags()); V.PlayOnBoard(this.board, move); - if (this.subTurn == 1) { - // TODO: is there a second move possible? - // (if the first move is a normal one, there may be no actions available) - // --> If not, just change turn as ion the else {} section - this.subTurn = 2; - this.movesCount++; - } else { - // subTurn == 2 + if (this.subTurn == 2) { this.turn = V.GetOppCol(this.turn); - this.subTurn = 1; + this.movesCount++; } + else this.firstMove.push(move); + this.subTurn = 3 - this.subTurn; 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 rooks are moved (only) - if (piece == V.KING && move.appear.length > 0) - this.castleFlags[c] = [V.size.y, V.size.y]; - else if ( - move.start.x == firstRank && - this.castleFlags[c].includes(move.start.y) - ) { - const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1); - this.castleFlags[c][flagIdx] = V.size.y; + // Update castling flags + if (piece == V.KING) this.castleFlags[c] = [V.size.y, V.size.y]; + 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; + } } } undo(move) { this.disaggregateFlags(JSON.parse(move.flags)); V.UndoOnBoard(this.board, move); - if (this.subTurn == 2) { - this.subTurn = 1; - this.movesCount--; - } - else { - // subTurn == 1 (after a move played) + if (this.subTurn == 1) { this.turn = V.GetOppCol(this.turn); - this.subTurn = 2; + this.movesCount--; } + else this.firstMove.pop(); + this.subTurn = 3 - this.subTurn; this.postUndo(move); } };