X-Git-Url: https://git.auder.net/variants/%24%7Bvname%7D/current/git-logo.png?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FEightpieces.js;h=c83cf5ed48a5b2576ddd1cc3d4766b10d12da5af;hb=afbf3ca7151ef15a9e579b0f913683ab212396c4;hp=8c08b6ae15913d5cda9ab187fe8d44e810ff2b7a;hpb=ee3070443a7db2d5702a3a13f234b9af3b360c11;p=vchess.git diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js index 8c08b6ae..c83cf5ed 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 { @@ -72,8 +72,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]; @@ -86,13 +86,6 @@ export const VariantRules = class EightpiecesRules extends ChessRules { } } - canTake([x1,y1], [x2, y2]) { - if (this.subTurn == 2) - // Sentry push: pieces can capture own color (only) - return this.getColor(x1, y1) == this.getColor(x2, y2); - return super.canTake([x1,y1], [x2, y2]); - } - static GenRandInitFen(randomness) { if (randomness == 0) // Deterministic: @@ -224,8 +217,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { this.board[i][j] != V.EMPTY && this.getColor(i, j) == oppCol ) { - const oppPiece = this.getPiece(i, j); - if (oppPiece == V.JAILER) return [i, j]; + if (this.getPiece(i, j) == V.JAILER) return [i, j]; } } return null; @@ -240,7 +232,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: [ @@ -248,7 +240,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) }) ] }); @@ -260,7 +252,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) }) ); } @@ -268,65 +260,78 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return mv; } - getPotentialMovesFrom_aux([x, y]) { + canIplay(side, [x, y]) { + return ( + (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side) || + (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.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 == 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].charAt(1); + } + let moves = []; switch (this.getPiece(x, y)) { case V.JAILER: - return this.getPotentialJailerMoves([x, y]); + moves = this.getPotentialJailerMoves([x, y]); + break; case V.SENTRY: - return this.getPotentialSentryMoves([x, y]); + moves = this.getPotentialSentryMoves([x, y]); + break; case V.LANCER: - return this.getPotentialLancerMoves([x, y]); + moves = this.getPotentialLancerMoves([x, y]); + break; default: - return super.getPotentialMovesFrom([x, y]); + moves = super.getPotentialMovesFrom([x, y]); + break; } - } - - getPotentialMovesFrom([x,y]) { - if (this.subTurn == 1) { - if (!!this.isImmobilized([x, y])) return []; - let moves = this.getPotentialMovesFrom_aux([x, y]); - const L = this.sentryPush.length; - if (!!this.sentryPush[L-1]) { - // Delete moves walking back on sentry push path - moves = moves.filter(m => { - if ( - m.vanish[0].p != V.PAWN && - this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y) - ) { - return false; - } - return true; - }); - } - return moves; + const L = this.sentryPush.length; + if (!!this.sentryPush[L-1]) { + // Delete moves walking back on sentry push path + moves = moves.filter(m => { + if ( + m.vanish[0].p != V.PAWN && + this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y) + ) { + return false; + } + return true; + }); } - // subTurn == 2: only the piece pushed by the sentry is allowed to move, - // as if the sentry didn't exist - if (x != this.sentryPos.x && y != this.sentryPos.y) return []; - const moves2 = this.getPotentialMovesFrom_aux([x, y]); - // Don't forget to re-add the sentry on the board: - const oppCol = V.GetOppCol(this.turn); - return moves2.map(m => { - m.appear.push({x: x, y: y, p: V.SENTRY, c: oppCol}); - return m; - }); + 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.vanish[0].c = oppCol; + }); + } + return moves; } getPotentialPawnMoves([x, y]) { - const color = this.turn; + const color = this.getColor(x, y); let moves = []; const [sizeX, sizeY] = [V.size.x, V.size.y]; - let shiftX = color == "w" ? -1 : 1; - // Special case of a sentry push: pawn goes in the capturer direction - if (this.subTurn == 2) shiftX *= -1; + const shiftX = color == "w" ? -1 : 1; const startRank = color == "w" ? sizeX - 2 : 1; const lastRank = color == "w" ? 0 : sizeX - 1; const finalPieces = - x + shiftX == lastRank + // No promotions after pushes! + x + shiftX == lastRank && this.subTurn == 1 ? - [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER] - .concat(Object.keys(V.LANCER_DIRS)) + Object.keys(V.LANCER_DIRS).concat( + [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]) : [V.PAWN]; if (this.board[x + shiftX][y] == V.EMPTY) { // One square forward @@ -365,7 +370,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { } } - // En passant: no subTurn consideration here (always == 1) + // En passant: const Lep = this.epSquares.length; const epSquare = this.epSquares[Lep - 1]; //always at least one element if ( @@ -388,20 +393,20 @@ export const VariantRules = class EightpiecesRules extends ChessRules { // Obtain all lancer moves in "step" direction, // without final re-orientation. - getPotentialLancerMoves_aux([x, y], step) { + getPotentialLancerMoves_aux([x, y], step, tr) { let moves = []; // Add all moves to vacant squares until opponent is met: - const oppCol = V.GetOppCol(this.turn); + 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; } @@ -419,10 +424,14 @@ export const VariantRules = class EightpiecesRules extends ChessRules { ) { // I was pushed: allow all directions (for this move only), but // do not change direction after moving. + const color = this.getColor(x, y); Object.values(V.LANCER_DIRS).forEach(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]); + }); Array.prototype.push.apply( moves, - this.getPotentialLancerMoves_aux([x, y], step) + this.getPotentialLancerMoves_aux([x, y], step, { p: dirCode, c: color }) ); }); return moves; @@ -432,15 +441,17 @@ export const VariantRules = class EightpiecesRules extends ChessRules { const dirCode = this.board[x][y][1]; const monodirMoves = this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]); - // Add all possible orientations aftermove: - monodirMoves.forEach(m => { - Object.keys(V.LANCER_DIRS).forEach(k => { - let mk = JSON.parse(JSON.stringify(m)); - mk.appear[0].p = k; - moves.push(mk); + // Add all possible orientations aftermove except if I'm being pushed + if (this.subTurn == 1) { + monodirMoves.forEach(m => { + Object.keys(V.LANCER_DIRS).forEach(k => { + let mk = JSON.parse(JSON.stringify(m)); + mk.appear[0].p = k; + moves.push(mk); + }); }); - }); - return moves; + return moves; + } else return monodirMoves; } getPotentialSentryMoves([x, y]) { @@ -448,6 +459,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: @@ -455,16 +468,26 @@ export const VariantRules = class EightpiecesRules extends ChessRules { m.vanish.pop(); } }); - // Can the pushed unit make any move? - this.subTurn = 2; + // Can the pushed unit make any move? ...resulting in a non-self-check? + const color = this.getColor(x, y); const fMoves = moves.filter(m => { - V.PlayOnBoard(this.board, m); - let res = - (this.filterValid(this.getPotentialMovesFrom([x, y])).length > 0); - V.UndoOnBoard(this.board, m); - return res; + // Sentry push? + if (m.appear.length == 0) { + let res = false; + this.play(m); + let moves2 = this.filterValid( + this.getPotentialMovesFrom([m.end.x, m.end.y])); + for (let m2 of moves2) { + this.play(m2); + res = !this.underCheck(color); + this.undo(m2); + if (res) break; + } + this.undo(m); + return res; + } + return true; }); - this.subTurn = 1; return fMoves; } @@ -573,12 +596,19 @@ export const VariantRules = class EightpiecesRules extends ChessRules { } filterValid(moves) { - // Disable check tests when subTurn == 2, because the move isn't finished - if (this.subTurn == 2) return moves; - const filteredMoves = super.filterValid(moves); + // Disable check tests for sentry pushes, + // because in this case the move isn't finished + let movesWithoutSentryPushes = []; + let movesWithSentryPushes = []; + moves.forEach(m => { + if (m.appear.length > 0) movesWithoutSentryPushes.push(m); + else movesWithSentryPushes.push(m); + }); + const filteredMoves = super.filterValid(movesWithoutSentryPushes); // If at least one full move made, everything is allowed: - if (this.movesCount >= 2) return filteredMoves; - // Else, forbid check and captures: + if (this.movesCount >= 2) + return filteredMoves.concat(movesWithSentryPushes); + // 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; @@ -586,13 +616,20 @@ export const VariantRules = class EightpiecesRules extends ChessRules { const res = !this.underCheck(oppCol); this.undo(m); return res; - }); + }).concat(movesWithSentryPushes); + } + + getAllValidMoves() { + if (this.subTurn == 1) return super.getAllValidMoves(); + // Sentry push: + 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; + const firstRank = (c == "w" ? V.size.x - 1 : 0); // Update king position + flags if (piece == V.KING) { @@ -636,12 +673,15 @@ export const VariantRules = class EightpiecesRules extends ChessRules { this.castleFlags[oppCol][flagIdx] = false; } - if (this.subTurn == 2) { + if (move.appear.length == 0 && move.vanish.length == 1) { + // 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) { // 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].insludes(move.vanish[0].p)) + if ([V.KNIGHT,V.KING].includes(move.vanish[0].p)) // short-range pieces: just forbid initial square squares.push(move.start); else { @@ -652,7 +692,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { deltaY / Math.abs(deltaY) || 0 ]; for ( - let sq = {x: x, y: y}; + 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] ) { @@ -676,19 +716,15 @@ export const VariantRules = class EightpiecesRules extends ChessRules { move.flags = JSON.stringify(this.aggregateFlags()); this.epSquares.push(this.getEpSquare(move)); V.PlayOnBoard(this.board, move); - if (this.subTurn == 1) this.movesCount++; this.updateVariables(move); - if (move.appear.length == 0 && move.vanish.length == 1) { - // The sentry is about to push a piece: - this.sentryPos = { x: move.end.x, y: move.end.y }; - this.subTurn = 2; - } else { + // 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; + else { // Turn changes only if not a sentry "pre-push" this.turn = V.GetOppCol(this.turn); this.subTurn = 1; - const L = this.sentryPush.length; - // Is it a sentry push? (useful for undo) - move.sentryPush = !!this.sentryPush[L-1]; } } @@ -699,12 +735,134 @@ export const VariantRules = class EightpiecesRules extends ChessRules { const L = this.sentryPush.length; // Decrement movesCount except if the move is a sentry push if (!move.sentryPush) this.movesCount--; - this.unupdateVariables(move); // Turn changes only if not undoing second part of a sentry push if (!move.sentryPush || this.subTurn == 1) this.turn = V.GetOppCol(this.turn); + this.unupdateVariables(move); + } + + 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)) + ) + ) { + lancerPos.push(coord); + } + 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 (allowedStep.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 ( + (!lancer && this.board[sq[0]][sq[1]] != V.EMPTY) || + (!!lancer && this.getColor(sq[0], sq[1]) != color) + ) { + return false; + } + } + 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(); + if (score != "*") { + move.eval = + score == "1/2" + ? 0 + : (score == "1-0" ? 1 : -1) * maxeval; + } else move[i].eval = this.evalPosition(); + }; + + // 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); + 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); + return moves1[candidates[randInt(candidates.length)]]; + } + getNotation(move) { // Special case "king takes jailer" is a pass move if (move.appear.length == 0 && move.vanish.length == 0) return "pass";