From afbf3ca7151ef15a9e579b0f913683ab212396c4 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 13 Mar 2020 20:54:45 +0100 Subject: [PATCH] Eightpieces almost ready --- client/src/base_rules.js | 70 +++--- client/src/components/Board.vue | 3 +- client/src/store.js | 1 - .../src/translations/rules/Eightpieces/en.pug | 34 +-- .../src/translations/rules/Eightpieces/es.pug | 36 +-- .../src/translations/rules/Eightpieces/fr.pug | 36 +-- client/src/variants/Eightpieces.js | 219 ++++++++++++++++-- 7 files changed, 237 insertions(+), 162 deletions(-) diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 41ebc140..4c32fe69 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -1141,44 +1141,48 @@ export const ChessRules = class ChessRules { // Rank moves using a min-max at depth 2 (if search_depth >= 2!) for (let i = 0; i < moves1.length; i++) { - if (V.SEARCH_DEPTH == 1) { - moves1[i].eval = this.evalPosition(); + this.play(moves1[i]); + const score1 = this.getCurrentScore(); + if (score1 != "*") { + moves1[i].eval = + score1 == "1/2" + ? 0 + : (score1 == "1-0" ? 1 : -1) * maxeval; + } + if (V.SEARCH_DEPTH == 1 || score1 != "*") { + if (!moves1[i].eval) moves1[i].eval = this.evalPosition(); + this.undo(moves1[i]); continue; } // Initial self evaluation is very low: "I'm checkmated" moves1[i].eval = (color == "w" ? -1 : 1) * maxeval; - this.play(moves1[i]); - const score1 = this.getCurrentScore(); - let eval2 = undefined; - if (score1 == "*") { - // Initial enemy evaluation is very low too, for him - eval2 = (color == "w" ? 1 : -1) * maxeval; - // Second half-move: - let moves2 = this.getAllValidMoves(); - for (let j = 0; j < moves2.length; j++) { - this.play(moves2[j]); - const score2 = this.getCurrentScore(); - let evalPos = 0; //1/2 value - switch (score2) { - case "*": - evalPos = this.evalPosition(); - break; - case "1-0": - evalPos = maxeval; - break; - case "0-1": - evalPos = -maxeval; - break; - } - if ( - (color == "w" && evalPos < eval2) || - (color == "b" && evalPos > eval2) - ) { - eval2 = evalPos; - } - this.undo(moves2[j]); + // Initial enemy evaluation is very low too, for him + let eval2 = (color == "w" ? 1 : -1) * maxeval; + // Second half-move: + let moves2 = this.getAllValidMoves(); + for (let j = 0; j < moves2.length; j++) { + this.play(moves2[j]); + const score2 = this.getCurrentScore(); + let evalPos = 0; //1/2 value + switch (score2) { + case "*": + evalPos = this.evalPosition(); + break; + case "1-0": + evalPos = maxeval; + break; + case "0-1": + evalPos = -maxeval; + break; } - } else eval2 = score1 == "1/2" ? 0 : (score1 == "1-0" ? 1 : -1) * maxeval; + if ( + (color == "w" && evalPos < eval2) || + (color == "b" && evalPos > eval2) + ) { + eval2 = evalPos; + } + this.undo(moves2[j]); + } if ( (color == "w" && eval2 > moves1[i].eval) || (color == "b" && eval2 < moves1[i].eval) diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index f2fd44c3..fdeeef31 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -275,9 +275,10 @@ export default { if (!!this.vr.reserve) elementArray.push(reserveBottom); const boardElt = document.querySelector(".game"); if (this.choices.length > 0 && !!boardElt) { - //no choices to show at first drawing + // No choices to show at first drawing const squareWidth = boardElt.offsetWidth / sizeY; const offset = [boardElt.offsetTop, boardElt.offsetLeft]; + // TODO: multi-rows if more than V.size.y pieces (as inEightpieces) const choices = h( "div", { diff --git a/client/src/store.js b/client/src/store.js index 8fe46b90..76210ee3 100644 --- a/client/src/store.js +++ b/client/src/store.js @@ -31,7 +31,6 @@ export const store = { return; } this.state.variants = json.variantArray - .filter(v => v.name != "Eightpieces") //TODO: not ready yet .sort((v1,v2) => v1.name.localeCompare(v2.name)); }); let mysid = localStorage.getItem("mysid"); diff --git a/client/src/translations/rules/Eightpieces/en.pug b/client/src/translations/rules/Eightpieces/en.pug index db673e5a..3a33838b 100644 --- a/client/src/translations/rules/Eightpieces/en.pug +++ b/client/src/translations/rules/Eightpieces/en.pug @@ -1,34 +1,2 @@ p.boxed - | Pawns start on the 7th rank. Move a knight to promote them. - -p. - ...Only the initial position changes, but this makes a huge difference. - In particular, castling would be rather pointless so it's disabled here. - En-passant captures are impossible because all pawns already reached 7th rank. - -h3 About the initial position - -p. - Since truly random start can allow a mate in 3 with a knight, - the kings have at least one knight neighbor in the initial position. - This allows to move free out of potential check from the very beginning. - -p. - A less constraining condition would be to require the two knights to stand on - two squares of different colors, but it's not enough as proved by the - following diagram. - White can mate in 3: 1.Nc6 followed by Nb4 threatening both a2 and d3. - -figure.diagram-container - .diagram - | fen:RBN1BRRQ/PPPPPPP/8/4n3/8/8/Nppppppp/brkbqr1n: - figcaption After 1.Nc6 Nf3 2.Nb4 Ne5 (covers d3 but not a2) 3.Nxa2# - -p Note: in the standard initial position, kings and knights are not neighbors. - -h3 Source - -p - | See for example the - a(href="https://www.chessvariants.com/diffsetup.dir/upside.html") Upside down chess - |  page on chessvariants.com. + | TODO diff --git a/client/src/translations/rules/Eightpieces/es.pug b/client/src/translations/rules/Eightpieces/es.pug index 80d76fdc..3a33838b 100644 --- a/client/src/translations/rules/Eightpieces/es.pug +++ b/client/src/translations/rules/Eightpieces/es.pug @@ -1,36 +1,2 @@ p.boxed - | Las piezas comienzan en la séptima fila. Mueve un caballo para promocionar uno. - -p. - ...Solo cambia la posición inicial, pero es una gran diferencia. - En particular, el enroque no sería interesante y, por lo tanto, está deshabilitado aquí. - Las capturas en passant también son imposibles porque todos los peones - ya están en la 7ma fila. - -h3 Acerca de la posición inicial - -p. - Una disposición de piezas completamente al azar puede permitir un mate en 3 - con un caballo: es por eso que el rey siempre está al lado de a menos - un caballo al comienzo del juego. Esto le permite liberarse de jaques tan pronto - primer movimiento. - -p. - Para ilustrar este fenómeno, los blancos pueden mate en 3 en la posición - del siguiente diagrama: 1.Na6 seguido de Nc5 luego Nd3#. - Si el caballo negro estuviera en g1, una defensa sería posible con Nf3-Ne5; - pero con un caballo al lado del rey se ofrecen más opciones. - -figure.diagram-container - .diagram - | fen:R1BQKBNR/PPPPPPP/N7/8/8/8/pppppppp/rnbqkbrn c5,d3: - figcaption Posición inicial estándar después de 1.Na6 - -p Nota: en la posición inicial habitual, los reyes y los caballos no son vecinos. - -h3 Fuente - -p - | Ver por ejemplo la página - a(href="https://www.chessvariants.com/diffsetup.dir/upside.html") ajedrez al revés - |  en chessvariants.com. + | TODO diff --git a/client/src/translations/rules/Eightpieces/fr.pug b/client/src/translations/rules/Eightpieces/fr.pug index 0631994b..3a33838b 100644 --- a/client/src/translations/rules/Eightpieces/fr.pug +++ b/client/src/translations/rules/Eightpieces/fr.pug @@ -1,36 +1,2 @@ p.boxed - | Les pions démarrent sur la 7eme rangée. Déplacez un cavalier pour en promouvoir un. - -p. - ...Seule la position de départ change, mais c'est une énorme différence. - En particulier, le roque serait sans intérêt et est donc désactivé ici. - Les captures en passant sont également impossible car tous les pions - sont déjà sur la 7eme rangée. - -h3 Au sujet de la position initiale - -p. - Un placement complètement aléatoire des pièces peut permettre un mat en 3 - à l'aide d'un cavalier : c'est pourquoi le roi est toujours à côté d'au moins - un cavalier en début de partie. Cela permet de se dégager des échecs dès le - premier coup. - -p. - Pour illustrer ce phénomène, les blancs peuvent mater en 3 dans la position - du diagramme suivant : 1.Na6 suivi de Nc5 puis Nd3#. - Si le cavalier noir était en g1 une défense serait possible par Nf3-Ne5 ; - mais avec un cavalier voisin du roi plus d'options sont offertes. - -figure.diagram-container - .diagram - | fen:R1BQKBNR/PPPPPPP/N7/8/8/8/pppppppp/rnbqkbrn c5,d3: - figcaption Standard initial position after 1.Na6 - -p Note : dans la position initiale habituelle, rois et cavaliers ne sont pas voisins. - -h3 Source - -p - | Voir par exemple la page - a(href="https://www.chessvariants.com/diffsetup.dir/upside.html") Échecs Upside down - |  sur chessvariants.com. + | TODO diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js index f040a838..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]; @@ -232,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: [ @@ -240,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) }) ] }); @@ -252,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) }) ); } @@ -276,7 +276,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { 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,7 +306,7 @@ 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 => { @@ -327,7 +327,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]) @@ -392,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.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; } @@ -423,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; @@ -454,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: @@ -597,11 +604,11 @@ export const VariantRules = class EightpiecesRules extends ChessRules { if (m.appear.length > 0) movesWithoutSentryPushes.push(m); else movesWithSentryPushes.push(m); }); - const filteredMoves = super.filterValid(movesWithoutSentryPushes) + 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,14 +622,14 @@ 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; + const firstRank = (c == "w" ? V.size.x - 1 : 0); // Update king position + flags if (piece == V.KING) { @@ -667,7 +674,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { } 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) { // A piece is pushed: forbid array of squares between start and end @@ -709,18 +716,16 @@ export const VariantRules = class EightpiecesRules extends ChessRules { move.flags = JSON.stringify(this.aggregateFlags()); this.epSquares.push(this.getEpSquare(move)); V.PlayOnBoard(this.board, move); + this.updateVariables(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]; } undo(move) { @@ -736,6 +741,128 @@ export const VariantRules = class EightpiecesRules extends ChessRules { 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"; -- 2.44.0