X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FEightpieces.js;h=88c3739ae0791e05f73818421940a153ed1d06b3;hb=3a2a7b5fd3c6bfd0752838094c27e1fb6172d109;hp=f040a8389bf6e9e4958693a6c59fb4c32520bd37;hpb=93e8684c9c995ac6bd53e97289dd294a3bcefc14;p=vchess.git diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js index f040a838..88c3739a 100644 --- a/client/src/variants/Eightpieces.js +++ b/client/src/variants/Eightpieces.js @@ -1,5 +1,5 @@ import { ArrayFun } from "@/utils/array"; -import { randInt, shuffle } from "@/utils/alea"; +import { randInt } from "@/utils/alea"; import { ChessRules, PiPo, Move } from "@/base_rules"; export const VariantRules = class EightpiecesRules extends ChessRules { @@ -13,10 +13,6 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return "l"; } - static get PIECES() { - return ChessRules.PIECES.concat([V.JAILER, V.SENTRY, V.LANCER]); - } - // Lancer directions *from white perspective* static get LANCER_DIRS() { return { @@ -31,6 +27,12 @@ export const VariantRules = class EightpiecesRules extends ChessRules { }; } + static get PIECES() { + return ChessRules.PIECES + .concat([V.JAILER, V.SENTRY]) + .concat(Object.keys(V.LANCER_DIRS)); + } + getPiece(i, j) { const piece = this.board[i][j].charAt(1); // Special lancer case: 8 possible orientations @@ -38,17 +40,66 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return piece; } - getPpath(b) { - if ([V.JAILER, V.SENTRY].concat(Object.keys(V.LANCER_DIRS)).includes(b[1])) - return "Eightpieces/" + b; + getPpath(b, color, score, orientation) { + if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/" + b; + if (Object.keys(V.LANCER_DIRS).includes(b[1])) { + if (orientation == 'w') return "Eightpieces/" + b; + // Find opposite direction for adequate display: + let oppDir = ''; + switch (b[1]) { + case 'c': + oppDir = 'g'; + break; + case 'g': + oppDir = 'c'; + break; + case 'd': + oppDir = 'h'; + break; + case 'h': + oppDir = 'd'; + break; + case 'e': + oppDir = 'm'; + break; + case 'm': + oppDir = 'e'; + break; + case 'f': + oppDir = 'o'; + break; + case 'o': + oppDir = 'f'; + break; + } + return "Eightpieces/" + b[0] + oppDir; + } return b; } + getPPpath(b, orientation) { + return this.getPpath(b, null, null, orientation); + } + static ParseFen(fen) { const fenParts = fen.split(" "); - return Object.assign(ChessRules.ParseFen(fen), { - sentrypush: fenParts[5] - }); + return Object.assign( + ChessRules.ParseFen(fen), + { sentrypush: fenParts[5] } + ); + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 5) Check sentry push (if any) + if ( + fenParsed.sentrypush != "-" && + !fenParsed.sentrypush.match(/^([a-h][1-8],?)+$/) + ) { + return false; + } + return true; } getFen() { @@ -72,8 +123,8 @@ export const VariantRules = class EightpiecesRules extends ChessRules { super.setOtherVariables(fen); // subTurn == 2 only when a sentry moved, and is about to push something this.subTurn = 1; - // Pushing sentry position, updated after each push (subTurn == 1) - this.sentryPos = { x: -1, y: -1 }; + // Sentry position just after a "capture" (subTurn from 1 to 2) + this.sentryPos = null; // Stack pieces' forbidden squares after a sentry move at each turn const parsedFen = V.ParseFen(fen); if (parsedFen.sentrypush == "-") this.sentryPush = [null]; @@ -89,9 +140,10 @@ export const VariantRules = class EightpiecesRules extends ChessRules { static GenRandInitFen(randomness) { if (randomness == 0) // Deterministic: - return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -"; + return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 ahah - -"; let pieces = { w: new Array(8), b: new Array(8) }; + let flags = ""; // Shuffle pieces on first (and last rank if randomness == 2) for (let c of ["w", "b"]) { if (c == 'b' && randomness == 1) { @@ -102,6 +154,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { pieces['w'].slice(0, lancerIdx) .concat(['g']) .concat(pieces['w'].slice(lancerIdx + 1)); + flags += flags; break; } @@ -140,6 +193,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { let rookPos = positions[0]; let jailerPos = positions[2]; const kingPos = positions[1]; + flags += V.CoordToColumn(rookPos) + V.CoordToColumn(jailerPos); if (Math.random() < 0.5) [rookPos, jailerPos] = [jailerPos, rookPos]; pieces[c][rookPos] = "r"; @@ -156,56 +210,10 @@ export const VariantRules = class EightpiecesRules extends ChessRules { pieces["b"].join("") + "/pppppppp/8/8/8/8/PPPPPPPP/" + pieces["w"].join("").toUpperCase() + - " w 0 1111 - -" + " w 0 " + flags + " - -" ); } - // Scan kings, rooks and jailers - scanKingsRooks(fen) { - this.INIT_COL_KING = { w: -1, b: -1 }; - this.INIT_COL_ROOK = { w: -1, b: -1 }; - this.INIT_COL_JAILER = { w: -1, b: -1 }; - this.kingPos = { w: [-1, -1], b: [-1, -1] }; - const fenRows = V.ParseFen(fen).position.split("/"); - const startRow = { 'w': V.size.x - 1, 'b': 0 }; - for (let i = 0; i < fenRows.length; i++) { - let k = 0; - for (let j = 0; j < fenRows[i].length; j++) { - switch (fenRows[i].charAt(j)) { - case "k": - this.kingPos["b"] = [i, k]; - this.INIT_COL_KING["b"] = k; - break; - case "K": - this.kingPos["w"] = [i, k]; - this.INIT_COL_KING["w"] = k; - break; - case "r": - if (i == startRow['b'] && this.INIT_COL_ROOK["b"] < 0) - this.INIT_COL_ROOK["b"] = k; - break; - case "R": - if (i == startRow['w'] && this.INIT_COL_ROOK["w"] < 0) - this.INIT_COL_ROOK["w"] = k; - break; - case "j": - if (i == startRow['b'] && this.INIT_COL_JAILER["b"] < 0) - this.INIT_COL_JAILER["b"] = k; - break; - case "J": - if (i == startRow['w'] && this.INIT_COL_JAILER["w"] < 0) - this.INIT_COL_JAILER["w"] = k; - break; - default: { - const num = parseInt(fenRows[i].charAt(j)); - if (!isNaN(num)) k += num - 1; - } - } - k++; - } - } - } - // Is piece on square (x,y) immobilized? isImmobilized([x, y]) { const color = this.getColor(x, y); @@ -232,7 +240,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { x: ex, y: ey, c: tr ? tr.c : this.getColor(sx, sy), - p: tr ? tr.p : this.board[sx][sy][1] + p: tr ? tr.p : this.board[sx][sy].charAt(1) }) ], vanish: [ @@ -240,7 +248,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { x: sx, y: sy, c: this.getColor(sx, sy), - p: this.board[sx][sy][1] + p: this.board[sx][sy].charAt(1) }) ] }); @@ -252,7 +260,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { x: ex, y: ey, c: this.getColor(ex, ey), - p: this.board[ex][ey][1] + p: this.board[ex][ey].charAt(1) }) ); } @@ -267,16 +275,33 @@ export const VariantRules = class EightpiecesRules extends ChessRules { ); } - getPotentialMovesFrom([x,y]) { + getPotentialMovesFrom([x, y]) { // At subTurn == 2, jailers aren't effective (Jeff K) - if (this.subTurn == 1 && !!this.isImmobilized([x, y])) return []; + if (this.subTurn == 1) { + const jsq = this.isImmobilized([x, y]); + if (!!jsq) { + let moves = []; + // Special pass move if king: + if (this.getPiece(x, y) == V.KING) { + moves.push( + new Move({ + appear: [], + vanish: [], + start: { x: x, y: y }, + end: { x: jsq[0], y: jsq[1] } + }) + ); + } + return moves; + } + } if (this.subTurn == 2) { // Temporarily change pushed piece color. // (Not using getPiece() because of lancers) var oppCol = this.getColor(x, y); var color = V.GetOppCol(oppCol); var saveXYstate = this.board[x][y]; - this.board[x][y] = color + this.board[x][y][1]; + this.board[x][y] = color + this.board[x][y].charAt(1); } let moves = []; switch (this.getPiece(x, y)) { @@ -306,14 +331,15 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return true; }); } - if (this.subTurn == 2) { + else if (this.subTurn == 2) { // Don't forget to re-add the sentry on the board: // Also fix color of pushed piece afterward: moves.forEach(m => { - m.appear.push({x: x, y: y, p: V.SENTRY, c: color}); - m.appear[0].c = oppCol; + m.appear.unshift({x: x, y: y, p: V.SENTRY, c: color}); + m.appear[1].c = oppCol; m.vanish[0].c = oppCol; }); + this.board[x][y] = saveXYstate; } return moves; } @@ -327,7 +353,8 @@ export const VariantRules = class EightpiecesRules extends ChessRules { const lastRank = color == "w" ? 0 : sizeX - 1; const finalPieces = - x + shiftX == lastRank + // No promotions after pushes! + x + shiftX == lastRank && this.subTurn == 1 ? Object.keys(V.LANCER_DIRS).concat( [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]) @@ -371,7 +398,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { // En passant: const Lep = this.epSquares.length; - const epSquare = this.epSquares[Lep - 1]; //always at least one element + const epSquare = this.epSquares[Lep - 1]; if ( !!epSquare && epSquare.x == x + shiftX && @@ -390,22 +417,21 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return moves; } - // Obtain all lancer moves in "step" direction, - // without final re-orientation. - getPotentialLancerMoves_aux([x, y], step) { + // Obtain all lancer moves in "step" direction + getPotentialLancerMoves_aux([x, y], step, tr) { let moves = []; // Add all moves to vacant squares until opponent is met: const oppCol = V.GetOppCol(this.getColor(x, y)); let sq = [x + step[0], y + step[1]]; while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) { if (this.board[sq[0]][sq[1]] == V.EMPTY) - moves.push(this.getBasicMove([x, y], sq)); + moves.push(this.getBasicMove([x, y], sq, tr)); sq[0] += step[0]; sq[1] += step[1]; } if (V.OnBoard(sq[0], sq[1])) // Add capturing move - moves.push(this.getBasicMove([x, y], sq)); + moves.push(this.getBasicMove([x, y], sq, tr)); return moves; } @@ -422,12 +448,35 @@ export const VariantRules = class EightpiecesRules extends ChessRules { this.sentryPush[L-1][pl-1].y == y ) { // I was pushed: allow all directions (for this move only), but - // do not change direction after moving. + // do not change direction after moving, *except* if I keep the + // same orientation in which I was pushed. + const color = this.getColor(x, y); + const curDir = V.LANCER_DIRS[this.board[x][x].charAt(1)]; Object.values(V.LANCER_DIRS).forEach(step => { - Array.prototype.push.apply( - moves, - this.getPotentialLancerMoves_aux([x, y], step) - ); + const dirCode = Object.keys(V.LANCER_DIRS).find(k => { + return ( + V.LANCER_DIRS[k][0] == step[0] && + V.LANCER_DIRS[k][1] == step[1] + ); + }); + const dirMoves = + this.getPotentialLancerMoves_aux( + [x, y], + step, + { p: dirCode, c: color } + ); + if (curDir[0] == step[0] && curDir[1] == step[1]) { + // Keeping same orientation: can choose after + let chooseMoves = []; + dirMoves.forEach(m => { + Object.keys(V.LANCER_DIRS).forEach(k => { + let mk = JSON.parse(JSON.stringify(m)); + mk.appear[0].p = k; + moves.push(mk); + }); + }); + Array.prototype.push.apply(moves, chooseMoves); + } else Array.prototype.push.apply(moves, dirMoves); }); return moves; } @@ -446,7 +495,24 @@ export const VariantRules = class EightpiecesRules extends ChessRules { }); }); return moves; - } else return monodirMoves; + } else { + // I'm pushed: add potential nudges + let potentialNudges = []; + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + if ( + V.OnBoard(x + step[0], y + step[1]) && + this.board[x + step[0]][y + step[1]] == V.EMPTY + ) { + potentialNudges.push( + this.getBasicMove( + [x, y], + [x + step[0], y + step[1]] + ) + ); + } + } + return monodirMoves.concat(potentialNudges); + } } getPotentialSentryMoves([x, y]) { @@ -454,6 +520,8 @@ export const VariantRules = class EightpiecesRules extends ChessRules { let moves = super.getPotentialBishopMoves([x, y]); // ...but captures are replaced by special move, if and only if // "captured" piece can move now, considered as the capturer unit. + // --> except is subTurn == 2, in this case I don't push anything. + if (this.subTurn == 2) return moves.filter(m => m.vanish.length == 1); moves.forEach(m => { if (m.vanish.length == 2) { // Temporarily cancel the sentry capture: @@ -468,8 +536,22 @@ export const VariantRules = class EightpiecesRules extends ChessRules { if (m.appear.length == 0) { let res = false; this.play(m); - let moves2 = this.filterValid( - this.getPotentialMovesFrom([m.end.x, m.end.y])); + let potentialMoves = this.getPotentialMovesFrom([m.end.x, m.end.y]); + // Add nudges (if any a priori possible) + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + if ( + V.OnBoard(m.end.x + step[0], m.end.y + step[1]) && + this.board[m.end.x + step[0]][m.end.y + step[1]] == V.EMPTY + ) { + potentialMoves.push( + this.getBasicMove( + [m.end.x, m.end.y], + [m.end.x + step[0], m.end.y + step[1]] + ) + ); + } + } + let moves2 = this.filterValid(potentialMoves); for (let m2 of moves2) { this.play(m2); res = !this.underCheck(color); @@ -491,23 +573,6 @@ export const VariantRules = class EightpiecesRules extends ChessRules { }); } - getPotentialKingMoves([x, y]) { - let moves = super.getPotentialKingMoves([x, y]); - // Augment with pass move is the king is immobilized: - const jsq = this.isImmobilized([x, y]); - if (!!jsq) { - moves.push( - new Move({ - appear: [], - vanish: [], - start: { x: x, y: y }, - end: { x: jsq[0], y: jsq[1] } - }) - ); - } - return moves; - } - // Adapted: castle with jailer possible getCastleMoves([x, y]) { const c = this.getColor(x, y); @@ -528,9 +593,8 @@ export const VariantRules = class EightpiecesRules extends ChessRules { castleSide < 2; castleSide++ ) { - if (!this.castleFlags[c][castleSide]) continue; + if (this.castleFlags[c][castleSide] >= 8) continue; // Rook (or jailer) and king are on initial position - const finDist = finalSquares[castleSide][0] - y; let step = finDist / Math.max(1, Math.abs(finDist)); i = y; @@ -539,18 +603,14 @@ export const VariantRules = class EightpiecesRules extends ChessRules { this.isAttacked([x, i], [oppCol]) || (this.board[x][i] != V.EMPTY && (this.getColor(x, i) != c || - ![V.KING, V.ROOK].includes(this.getPiece(x, i)))) + ![V.KING, V.ROOK, V.JAILER].includes(this.getPiece(x, i)))) ) { continue castlingCheck; } i += step; } while (i != finalSquares[castleSide][0]); - step = castleSide == 0 ? -1 : 1; - const rookOrJailerPos = - castleSide == 0 - ? Math.min(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]) - : Math.max(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]); + const rookOrJailerPos = this.castleFlags[c][castleSide]; for (i = y + step; i != rookOrJailerPos; i += step) if (this.board[x][i] != V.EMPTY) continue castlingCheck; @@ -588,6 +648,12 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return moves; } + atLeastOneMove() { + // If in second-half of a move, we already know that a move is possible + if (this.subTurn == 2) return true; + return super.atLeastOneMove(); + } + filterValid(moves) { // Disable check tests for sentry pushes, // because in this case the move isn't finished @@ -597,11 +663,14 @@ export const VariantRules = class EightpiecesRules extends ChessRules { if (m.appear.length > 0) movesWithoutSentryPushes.push(m); else movesWithSentryPushes.push(m); }); - const filteredMoves = super.filterValid(movesWithoutSentryPushes) + + // TODO: if after move a sentry can take king in 2 times?! + + const filteredMoves = super.filterValid(movesWithoutSentryPushes); // If at least one full move made, everything is allowed: if (this.movesCount >= 2) return filteredMoves.concat(movesWithSentryPushes); - // Else, forbid check and captures: + // Else, forbid checks and captures: const oppCol = V.GetOppCol(this.turn); return filteredMoves.filter(m => { if (m.vanish.length == 2 && m.appear.length == 1) return false; @@ -615,127 +684,221 @@ export const VariantRules = class EightpiecesRules extends ChessRules { getAllValidMoves() { if (this.subTurn == 1) return super.getAllValidMoves(); // Sentry push: - const sentrySq = [this.sentryPos.x, this.SentryPos.y]; + const sentrySq = [this.sentryPos.x, this.sentryPos.y]; return this.filterValid(this.getPotentialMovesFrom(sentrySq)); } - updateVariables(move) { - const c = this.turn; - const piece = move.vanish[0].p; - const firstRank = c == "w" ? V.size.x - 1 : 0; - - // Update king position + flags - if (piece == V.KING) { - this.kingPos[c][0] = move.appear[0].x; - this.kingPos[c][1] = move.appear[0].y; - this.castleFlags[c] = [false, false]; - return; - } - - // Update castling flags if rook or jailer moved (or is captured) - const oppCol = V.GetOppCol(c); - const oppFirstRank = V.size.x - 1 - firstRank; - let flagIdx = 0; - if ( - // Our rook moves? - move.start.x == firstRank && - this.INIT_COL_ROOK[c] == move.start.y - ) { - if (this.INIT_COL_ROOK[c] > this.INIT_COL_JAILER[c]) flagIdx++; - this.castleFlags[c][flagIdx] = false; - } else if ( - // Our jailer moves? - move.start.x == firstRank && - this.INIT_COL_JAILER[c] == move.start.y - ) { - if (this.INIT_COL_JAILER[c] > this.INIT_COL_ROOK[c]) flagIdx++; - this.castleFlags[c][flagIdx] = false; - } else if ( - // We took opponent's rook? - move.end.x == oppFirstRank && - this.INIT_COL_ROOK[oppCol] == move.end.y - ) { - if (this.INIT_COL_ROOK[oppCol] > this.INIT_COL_JAILER[oppCol]) flagIdx++; - this.castleFlags[oppCol][flagIdx] = false; - } else if ( - // We took opponent's jailer? - move.end.x == oppFirstRank && - this.INIT_COL_JAILER[oppCol] == move.end.y - ) { - if (this.INIT_COL_JAILER[oppCol] > this.INIT_COL_ROOK[oppCol]) flagIdx++; - this.castleFlags[oppCol][flagIdx] = false; - } - + prePlay(move) { if (move.appear.length == 0 && move.vanish.length == 1) { - // The sentry is about to push a piece: + // The sentry is about to push a piece: subTurn goes from 1 to 2 this.sentryPos = { x: move.end.x, y: move.end.y }; - } else if (this.subTurn == 2) { + } else if (this.subTurn == 2 && move.vanish[0].p != V.PAWN) { // A piece is pushed: forbid array of squares between start and end // of move, included (except if it's a pawn) let squares = []; - if (move.vanish[0].p != V.PAWN) { - if ([V.KNIGHT,V.KING].includes(move.vanish[0].p)) - // short-range pieces: just forbid initial square - squares.push(move.start); - else { - const deltaX = move.end.x - move.start.x; - const deltaY = move.end.y - move.start.y; - const step = [ - deltaX / Math.abs(deltaX) || 0, - deltaY / Math.abs(deltaY) || 0 - ]; - for ( - let sq = {x: move.start.x, y: move.start.y}; - sq.x != move.end.x && sq.y != move.end.y; - sq.x += step[0], sq.y += step[1] - ) { - squares.push(sq); - } + if ([V.KNIGHT,V.KING].includes(move.vanish[0].p)) + // short-range pieces: just forbid initial square + squares.push({ x: move.start.x, y: move.start.y }); + else { + const deltaX = move.end.x - move.start.x; + const deltaY = move.end.y - move.start.y; + const step = [ + deltaX / Math.abs(deltaX) || 0, + deltaY / Math.abs(deltaY) || 0 + ]; + for ( + let sq = {x: move.start.x, y: move.start.y}; + sq.x != move.end.x && sq.y != move.end.y; + sq.x += step[0], sq.y += step[1] + ) { + squares.push({ x: sq.x, y: sq.y }); } - // Add end square as well, to know if I was pushed (useful for lancers) - squares.push(move.end); } + // Add end square as well, to know if I was pushed (useful for lancers) + squares.push({ x: move.end.x, y: move.end.y }); this.sentryPush.push(squares); } else this.sentryPush.push(null); } - // TODO: cleaner (global) update/unupdate variables logic, rename... - unupdateVariables(move) { - super.unupdateVariables(move); - this.sentryPush.pop(); - } - play(move) { +// if (!this.states) this.states = []; +// const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen(); +// this.states.push(stateFen); + + this.prePlay(move); move.flags = JSON.stringify(this.aggregateFlags()); this.epSquares.push(this.getEpSquare(move)); V.PlayOnBoard(this.board, move); + // Is it a sentry push? (useful for undo) + move.sentryPush = (this.subTurn == 2); if (this.subTurn == 1) this.movesCount++; - if (move.appear.length == 0 && move.vanish.length == 1) - this.subTurn = 2; + if (move.appear.length == 0 && move.vanish.length == 1) this.subTurn = 2; else { // Turn changes only if not a sentry "pre-push" this.turn = V.GetOppCol(this.turn); this.subTurn = 1; } - this.updateVariables(move); - const L = this.sentryPush.length; - // Is it a sentry push? (useful for undo) - move.sentryPush = !!this.sentryPush[L-1]; + this.postPlay(move); + } + + postPlay(move) { + if (move.vanish.length == 0) + // Special pass move of the king: nothing to update! + return; + super.postPlay(move); } undo(move) { this.epSquares.pop(); this.disaggregateFlags(JSON.parse(move.flags)); V.UndoOnBoard(this.board, move); - const L = this.sentryPush.length; // Decrement movesCount except if the move is a sentry push if (!move.sentryPush) this.movesCount--; - // Turn changes only if not undoing second part of a sentry push - if (!move.sentryPush || this.subTurn == 1) + if (this.subTurn == 2) this.subTurn = 1; + else { this.turn = V.GetOppCol(this.turn); - this.unupdateVariables(move); + if (move.sentryPush) this.subTurn = 2; + } + this.postUndo(move); + +// const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen(); +// if (stateFen != this.states[this.states.length-1]) debugger; +// this.states.pop(); + } + + postUndo(move) { + super.postUndo(move); + this.sentryPush.pop(); + } + + isAttacked(sq, colors) { + return ( + super.isAttacked(sq, colors) || + this.isAttackedByLancer(sq, colors) || + this.isAttackedBySentry(sq, colors) + ); } + isAttackedByLancer([x, y], colors) { + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + // If in this direction there are only enemy pieces and empty squares, + // and we meet a lancer: can he reach us? + // NOTE: do not stop at first lancer, there might be several! + let coord = { x: x + step[0], y: y + step[1] }; + let lancerPos = []; + while ( + V.OnBoard(coord.x, coord.y) && + ( + this.board[coord.x][coord.y] == V.EMPTY || + colors.includes(this.getColor(coord.x, coord.y)) + ) + ) { + if (this.getPiece(coord.x, coord.y) == V.LANCER) + lancerPos.push({x: coord.x, y: coord.y}); + coord.x += step[0]; + coord.y += step[1]; + } + for (let xy of lancerPos) { + const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)]; + if (dir[0] == -step[0] && dir[1] == -step[1]) return true; + } + } + return false; + } + + // Helper to check sentries attacks: + selfAttack([x1, y1], [x2, y2]) { + const color = this.getColor(x1, y1); + const sliderAttack = (allowedSteps, lancer) => { + const deltaX = x2 - x1; + const deltaY = y2 - y1; + const step = [ deltaX / Math.abs(deltaX), deltaY / Math.abs(deltaY) ]; + if (allowedSteps.every(st => st[0] != step[0] || st[1] != step[1])) + return false; + let sq = [ x1 + step[0], y1 + step[1] ]; + while (sq[0] != x2 && sq[1] != y2) { + if ( + // NOTE: no need to check OnBoard in this special case + (!lancer && this.board[sq[0]][sq[1]] != V.EMPTY) || + (!!lancer && this.getColor(sq[0], sq[1]) != color) + ) { + return false; + } + sq[0] += step[0]; + sq[1] += step[1]; + } + return true; + }; + switch (this.getPiece(x1, y1)) { + case V.PAWN: { + // Pushed pawns move as enemy pawns + const shift = (color == 'w' ? 1 : -1); + return (x1 + shift == x2 && Math.abs(y1 - y2) == 1); + } + case V.KNIGHT: { + const deltaX = Math.abs(x1 - x2); + const deltaY = Math.abs(y1 - y2); + return ( + deltaX + deltaY == 3 && + [1, 2].includes(deltaX) && + [1, 2].includes(deltaY) + ); + } + case V.ROOK: + return sliderAttack(V.steps[V.ROOK]); + case V.BISHOP: + return sliderAttack(V.steps[V.BISHOP]); + case V.QUEEN: + return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP])); + case V.LANCER: { + // Special case: as long as no enemy units stands in-between, it attacks + // (if it points toward the king). + const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)]; + return sliderAttack([allowedStep], "lancer"); + } + // No sentries or jailer tests: they cannot self-capture + } + return false; + } + + isAttackedBySentry([x, y], colors) { + // Attacked by sentry means it can self-take our king. + // Just check diagonals of enemy sentry(ies), and if it reaches + // one of our pieces: can I self-take? + const color = V.GetOppCol(colors[0]); + let candidates = []; + for (let i=0; i { + const score = this.getCurrentScore(); + const curEval = move.eval; + if (score != "*") { + move.eval = + score == "1/2" + ? 0 + : (score == "1-0" ? 1 : -1) * maxeval; + } else move.eval = this.evalPosition(); + if ( + // "next" is defined after sentry pushes + !!next && ( + !curEval || + color == 'w' && move.eval > curEval || + color == 'b' && move.eval < curEval + ) + ) { + move.second = next; + } + }; + + // Just search_depth == 1 (because of sentries. TODO: can do better...) + moves1.forEach(m1 => { + this.play(m1); + if (this.subTurn == 1) setEval(m1); + else { + // Need to play every pushes and count: + const moves2 = this.getAllValidMoves(); + moves2.forEach(m2 => { + this.play(m2); + setEval(m1, m2); + this.undo(m2); + }); + } + this.undo(m1); + }); + + moves1.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++) + candidates.push(j); + const choice = moves1[candidates[randInt(candidates.length)]]; + return (!choice.second ? choice : [choice, choice.second]); + } + + // TODO: if subTurn == 2, take some precautions, in particular pawn pushed on 1st rank. + // --> should indicate Sxb2,bxc1 getNotation(move) { // Special case "king takes jailer" is a pass move if (move.appear.length == 0 && move.vanish.length == 0) return "pass";