X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=base_rules.js;h=a2e5f61f4f78b4fb524263406c612b5a7016e6ca;hb=ca8a399316d2496c069ea9c6ccf2dc241aeb70ef;hp=6562673aeb3804f8fc7bfa494d6494d839769f98;hpb=5f08c59b29c2173cc8b2df1a3799ee971a14e691;p=xogo.git diff --git a/base_rules.js b/base_rules.js index 6562673..a2e5f61 100644 --- a/base_rules.js +++ b/base_rules.js @@ -111,10 +111,6 @@ export default class ChessRules { return !!this.options["dark"]; } - get hasMoveStack() { - return false; - } - // Some variants use click infos: doClick(coords) { if (typeof coords.x != "number") @@ -129,7 +125,7 @@ export default class ChessRules { new PiPo({ x: coords.x, y: coords.y, - c: this.captured.c, //this.turn, + c: this.captured.c, p: this.captured.p }) ], @@ -470,46 +466,9 @@ export default class ChessRules { this.moveStack = []; } - updateEnlightened() { - this.oldEnlightened = this.enlightened; - this.enlightened = ArrayFun.init(this.size.x, this.size.y, false); - // Add pieces positions + all squares reachable by moves (includes Zen): - for (let x=0; x { - this.enlightened[m.end.x][m.end.y] = true; - }); - } - } - } - if (this.epSquare) - this.enlightEnpassant(); - } - - // Include square of the en-passant capturing square: - enlightEnpassant() { - // NOTE: shortcut, pawn has only one attack type, doesn't depend on square - const steps = this.pieces(this.playerColor)["p"].attack[0].steps; - for (let step of steps) { - const x = this.epSquare.x - step[0], - y = this.getY(this.epSquare.y - step[1]); - if ( - this.onBoard(x, y) && - this.getColor(x, y) == this.playerColor && - this.getPieceType(x, y) == "p" - ) { - this.enlightened[x][this.epSquare.y] = true; - break; - } - } - } - - // ordering as in pieces() p,r,n,b,q,k (+ count in base 30 if needed) + // ordering as in pieces() p,r,n,b,q,k initReserves(reserveStr) { - const counts = reserveStr.split("").map(c => parseInt(c, 30)); + const counts = reserveStr.split("").map(c => parseInt(c, 36)); this.reserve = { w: {}, b: {} }; const pieceName = ['p', 'r', 'n', 'b', 'q', 'k']; const L = pieceName.length; @@ -528,6 +487,22 @@ export default class ChessRules { this.ispawn = {}; } + //////////////// + // VISUAL UTILS + + getPieceWidth(rwidth) { + return (rwidth / this.size.y); + } + + getReserveSquareSize(rwidth, nbR) { + const sqSize = this.getPieceWidth(rwidth); + return Math.min(sqSize, rwidth / nbR); + } + + getReserveNumId(color, piece) { + return `${this.containerId}|rnum-${color}${piece}`; + } + getNbReservePieces(color) { return ( Object.values(this.reserve[color]).reduce( @@ -543,22 +518,6 @@ export default class ChessRules { (oldV,newV) => oldV + (this.reserve[c][newV] > 0 ? 1 : 0), 0); } - ////////////// - // VISUAL PART - - getPieceWidth(rwidth) { - return (rwidth / this.size.y); - } - - getReserveSquareSize(rwidth, nbR) { - const sqSize = this.getPieceWidth(rwidth); - return Math.min(sqSize, rwidth / nbR); - } - - getReserveNumId(color, piece) { - return `${this.containerId}|rnum-${color}${piece}`; - } - static AddClass_es(piece, class_es) { if (!Array.isArray(class_es)) class_es = [class_es]; @@ -575,6 +534,23 @@ export default class ChessRules { }); } + // Generally light square bottom-right + getSquareColorClass(x, y) { + return ((x+y) % 2 == 0 ? "light-square": "dark-square"); + } + + getMaxDistance(r) { + // Works for all rectangular boards: + return Math.sqrt(r.width ** 2 + r.height ** 2); + } + + getDomPiece(x, y) { + return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y]; + } + + ////////////////// + // VISUAL METHODS + graphicalInit() { // NOTE: not window.onresize = this.re_drawBoardElts because scope (this) window.onresize = () => this.re_drawBoardElements(); @@ -661,11 +637,6 @@ export default class ChessRules { return board; } - // Generally light square bottom-right - getSquareColorClass(x, y) { - return ((x+y) % 2 == 0 ? "light-square": "dark-square"); - } - setupPieces(r) { if (this.g_pieces) { // Refreshing: delete old pieces first @@ -793,30 +764,6 @@ export default class ChessRules { } } - // Apply diff this.enlightened --> oldEnlightened on board - graphUpdateEnlightened() { - let chessboard = - document.getElementById(this.containerId).querySelector(".chessboard"); - const r = chessboard.getBoundingClientRect(); - const pieceWidth = this.getPieceWidth(r.width); - for (let x=0; x= 2) this.showChoices(moves, r); else if (moves.length == 1) - this.playPlusVisual(moves[0], r); + this.buildMoveStack(moves[0], r); } curPiece.remove(); }; @@ -1050,7 +997,7 @@ export default class ChessRules { const callback = (m) => { chessboard.style.opacity = "1"; container.removeChild(choices); - this.playPlusVisual(m, r); + this.buildMoveStack(m, r); } for (let i=0; i < moves.length; i++) { let choice = document.createElement("div"); @@ -1073,6 +1020,70 @@ export default class ChessRules { } } + //////////////// + // DARK METHODS + + updateEnlightened() { + this.oldEnlightened = this.enlightened; + this.enlightened = ArrayFun.init(this.size.x, this.size.y, false); + // Add pieces positions + all squares reachable by moves (includes Zen): + for (let x=0; x { + this.enlightened[m.end.x][m.end.y] = true; + }); + } + } + } + if (this.epSquare) + this.enlightEnpassant(); + } + + // Include square of the en-passant capturing square: + enlightEnpassant() { + // NOTE: shortcut, pawn has only one attack type, doesn't depend on square + const steps = this.pieces(this.playerColor)["p"].attack[0].steps; + for (let step of steps) { + const x = this.epSquare.x - step[0], + y = this.getY(this.epSquare.y - step[1]); + if ( + this.onBoard(x, y) && + this.getColor(x, y) == this.playerColor && + this.getPieceType(x, y) == "p" + ) { + this.enlightened[x][this.epSquare.y] = true; + break; + } + } + } + + // Apply diff this.enlightened --> oldEnlightened on board + graphUpdateEnlightened() { + let chessboard = + document.getElementById(this.containerId).querySelector(".chessboard"); + const r = chessboard.getBoundingClientRect(); + const pieceWidth = this.getPieceWidth(r.width); + for (let x=0; x= 0 && x < this.size.x && @@ -1157,14 +1163,12 @@ export default class ChessRules { } ] }, - // rook 'r': { "class": "rook", moves: [ {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]} ] }, - // knight 'n': { "class": "knight", moves: [ @@ -1177,14 +1181,12 @@ export default class ChessRules { } ] }, - // bishop 'b': { "class": "bishop", moves: [ {steps: [[1, 1], [1, -1], [-1, 1], [-1, -1]]} ] }, - // queen 'q': { "class": "queen", moves: [ @@ -1196,7 +1198,6 @@ export default class ChessRules { } ] }, - // king 'k': { "class": "king", moves: [ @@ -1218,8 +1219,31 @@ export default class ChessRules { }; } - //////////////////// - // MOVES GENERATION + // NOTE: using special symbols to not interfere with variants' pieces codes + static get CannibalKings() { + return { + "!": "p", + "#": "r", + "$": "n", + "%": "b", + "*": "q", + "k": "k" + }; + } + + static get CannibalKingCode() { + return { + "p": "!", + "r": "#", + "n": "$", + "b": "%", + "q": "*", + "k": "k" + }; + } + + ////////////////////////// + // MOVES GENERATION UTILS // For Cylinder: get Y coordinate getY(y) { @@ -1231,38 +1255,93 @@ export default class ChessRules { return res; } + getSegments(curSeg, segStart, segEnd) { + if (curSeg.length == 0) + return undefined; + let segments = JSON.parse(JSON.stringify(curSeg)); //not altering + segments.push([[segStart[0], segStart[1]], [segEnd[0], segEnd[1]]]); + return segments; + } + + getStepSpec(color, x, y) { + const allSpecs = this.pieces(color, x, y); + let stepSpec = allSpecs[piece]; + if (stepSpec.moveas) + stepSpec = allSpecs[stepSpec.moveas]; + return stepSpec; + } + + // Can thing on square1 capture thing on square2? + canTake([x1, y1], [x2, y2]) { + return this.getColor(x1, y1) !== this.getColor(x2, y2); + } + + canStepOver(i, j, p) { + // In some variants, objects on boards don't stop movement (Chakart) + return this.board[i][j] == ""; + } + + canDrop([c, p], [i, j]) { + return ( + this.board[i][j] == "" && + (!this.enlightened || this.enlightened[i][j]) && + ( + p != "p" || + (c == 'w' && i < this.size.x - 1) || + (c == 'b' && i > 0) + ) + ); + } + + // For Madrasi: + // (redefined in Baroque etc, where Madrasi condition doesn't make sense) + isImmobilized([x, y]) { + if (!this.options["madrasi"]) + return false; + const color = this.getColor(x, y); + const oppCol = C.GetOppCol(color); + const piece = this.getPieceType(x, y); //ok not cannibal king + const stepSpec = this.getStepSpec(color, x, y); + const attacks = stepSpec.attack || stepSpec.moves; + for (let a of attacks) { + outerLoop: for (let step of a.steps) { + let [i, j] = [x + step[0], y + step[1]]; + let stepCounter = 1; + while (this.onBoard(i, j) && this.board[i][j] == "") { + if (a.range <= stepCounter++) + continue outerLoop; + i += step[0]; + j = this.getY(j + step[1]); + } + if ( + this.onBoard(i, j) && + this.getColor(i, j) == oppCol && + this.getPieceType(i, j) == piece + ) { + return true; + } + } + } + return false; + } + // Stop at the first capture found atLeastOneCapture(color) { - color = color || this.turn; const oppCol = C.GetOppCol(color); - for (let i = 0; i < this.size.x; i++) { - for (let j = 0; j < this.size.y; j++) { - if (this.board[i][j] != "" && this.getColor(i, j) == color) { - const allSpecs = this.pieces(color, i, j) - let specs = allSpecs[this.getPieceType(i, j)]; - if (specs.moveas) - specs = allSpecs[specs.moveas]; - const attacks = specs.attack || specs.moves; - for (let a of attacks) { - outerLoop: for (let step of a.steps) { - let [ii, jj] = [i + step[0], this.getY(j + step[1])]; - let stepCounter = 1; - while (this.onBoard(ii, jj) && this.board[ii][jj] == "") { - if (a.range <= stepCounter++) - continue outerLoop; - ii += step[0]; - jj = this.getY(jj + step[1]); - } - if ( - this.onBoard(ii, jj) && - this.getColor(ii, jj) == oppCol && - this.filterValid( - [this.getBasicMove([i, j], [ii, jj])] - ).length >= 1 - ) { - return true; - } - } + const allowed = ([x, y]) => { + this.getColor(x, y) == oppCol && + this.filterValid([this.getBasicMove([i, j], [x, y])]).length >= 1 + }; + for (let i=0; i 1e-7) + continue; + distance = Math.round(distance); //in case of (numerical...) + if (range >= distance) + return true; + } + return false; + } + + //////////////////// + // MOVES GENERATION + getDropMovesFrom([c, p]) { // NOTE: by design, this.reserve[c][p] >= 1 on user click // (but not necessarily otherwise: atLeastOneMove() etc) @@ -1278,23 +1384,22 @@ export default class ChessRules { let moves = []; for (let i=0; i 0) - ) - ) { - moves.push( - new Move({ - start: {x: c, y: p}, - end: {x: i, y: j}, - appear: [new PiPo({x: i, y: j, c: c, p: p})], - vanish: [] - }) - ); + if (this.canDrop([c, p], [i, j])) { + let mv = new Move({ + start: {x: c, y: p}, + end: {x: i, y: j}, + appear: [new PiPo({x: i, y: j, c: c, p: p})], + vanish: [] + }); + if (this.board[i][j] != "") { + mv.vanish.push(new PiPo({ + x: i, + y: j, + c: this.getColor(i, j), + p: this.getPiece(i, j) + })); + } + moves.push(mv); } } } @@ -1302,28 +1407,22 @@ export default class ChessRules { } // All possible moves from selected square - getPotentialMovesFrom(sq, color) { + getPotentialMovesFrom([x, y], color) { if (this.subTurnTeleport == 2) return []; - if (typeof sq[0] == "string") - return this.getDropMovesFrom(sq); - if (this.isImmobilized(sq)) + if (typeof x == "string") + return this.getDropMovesFrom([x, y]); + if (this.isImmobilized([x, y])) return []; - const piece = this.getPieceType(sq[0], sq[1]); - let moves = this.getPotentialMovesOf(piece, sq); - if ( - piece == "p" && - this.hasEnpassant && - this.epSquare - ) { - Array.prototype.push.apply(moves, this.getEnpassantCaptures(sq)); - } + const piece = this.getPieceType(x, y); + let moves = this.getPotentialMovesOf(piece, [x, y]); + if (piece == "p" && this.hasEnpassant && this.epSquare) + Array.prototype.push.apply(moves, this.getEnpassantCaptures([x, y])); if ( - piece == "k" && - this.hasCastle && + piece == "k" && this.hasCastle && this.castleFlags[color || this.turn].some(v => v < this.size.y) ) { - Array.prototype.push.apply(moves, this.getCastleMoves(sq)); + Array.prototype.push.apply(moves, this.getCastleMoves([x, y])); } return this.postProcessPotentialMoves(moves); } @@ -1338,7 +1437,7 @@ export default class ChessRules { moves = this.capturePostProcess(moves, oppCol); if (this.options["atomic"]) - this.atomicPostProcess(moves, oppCol); + this.atomicPostProcess(moves, color, oppCol); if ( moves.length > 0 && @@ -1369,7 +1468,7 @@ export default class ChessRules { }); } - atomicPostProcess(moves, oppCol) { + atomicPostProcess(moves, color, oppCol) { moves.forEach(m => { if ( this.board[m.end.x][m.end.y] != "" && @@ -1386,15 +1485,22 @@ export default class ChessRules { [1, 0], [1, 1] ]; + let mNext = new Move({ + start: m.end, + end: m.end, + appear: [], + vanish: [] + }); for (let step of steps) { let x = m.end.x + step[0]; let y = this.getY(m.end.y + step[1]); if ( this.onBoard(x, y) && this.board[x][y] != "" && + (x != m.start.x || y != m.start.y) && this.getPieceType(x, y) != "p" ) { - m.vanish.push( + mNext.vanish.push( new PiPo({ p: this.getPiece(x, y), c: this.getColor(x, y), @@ -1404,8 +1510,18 @@ export default class ChessRules { ); } } - if (!this.options["rifle"]) - m.appear.pop(); //nothing appears + if (!this.options["rifle"]) { + // The moving piece also vanish + mNext.vanish.unshift( + new PiPo({ + x: m.end.x, + y: m.end.y, + c: color, + p: this.getPiece(m.start.x, m.start.y) + }) + ); + } + m.next = mNext; } }); } @@ -1475,121 +1591,124 @@ export default class ChessRules { Array.prototype.push.apply(moves, newMoves); } - // NOTE: using special symbols to not interfere with variants' pieces codes - static get CannibalKings() { - return { - "!": "p", - "#": "r", - "$": "n", - "%": "b", - "*": "q", - "k": "k" - }; - } - - static get CannibalKingCode() { - return { - "p": "!", - "r": "#", - "n": "$", - "b": "%", - "q": "*", - "k": "k" - }; - } - - isKing(symbol) { - return !!C.CannibalKings[symbol]; - } - - // For Madrasi: - // (redefined in Baroque etc, where Madrasi condition doesn't make sense) - isImmobilized([x, y]) { - if (!this.options["madrasi"]) - return false; + // Generic method to find possible moves of "sliding or jumping" pieces + getPotentialMovesOf(piece, [x, y], color) { const color = this.getColor(x, y); - const oppCol = C.GetOppCol(color); - const piece = this.getPieceType(x, y); //ok not cannibal king - const allSpecs = this.pieces(color, x, y); - let stepSpec = allSpecs[piece]; - if (stepSpec.moveas) - stepSpec = allSpecs[stepSpec.moveas]; - const attacks = stepSpec.attack || stepSpec.moves; - for (let a of attacks) { - outerLoop: for (let step of a.steps) { - let [i, j] = [x + step[0], y + step[1]]; - let stepCounter = 1; - while (this.onBoard(i, j) && this.board[i][j] == "") { - if (a.range <= stepCounter++) - continue outerLoop; - i += step[0]; - j = this.getY(j + step[1]); - } - if ( - this.onBoard(i, j) && - this.getColor(i, j) == oppCol && - this.getPieceType(i, j) == piece - ) { - return true; + const specialAttack = !!this.getStepSpec(color, x, y).attack; + let squares = []; + if (specialAttack) { + squares = this.findDestSquares( + [x, y], + { + attackOnly: true, + segments: this.options["cylinder"] + }, + ([i, j]) => { + return ( + (!this.options["zen"] || this.getPieceType(i, j) == 'k') && + this.canTake([x, y], [i, j]) + ); } + ); + } + const noSpecials = this.findDestSquares( + [x, y], + { + moveOnly: specialAttack || this.options["zen"], + segments: this.options["cylinder"] + }, + ([i, j]) => this.board[i][j] == "" || this.canTake([x, y], [i, j]) + ); + Array.prototype.push.apply(squares, noSpecials); + if (this.options["zen"]) { + let zenCaptures = this.findCapturesOn( + [x, y], + {}, + ([i, j]) => this.getPieceType(i, j) != 'k' + ); + // Technical step: segments (if any) are reversed + if (this.options["cylinder"]) { + zenCaptures.forEach(z => { + z.segments = z.segments.reverse().map(s => s.reverse()) + }); } + Array.prototype.push.apply(squares, zenCaptures); } - return false; - } - - canStepOver(i, j, p) { - // In some variants, objects on boards don't stop movement (Chakart) - return this.board[i][j] == ""; + if ( + this.options["recycle"] || + (this.options["teleport"] && this.subTurnTeleport == 1) + ) { + const selfCaptures = this.findDestSquares( + [x, y], + { + attackOnly: true, + segments: this.options["cylinder"] + }, + ([i, j]) => + this.getColor(i, j) == color && this.getPieceType(i, j) != 'k' + ); + Array.prototype.push.apply(squares, selfCaptures); + } + return squares.map(s => { + let mv = this.getBasicMove([x, y], s.sq); + if (this.options["cylinder"] && s.segments.length >= 2) + mv.segments = s.segments; + return mv; + }); } - // Generic method to find possible moves of "sliding or jumping" pieces - getPotentialMovesOf(piece, [x, y]) { - const color = this.getColor(x, y); + findDestSquares([x, y], o, allowed) { + if (!allowed) + allowed = () => true; const apparentPiece = this.getPiece(x, y); //how it looks - const allSpecs = this.pieces(color, x, y); - let stepSpec = allSpecs[piece]; - if (stepSpec.moveas) - stepSpec = allSpecs[stepSpec.moveas]; - let moves = []; - // Next 3 for Cylinder mode: + let res = []; + // Next 3 for Cylinder mode: (unused if !o.segments) let explored = {}; let segments = []; let segStart = []; - - const addMove = (start, end) => { - let newMove = this.getBasicMove(start, end); - if (segments.length > 0) { - newMove.segments = JSON.parse(JSON.stringify(segments)); - newMove.segments.push([[segStart[0], segStart[1]], [end[0], end[1]]]); - } - moves.push(newMove); + const addSquare = ([i, j]) => { + let elt = {sq: [i, j]}; + if (o.segments) + elt.segments = this.getSegments(segments, segStart, end); + res.push(elt); }; - - const findAddMoves = (type, stepArray) => { + const exploreSteps = (stepArray) => { for (let s of stepArray) { outerLoop: for (let step of s.steps) { - segments = []; - segStart = [x, y]; + if (o.segments) { + segments = []; + segStart = [x, y]; + } let [i, j] = [x, y]; let stepCounter = 0; while ( this.onBoard(i, j) && ((i == x && j == y) || this.canStepOver(i, j, apparentPiece)) ) { - if ( - type != "attack" && - !explored[i + "." + j] && - (i != x || j != y) - ) { + if (!explored[i + "." + j] && (i != x || j != y)) + { explored[i + "." + j] = true; - addMove([x, y], [i, j]); + if ( + allowed([i, j]) && + ( + !o.captureTarget || + (o.captureTarget[0] == i && o.captureTarget[1] == j) + ) + ) { + if (o.one && !o.attackOnly) + return true; + if (!o.attackOnly) + addSquare(!o.captureTarget ? [i, j] : [x, y]); + if (o.captureTarget) + return res[0]; + } } if (s.range <= stepCounter++) continue outerLoop; const oldIJ = [i, j]; i += step[0]; j = this.getY(j + step[1]); - if (Math.abs(j - oldIJ[1]) > 1) { + if (o.segments && Math.abs(j - oldIJ[1]) > 1) { // Boundary between segments (cylinder mode) segments.push([[segStart[0], segStart[1]], oldIJ]); segStart = [i, j]; @@ -1598,112 +1717,84 @@ export default class ChessRules { if (!this.onBoard(i, j)) continue; const pieceIJ = this.getPieceType(i, j); - if ( - type != "moveonly" && - !explored[i + "." + j] && - ( - !this.options["zen"] || - pieceIJ == "k" - ) && - ( - this.canTake([x, y], [i, j]) || - ( - (this.options["recycle"] || this.options["teleport"]) && - pieceIJ != "k" - ) - ) - ) { + if (!explored[i + "." + j]) { explored[i + "." + j] = true; - addMove([x, y], [i, j]); + if (allowed([i, j])) { + if (o.one && !o.moveOnly) + return true; + if (!o.moveOnly) + addSquare(!o.captureTarget ? [i, j] : [x, y]); + if ( + o.captureTarget && + o.captureTarget[0] == i && o.captureTarget[1] == j + ) { + return res[0]; + } + } } } } }; - - const specialAttack = !!stepSpec.attack; - if (specialAttack) - findAddMoves("attack", stepSpec.attack); - findAddMoves(specialAttack ? "moveonly" : "all", stepSpec.moves); - if (this.options["zen"]) { - Array.prototype.push.apply(moves, - this.findCapturesOn([x, y], {zen: true})); + if (o.captureTarget) + exploreSteps(o.captureSteps) + else { + const stepSpec = this.getStepSpec(this.getColor(x, y), x, y); + if (!o.attackOnly || !stepSpec.attack) + exploreSteps(stepSpec.moves); + if (!o.moveOnly && !!stepSpec.attack) + exploreSteps(stepSpec.attack); } - return moves; + return o.captureTarget ? null : (o.one ? false : res); } // Search for enemy (or not) pieces attacking [x, y] - findCapturesOn([x, y], args) { - let moves = []; - if (!args.oppCol) - args.oppCol = C.GetOppCol(this.getColor(x, y) || this.turn); + findCapturesOn([x, y], o, allowed) { + if (!allowed) + allowed = () => true; + if (!o.byCol) + o.byCol = [C.GetOppCol(this.getColor(x, y) || this.turn)]; + let res = []; for (let i=0; i this.canTake([ii, jj], [x, y]) + ); + if (newSquare) { + if (o.one) + return true; + res.push(newSquare); } } } } } } - return moves; - } - - static CompatibleStep([x1, y1], [x2, y2], step, range) { - const rx = (x2 - x1) / step[0], - ry = (y2 - y1) / step[1]; - if ( - (!Number.isFinite(rx) && !Number.isNaN(rx)) || - (!Number.isFinite(ry) && !Number.isNaN(ry)) - ) { - return false; - } - let distance = (Number.isNaN(rx) ? ry : rx); - // TODO: 1e-7 here is totally arbitrary - if (Math.abs(distance - Math.round(distance)) > 1e-7) - return false; - distance = Math.round(distance); //in case of (numerical...) - if (range < distance) - return false; - return true; + return (one ? false : res); } // Build a regular move from its initial and destination squares. @@ -1752,7 +1843,7 @@ export default class ChessRules { if (this.options["cannibal"] && destColor != initColor) { const lastIdx = mv.vanish.length - 1; let trPiece = mv.vanish[lastIdx].p; - if (this.isKing(this.getPiece(sx, sy))) + if (this.getPieceType(sx, sy) == 'k') trPiece = C.CannibalKingCode[trPiece]; if (mv.appear.length >= 1) mv.appear[0].p = trPiece; @@ -1797,12 +1888,12 @@ export default class ChessRules { // Next conditions for variants like Atomic or Rifle, Recycle... ( move.appear.length > 0 && - this.getPieceType(0, 0, move.appear[0].p) == "p" + this.getPieceType(0, 0, move.appear[0].p) == 'p' ) && ( move.vanish.length > 0 && - this.getPieceType(0, 0, move.vanish[0].p) == "p" + this.getPieceType(0, 0, move.vanish[0].p) == 'p' ) ) { return { @@ -1818,25 +1909,26 @@ export default class ChessRules { const color = this.getColor(x, y); const shiftX = (color == 'w' ? -1 : 1); const oppCol = C.GetOppCol(color); - let enpassantMove = null; if ( - !!this.epSquare && + this.epSquare && this.epSquare.x == x + shiftX && Math.abs(this.getY(this.epSquare.y - y)) == 1 && - this.getColor(x, this.epSquare.y) == oppCol //Doublemove guard... + // Doublemove (and Progressive?) guards: + this.board[this.epSquare.x][this.epSquare.y] == "" && + this.getColor(x, this.epSquare.y) == oppCol ) { const [epx, epy] = [this.epSquare.x, this.epSquare.y]; - this.board[epx][epy] = oppCol + "p"; - enpassantMove = this.getBasicMove([x, y], [epx, epy]); + this.board[epx][epy] = oppCol + 'p'; + let enpassantMove = this.getBasicMove([x, y], [epx, epy]); this.board[epx][epy] = ""; const lastIdx = enpassantMove.vanish.length - 1; //think Rifle enpassantMove.vanish[lastIdx].x = x; + return [enpassantMove]; } - return !!enpassantMove ? [enpassantMove] : []; + return []; } - // "castleInCheck" arg to let some variants castle under check - getCastleMoves([x, y], finalSquares, castleInCheck, castleWith) { + getCastleMoves([x, y], finalSquares, castleWith) { const c = this.getColor(x, y); // Castling ? @@ -1861,7 +1953,7 @@ export default class ChessRules { if ( this.board[x][rookPos] == "" || this.getColor(x, rookPos) != c || - (!!castleWith && !castleWith.includes(castlingPiece)) + (castleWith && !castleWith.includes(castlingPiece)) ) { // Rook is not here, or changed color (see Benedict) continue; @@ -1872,7 +1964,13 @@ export default class ChessRules { let i = y; do { if ( - (!castleInCheck && this.underCheck([x, i], oppCol)) || + // NOTE: next weird test because underCheck() verification + // will be executed in filterValid() later. + ( + i != finalSquares[castleSide][0] && + this.underCheck([x, i], oppCol) + ) + || ( this.board[x][i] != "" && // NOTE: next check is enough, because of chessboard constraints @@ -1904,7 +2002,7 @@ export default class ChessRules { } } - // If this code is reached, castle is valid + // If this code is reached, castle is potentially valid moves.push( new Move({ appear: [ @@ -1940,50 +2038,48 @@ export default class ChessRules { //////////////////// // MOVES VALIDATION - // Is (king at) given position under check by "oppCol" ? + // Is piece (or square) at given position attacked by "oppCol" ? + underAttack([x, y], oppCol) { + const king = this.getPieceType(x, y) == 'k'; + return ( + ( + (!this.options["zen"] || king) && + this.findCapturesOn([x, y], + {byCol: [oppCol], segments: this.options["cylinder"], one: true}, + ([i, j]) => this.canTake([i, j], [x, y])) + ) + || + ( + (this.options["zen"] && !king) && + this.findDestSquares([x, y], + {attackOnly: true, segments: this.options["cylinder"], one: true}, + ([i, j]) => this.canTake([i, j], [x, y])) + ) + ); + } + underCheck([x, y], oppCol) { if (this.options["taking"] || this.options["dark"]) return false; - return ( - this.findCapturesOn([x, y], {oppCol: oppCol, one: true}).length >= 1 - ); + return this.underAttack([x, y], oppCol); } // Stop at first king found (TODO: multi-kings) searchKingPos(color) { for (let i=0; i < this.size.x; i++) { for (let j=0; j < this.size.y; j++) { - if (this.getColor(i, j) == color && this.isKing(this.getPiece(i, j))) + if (this.getColor(i, j) == color && this.getPieceType(i, j) == 'k') return [i, j]; } } return [-1, -1]; //king not found } - // Some variants (e.g. Refusal) may need to check opponent moves too + // 'color' arg because some variants (e.g. Refusal) check opponent moves filterValid(moves, color) { - if (moves.length == 0) - return []; if (!color) color = this.turn; const oppCol = C.GetOppCol(color); - if (this.options["balance"] && [1, 3].includes(this.movesCount)) { - // Forbid moves either giving check or exploding opponent's king: - const oppKingPos = this.searchKingPos(oppCol); - moves = moves.filter(m => { - if ( - m.vanish.some(v => v.c == oppCol && v.p == "k") && - m.appear.every(a => a.c != oppCol || a.p != "k") - ) - return false; - this.playOnBoard(m); - const res = !this.underCheck(oppKingPos, color); - this.undoOnBoard(m); - return res; - }); - } - if (this.options["taking"] || this.options["dark"]) - return moves; const kingPos = this.searchKingPos(color); let filtered = {}; //avoid re-checking similar moves (promotions...) return moves.filter(m => { @@ -1993,12 +2089,12 @@ export default class ChessRules { let square = kingPos, res = true; //a priori valid if (m.vanish.some(v => { - return this.isKing(v.p) && v.c == color; + return this.getPieceType(0, 0, v.p) == 'k' && v.c == color; })) { // Search king in appear array: const newKingIdx = m.appear.findIndex(a => { - return this.isKing(a.p) && a.c == color; + return this.getPieceType(0, 0, a.p) == 'k' && a.c == color; }); if (newKingIdx >= 0) square = [m.appear[newKingIdx].x, m.appear[newKingIdx].y]; @@ -2017,16 +2113,6 @@ export default class ChessRules { ///////////////// // MOVES PLAYING - // Aggregate flags into one object - aggregateFlags() { - return this.castleFlags; - } - - // Reverse operation - disaggregateFlags(flags) { - this.castleFlags = flags; - } - // Apply a move on board playOnBoard(move) { for (let psq of move.vanish) @@ -2069,8 +2155,8 @@ export default class ChessRules { if ( this.hasCastle && // If flags already off, no need to re-check: - Object.keys(this.castleFlags).some(c => { - return this.castleFlags[c].some(val => val < this.size.y)}) + Object.values(this.castleFlags).some(cvals => + cvals.some(val => val < this.size.y)) ) { this.updateCastleFlags(move); } @@ -2086,7 +2172,7 @@ export default class ChessRules { const destSquare = C.CoordsToSquare(move.end); if ( this.ispawn[initSquare] || - (move.vanish[0].p == "p" && move.appear[0].p != "p") + (move.vanish[0].p == 'p' && move.appear[0].p != 'p') ) { this.ispawn[destSquare] = true; } @@ -2094,7 +2180,7 @@ export default class ChessRules { this.ispawn[destSquare] && this.getColor(move.end.x, move.end.y) != move.vanish[0].c ) { - move.vanish[1].p = "p"; + move.vanish[1].p = 'p'; delete this.ispawn[destSquare]; } } @@ -2136,14 +2222,13 @@ export default class ChessRules { postPlay(move) { const color = this.turn; - const oppCol = C.GetOppCol(color); if (this.options["dark"]) this.updateEnlightened(); if (this.options["teleport"]) { if ( this.subTurnTeleport == 1 && move.vanish.length > move.appear.length && - move.vanish[move.vanish.length - 1].c == color + move.vanish[1].c == color ) { const v = move.vanish[move.vanish.length - 1]; this.captured = {x: v.x, y: v.y, c: v.c, p: v.p}; @@ -2153,37 +2238,39 @@ export default class ChessRules { this.subTurnTeleport = 1; this.captured = null; } - if ( - ( - this.options["doublemove"] && - this.movesCount >= 1 && - this.subTurn == 1 - ) || - (this.options["progressive"] && this.subTurn <= this.movesCount) - ) { - const oppKingPos = this.searchKingPos(oppCol); - if ( - oppKingPos[0] >= 0 && - ( - this.options["taking"] || - !this.underCheck(oppKingPos, color) - ) - ) { - this.subTurn++; - return; - } - } if (this.isLastMove(move)) { this.turn = oppCol; this.movesCount++; this.subTurn = 1; } + else if (!move.next) + this.subTurn++; } isLastMove(move) { + if (move.next) + return false; + const color = this.turn; + const oppCol = C.GetOppCol(color); + const oppKingPos = this.searchKingPos(oppCol); + if (oppKingPos[0] < 0 || this.underCheck(oppKingPos, color)) + return true; return ( - (this.options["balance"] && ![1, 3].includes(this.movesCount)) || - !move.next + ( + !this.options["balance"] || + ![1, 3].includes(this.movesCount) + ) + && + ( + !this.options["doublemove"] || + this.movesCount == 0 || + this.subTurn == 2 + ) + && + ( + !this.options["progressive"] || + this.subTurn == this.movesCount + 1 + ) ); } @@ -2260,34 +2347,24 @@ export default class ChessRules { this.graphUpdateEnlightened(); } - playPlusVisual(move, r) { - if (this.hasMoveStack) - this.buildMoveStack(move); - else { - this.play(move); - this.playVisual(move, r); - this.afterPlay(move, this.turn, {send: true, res: true}); //user method - } - } - // TODO: send stack receive stack, or allow incremental? (good/bad points) - buildMoveStack(move) { + buildMoveStack(move, r) { this.moveStack.push(move); this.computeNextMove(move); this.play(move); const newTurn = this.turn; - if (this.moveStack.length == 1) { - this.playVisual(move); + if (this.moveStack.length == 1) + this.playVisual(move, r); + if (move.next) { this.gameState = { fen: this.getFen(), board: JSON.parse(JSON.stringify(this.board)) //easier }; + this.buildMoveStack(move.next, r); } - if (move.next) - this.buildMoveStack(move.next); else { - // Send, animate + play until here if (this.moveStack.length == 1) { + // Usual case (one normal move) this.afterPlay(this.moveStack, newTurn, {send: true, res: true}); this.moveStack = [] } @@ -2305,15 +2382,6 @@ export default class ChessRules { // Implemented in variants using (automatic) moveStack computeNextMove(move) {} - getMaxDistance(r) { - // Works for all rectangular boards: - return Math.sqrt(r.width ** 2 + r.height ** 2); - } - - getDomPiece(x, y) { - return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y]; - } - animateMoving(start, end, drag, segments, cb) { let initPiece = this.getDomPiece(start.x, start.y); // NOTE: cloning often not required, but light enough, and simpler @@ -2400,7 +2468,8 @@ export default class ChessRules { } if (move.vanish.length > move.appear.length) { const arr = move.vanish.slice(move.appear.length) - .filter(v => v.x != move.end.x || v.y != move.end.y); + // Ignore disappearing pieces hidden by some appearing ones: + .filter(v => move.appear.every(a => a.x != v.x || a.y != v.y)); if (arr.length > 0) { targetObj.target++; this.animateFading(arr, () => targetObj.increment());