From a9e1202b681d9d2f814767180183a0b04c58f8ab Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 20 May 2020 22:52:22 +0200 Subject: [PATCH] A few fixes (for updateCastleFlags()) + add Madhouse and Pocketknight variants --- TODO | 12 +- client/src/base_rules.js | 4 +- client/src/components/BaseGame.vue | 8 +- client/src/translations/en.js | 2 + client/src/translations/es.js | 2 + client/src/translations/fr.js | 2 + client/src/translations/rules/Madhouse/en.pug | 26 ++ client/src/translations/rules/Madhouse/es.pug | 27 ++ client/src/translations/rules/Madhouse/fr.pug | 27 ++ .../translations/rules/Pocketknight/en.pug | 18 ++ .../translations/rules/Pocketknight/es.pug | 19 ++ .../translations/rules/Pocketknight/fr.pug | 18 ++ client/src/variants/Checkered1.js | 29 +- client/src/variants/Dynamo.js | 7 - client/src/variants/Madhouse.js | 257 +++++++++++++++++ client/src/variants/Pocketknight.js | 263 ++++++++++++++++++ client/src/variants/Takenmake.js | 2 +- client/src/variants/Teleport.js | 31 +-- server/db/populate.sql | 2 + 19 files changed, 681 insertions(+), 75 deletions(-) create mode 100644 client/src/translations/rules/Madhouse/en.pug create mode 100644 client/src/translations/rules/Madhouse/es.pug create mode 100644 client/src/translations/rules/Madhouse/fr.pug create mode 100644 client/src/translations/rules/Pocketknight/en.pug create mode 100644 client/src/translations/rules/Pocketknight/es.pug create mode 100644 client/src/translations/rules/Pocketknight/fr.pug create mode 100644 client/src/variants/Madhouse.js create mode 100644 client/src/variants/Pocketknight.js diff --git a/TODO b/TODO index f4d2c505..774d82de 100644 --- a/TODO +++ b/TODO @@ -3,16 +3,20 @@ Also: if new live game starts in background, "new game" notify OK but not first On smartphone for Teleport, Chakart, Weiqi and some others: option "confirm moves on touch screen" (=> comme pour corr) + option "confirm moves in corr games"? -https://www.chessvariants.com/difftaking.dir/replacement.html - -https://www.chessvariants.com/other.dir/pocket.html https://www.chessvariants.com/other.dir/fugue.html https://www.chessvariants.com/rules/spartan-chess https://www.chessvariants.com/mvopponent.dir/avalanche.html - https://www.chessvariants.com/mvopponent.dir/hypnotic-chess.html https://www.chessvariants.com/mvopponent.dir/mesmer-chess.html +Squatter Chess: safe on last rank = win +Companion Chess : pieces of same nature don't attack each others +Medusa Chess = Isardam +Crossing Chess = win when the king cross half-board +Kingmaker: pawns can promote also into enemy king +Eightkings: 8 pawns + 8 kings (non-royal until the last remains?) +Crown Chess: place all units on move 1 (similar to Sittuyin, more freely) + ===== fanorona https://fr.wikipedia.org/wiki/Fanorona diff --git a/client/src/base_rules.js b/client/src/base_rules.js index d219f78a..9ee506e5 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -1169,8 +1169,8 @@ export const ChessRules = class ChessRules { this.postPlay(move); } - updateCastleFlags(move, piece) { - const c = V.GetOppCol(this.turn); + updateCastleFlags(move, piece, color) { + const c = color || V.GetOppCol(this.turn); const firstRank = (c == "w" ? V.size.x - 1 : 0); // Update castling flags if rooks are moved const oppCol = this.turn; diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index cb65f033..87d18aba 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -252,12 +252,10 @@ export default { const checkSquares = this.vr.getCheckSquares(); if (checkSquares.length > 0) m.notation += "+"; if (idxM == Lm - 1) m.fen = this.vr.getFen(); - if (idx == L - 1 && idxM == Lm - 1) { - this.incheck = checkSquares; - this.score = this.vr.getCurrentScore(); - if (["1-0", "0-1"].includes(this.score)) m.notation += "#"; - } + if (idx == L - 1 && idxM == Lm - 1) this.incheck = checkSquares; }); + this.score = this.vr.getCurrentScore(); + if (["1-0", "0-1"].includes(this.score)) m.notation += "#"; }); } if (firstMoveColor == "b") { diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 78e112cc..d223dc14 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -200,11 +200,13 @@ export const translations = { "King of the Hill": "King of the Hill", "Kings cross the 8x8 board": "Kings cross the 8x8 board", "Kings cross the 11x11 board": "Kings cross the 11x11 board", + "Knight in pocket": "Knight in pocket", "Landing on the board": "Landing on the board", "Laws of attraction": "Laws of attraction", "Long jumps over pieces": "Long jumps over pieces", "Long live the Queen": "Long live the Queen", "Lose all pieces": "Lose all pieces", + "Rearrange enemy pieces": "Rearrange enemy pieces", "Mandatory captures": "Mandatory captures", "Mate any piece (v1)": "Mate any piece (v1)", "Mate any piece (v2)": "Mate any piece (v2)", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 0c7fca06..fe610f2d 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -200,11 +200,13 @@ export const translations = { "King of the Hill": "Rey de la Colina", "Kings cross the 8x8 board": "Los reyes cruzan el 8x8 tablero", "Kings cross the 11x11 board": "Los reyes cruzan el 11x11 tablero", + "Knight in pocket": "Caballo en bolsillo", "Landing on the board": "Aterrizando en el tablero", "Laws of attraction": "Las leyes de las atracciones", "Long jumps over pieces": "Saltos largos sobre las piezas", "Long live the Queen": "Larga vida a la reina", "Lose all pieces": "Perder todas las piezas", + "Rearrange enemy pieces": "Reorganizar piezas opuestas", "Mandatory captures": "Capturas obligatorias", "Mate any piece (v1)": "Matar cualquier pieza (v1)", "Mate any piece (v2)": "Matar cualquier pieza (v2)", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index e6b1c480..980beac5 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -200,11 +200,13 @@ export const translations = { "King of the Hill": "Roi de la Colline", "Kings cross the 8x8 board": "Les rois traversent l'échiquier 8x8", "Kings cross the 11x11 board": "Les rois traversent l'échiquier 11x11", + "Knight in pocket": "Cavalier en poche", "Landing on the board": "Débarquement sur l'échiquier", "Laws of attraction": "Les lois de l'attraction", "Long jumps over pieces": "Sauts longs par dessus les pièces", "Long live the Queen": "Long vie à la Reine", "Lose all pieces": "Perdez toutes les pièces", + "Rearrange enemy pieces": "Réorganisez les pièces adverses", "Mandatory captures": "Captures obligatoires", "Mate any piece (v1)": "Matez n'importe quelle pièce (v1)", "Mate any piece (v2)": "Matez n'importe quelle pièce (v2)", diff --git a/client/src/translations/rules/Madhouse/en.pug b/client/src/translations/rules/Madhouse/en.pug new file mode 100644 index 00000000..2320e8fa --- /dev/null +++ b/client/src/translations/rules/Madhouse/en.pug @@ -0,0 +1,26 @@ +p.boxed + | Captured pieces are immediatly repositioned on the board. + +p. + The orthodox rules apply, with the following exception. + Each piece taken from the opponent is placed back on an empty + square by the player that made the capture, such that +ul + li Bishops remain on squares of the same color. + li Pawns are not placed on the first or last row. + +figure.diagram-container + .diagram.diag12 + | fen:r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R: + .diagram.diag22 + | fen:r1bqkbnr/pppp1ppp/2B2n2/4p3/4P3/5N2/PPPP1PPP/RNBQK2R: + figcaption Bishop takes knight, which is repositioned on g8. + +p To place the captured piece, just click on an empty square. + +h3 Source + +p + a(href="https://www.chessvariants.com/difftaking.dir/replacement.html") + | Replacement chess + |  on chessvariants.com. diff --git a/client/src/translations/rules/Madhouse/es.pug b/client/src/translations/rules/Madhouse/es.pug new file mode 100644 index 00000000..2aaa4ac5 --- /dev/null +++ b/client/src/translations/rules/Madhouse/es.pug @@ -0,0 +1,27 @@ +p.boxed + | Las piezas capturadas se reemplazan inmediatamente en el tablero. + +p. + Se aplican reglas ortodoxas, con la siguiente excepción. + Cada pieza tomada del oponente se vuelve a poner inmediatamente en el + tablero, en casi cualquier espacio libre: +ul + li Los alfiles se quedan en las casillas del mismo color. + li Los peones no se colocan en la primera o última fila. + +figure.diagram-container + .diagram.diag12 + | fen:r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R: + .diagram.diag22 + | fen:r1bqkbnr/pppp1ppp/2B2n2/4p3/4P3/5N2/PPPP1PPP/RNBQK2R: + figcaption Alfil toma el caballo, que se reemplaza en g8. + +p Para colocar una pieza, simplemente haga clic en una casilla vacía. + +h3 Fuente + +p + | La + a(href="https://www.chessvariants.com/difftaking.dir/replacement.html") + | variante Reemplazo + |  en chessvariants.com. diff --git a/client/src/translations/rules/Madhouse/fr.pug b/client/src/translations/rules/Madhouse/fr.pug new file mode 100644 index 00000000..4c07a739 --- /dev/null +++ b/client/src/translations/rules/Madhouse/fr.pug @@ -0,0 +1,27 @@ +p.boxed + | Les pièces capturées sont immédiatement replacées sur l'échiquier. + +p. + Les règles orthodoxes s'appliquent, avec l'exception suivante. + Chaque pièce prise à l'adversaire est immédiatement remise sur l'échiquier, + sur presque n'importe quelle case libre : +ul + li Les fous restent sur les cases de la même couleur. + li Les pions ne sont pas posés sur la première ou dernière rangée. + +figure.diagram-container + .diagram.diag12 + | fen:r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R: + .diagram.diag22 + | fen:r1bqkbnr/pppp1ppp/2B2n2/4p3/4P3/5N2/PPPP1PPP/RNBQK2R: + figcaption Fou prend cavalier, qui est replacé en g8. + +p Pour poser une pièce, cliquez simplement sur une case vide. + +h3 Source + +p + | La + a(href="https://www.chessvariants.com/difftaking.dir/replacement.html") + | variante Remplacement + |  sur chessvariants.com. diff --git a/client/src/translations/rules/Pocketknight/en.pug b/client/src/translations/rules/Pocketknight/en.pug new file mode 100644 index 00000000..fda5baf7 --- /dev/null +++ b/client/src/translations/rules/Pocketknight/en.pug @@ -0,0 +1,18 @@ +p.boxed + | Each player has a knight in pocket, which can be dropped at any moment. + +p. + To use your pocket knight, "capture" the enemy king with yours, + and then click on any empty square. + +figure.diagram-container + .diagram + | fen:r1bqkbnr/ppNnpppp/2p5/3p4/2PP1B2/8/PP2PPPP/RN1QKBNR: + figcaption After N@c7+, black must give the queen. + +h3 Source + +p + a(href="https://www.chessvariants.com/other.dir/pocket.html") + | Pocket Knight + |  on chessvariants.com. diff --git a/client/src/translations/rules/Pocketknight/es.pug b/client/src/translations/rules/Pocketknight/es.pug new file mode 100644 index 00000000..dbb6b0d0 --- /dev/null +++ b/client/src/translations/rules/Pocketknight/es.pug @@ -0,0 +1,19 @@ +p.boxed + | Cada jugador tiene un caballo en su bolsillo, + | que puede poner en cualquier momento. + +p. + Para usar tu caballo de bolsillo, "captura" al rey contrario con el + su, luego haga clic en cualquier casilla vacía. + +figure.diagram-container + .diagram + | fen:r1bqkbnr/ppNnpppp/2p5/3p4/2PP1B2/8/PP2PPPP/RN1QKBNR: + figcaption Después de N@c7+, las negras deben dar la dama. + +h3 Fuente + +p + a(href="https://www.chessvariants.com/other.dir/pocket.html") + | Pocket Knight + |  en chessvariants.com. diff --git a/client/src/translations/rules/Pocketknight/fr.pug b/client/src/translations/rules/Pocketknight/fr.pug new file mode 100644 index 00000000..ea3a9db1 --- /dev/null +++ b/client/src/translations/rules/Pocketknight/fr.pug @@ -0,0 +1,18 @@ +p.boxed + | Chaque jouer a un cavalier en poche, qu'il peut poser à tout moment. + +p. + Pour utiliser votre cavalier de poche, "capturez" le roi adverse avec le + votre, puis cliquez sur n'importe quelle case vide. + +figure.diagram-container + .diagram + | fen:r1bqkbnr/ppNnpppp/2p5/3p4/2PP1B2/8/PP2PPPP/RN1QKBNR: + figcaption Après N@c7+, les noirs doivent donner la dame. + +h3 Source + +p + a(href="https://www.chessvariants.com/other.dir/pocket.html") + | Pocket Knight + |  sur chessvariants.com. diff --git a/client/src/variants/Checkered1.js b/client/src/variants/Checkered1.js index 47e7375e..4556f07b 100644 --- a/client/src/variants/Checkered1.js +++ b/client/src/variants/Checkered1.js @@ -487,31 +487,6 @@ export class Checkered1Rules extends ChessRules { this.postPlay(move); } - updateCastleFlags(move, piece) { - const c = V.GetOppCol(this.turn); - const firstRank = (c == "w" ? V.size.x - 1 : 0); - // Update castling flags if rooks are moved - const oppCol = this.turn; - const oppFirstRank = V.size.x - 1 - firstRank; - if (piece == V.KING && move.appear.length > 0) - this.castleFlags[c] = [V.size.y, V.size.y]; - else if ( - move.start.x == firstRank && //our rook moves? - this.castleFlags[c].includes(move.start.y) - ) { - const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1); - this.castleFlags[c][flagIdx] = V.size.y; - } - // NOTE: not "else if" because a rook could take an opposing rook - if ( - move.end.x == oppFirstRank && //we took opponent rook? - this.castleFlags[oppCol].includes(move.end.y) - ) { - const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1); - this.castleFlags[oppCol][flagIdx] = V.size.y; - } - } - postPlay(move) { if (move.appear.length == 0 && move.vanish.length == 0) { this.stage = 2; @@ -524,7 +499,7 @@ export class Checkered1Rules extends ChessRules { this.kingPos[c][0] = move.appear[0].x; this.kingPos[c][1] = move.appear[0].y; } - this.updateCastleFlags(move, piece); + super.updateCastleFlags(move, piece); // Does this move turn off a 2-squares pawn flag? if ([1, 6].includes(move.start.x) && move.vanish[0].p == V.PAWN) this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false; @@ -609,7 +584,7 @@ export class Checkered1Rules extends ChessRules { } static GenRandInitFen(randomness) { - // Add 16 pawns flags + empty cmovei + stage == 1: + // Add 16 pawns flags + empty cmove + stage == 1: return ChessRules.GenRandInitFen(randomness) .slice(0, -2) + "1111111111111111 - - 1"; } diff --git a/client/src/variants/Dynamo.js b/client/src/variants/Dynamo.js index cdb02c6a..dbc19c21 100644 --- a/client/src/variants/Dynamo.js +++ b/client/src/variants/Dynamo.js @@ -738,13 +738,6 @@ export class DynamoRules extends ChessRules { return potentialMoves; } - getCurrentScore() { - if (this.subTurn == 2) - // Move not over - return "*"; - return super.getCurrentScore(); - } - doClick(square) { // A click to promote a piece on subTurn 2 would trigger this. // For now it would then return [NaN, NaN] because surrounding squares diff --git a/client/src/variants/Madhouse.js b/client/src/variants/Madhouse.js new file mode 100644 index 00000000..c157437d --- /dev/null +++ b/client/src/variants/Madhouse.js @@ -0,0 +1,257 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class MadhouseRules extends ChessRules { + hoverHighlight(x, y) { + // Testing move validity results in an infinite update loop. + // TODO: find a way to test validity anyway. + return (this.subTurn == 2 && this.board[x][y] == V.EMPTY); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + this.subTurn = 1; + this.firstMove = []; + } + + getPotentialMovesFrom([x, y]) { + if (this.subTurn == 1) return super.getPotentialMovesFrom([x, y]); + // subTurn == 2: a move is a click, not handled here + return []; + } + + filterValid(moves) { + if (this.subTurn == 2) return super.filterValid(moves); + const color = this.turn; + return moves.filter(m => { + this.play(m); + let res = false; + if (m.vanish.length == 1 || m.appear.length == 2) + // Standard check: + res = !this.underCheck(color); + else { + // Capture: find landing square not resulting in check + const boundary = (m.vanish[1].p != V.PAWN ? [0, 7] : [1, 6]); + const sqColor = + m.vanish[1].p == V.BISHOP + ? (m.vanish[1].x + m.vanish[1].y) % 2 + : null; + outerLoop: for (let i = boundary[0]; i <= boundary[1]; i++) { + for (let j=0; j<8; j++) { + if ( + this.board[i][j] == V.EMPTY && + (!sqColor || (i + j) % 2 == sqColor) + ) { + const tMove = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: m.vanish[1].c, + p: m.vanish[1].p + }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + this.play(tMove); + const moveOk = !this.underCheck(color); + this.undo(tMove); + if (moveOk) { + res = true; + break outerLoop; + } + } + } + } + } + this.undo(m); + return res; + }); + } + + getAllValidMoves() { + if (this.subTurn == 1) return super.getAllValidMoves(); + // Subturn == 2: only replacements + let moves = []; + const L = this.firstMove.length; + const fm = this.firstMove[L - 1]; + const color = this.turn; + const oppCol = V.GetOppCol(color); + const boundary = (fm.vanish[1].p != V.PAWN ? [0, 7] : [1, 6]); + const sqColor = + fm.vanish[1].p == V.BISHOP + ? (fm.vanish[1].x + fm.vanish[1].y) % 2 + : null; + for (let i = boundary[0]; i < boundary[1]; i++) { + for (let j=0; j<8; j++) { + if ( + this.board[i][j] == V.EMPTY && + (!sqColor || (i + j) % 2 == sqColor) + ) { + const tMove = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: oppCol, + p: fm.vanish[1].p + }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + this.play(tMove); + const moveOk = !this.underCheck(color); + this.undo(tMove); + if (moveOk) moves.push(tMove); + } + } + } + return moves; + } + + doClick(square) { + if (isNaN(square[0])) return null; + // If subTurn == 2 && square is empty && !underCheck, then replacement + if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) { + const L = this.firstMove.length; + const fm = this.firstMove[L - 1]; + const color = this.turn; + const oppCol = V.GetOppCol(color); + if ( + (fm.vanish[1].p == V.PAWN && [0, 7].includes(square[0])) || + ( + fm.vanish[1].p == V.BISHOP && + (square[0] + square[1] + fm.vanish[1].x + fm.vanish[1].y) % 2 != 0 + ) + ) { + // Pawns cannot be replaced on first or last rank, + // bishops must be replaced on same square color. + return null; + } + const tMove = new Move({ + appear: [ + new PiPo({ + x: square[0], + y: square[1], + c: oppCol, + p: fm.vanish[1].p + }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + this.play(tMove); + const moveOk = !this.underCheck(color); + this.undo(tMove); + if (moveOk) return tMove; + } + return null; + } + + play(move) { + move.flags = JSON.stringify(this.aggregateFlags()); + if (move.vanish.length > 0) { + this.epSquares.push(this.getEpSquare(move)); + this.firstMove.push(move); + } + V.PlayOnBoard(this.board, move); + if ( + this.subTurn == 2 || + move.vanish.length == 1 || + move.appear.length == 2 + ) { + this.turn = V.GetOppCol(this.turn); + this.subTurn = 1; + this.movesCount++; + } + else this.subTurn = 2; + if (move.vanish.length > 0) this.postPlay(move); + } + + postPlay(move) { + if (move.appear[0].p == V.KING) + this.kingPos[move.appear[0].c] = [move.appear[0].x, move.appear[0].y]; + this.updateCastleFlags(move, move.appear[0].p, move.appear[0].c); + } + + undo(move) { + this.disaggregateFlags(JSON.parse(move.flags)); + if (move.vanish.length > 0) { + this.epSquares.pop(); + this.firstMove.pop(); + } + V.UndoOnBoard(this.board, move); + if (this.subTurn == 2) this.subTurn = 1; + else { + this.turn = V.GetOppCol(this.turn); + this.movesCount--; + this.subTurn = (move.vanish.length > 0 ? 1 : 2); + } + if (move.vanish.length > 0) super.postUndo(move); + } + + getComputerMove() { + let moves = this.getAllValidMoves(); + if (moves.length == 0) return null; + // Custom "search" at depth 1 (for now. TODO?) + const maxeval = V.INFINITY; + const color = this.turn; + const initEval = this.evalPosition(); + moves.forEach(m => { + this.play(m); + m.eval = (color == "w" ? -1 : 1) * maxeval; + if (m.vanish.length == 2 && m.appear.length == 1) { + const moves2 = this.getAllValidMoves(); + m.next = moves2[0]; + moves2.forEach(m2 => { + this.play(m2); + const score = this.getCurrentScore(); + let mvEval = 0; + if (["1-0", "0-1"].includes(score)) + mvEval = (score == "1-0" ? 1 : -1) * maxeval; + else if (score == "*") + // Add small fluctuations to avoid dropping pieces always on the + // first square available. + mvEval = initEval + 0.05 - Math.random() / 10; + if ( + (color == 'w' && mvEval > m.eval) || + (color == 'b' && mvEval < m.eval) + ) { + m.eval = mvEval; + m.next = m2; + } + this.undo(m2); + }); + } + else { + const score = this.getCurrentScore(); + if (score != "1/2") { + if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval; + else m.eval = this.evalPosition(); + } + } + this.undo(m); + }); + moves.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++) + candidates.push(i); + const mIdx = candidates[randInt(candidates.length)]; + if (!moves[mIdx].next) return moves[mIdx]; + const move2 = moves[mIdx].next; + delete moves[mIdx]["next"]; + return [moves[mIdx], move2]; + } + + getNotation(move) { + if (move.vanish.length > 0) return super.getNotation(move); + // Replacement: + const piece = + move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : ""; + return piece + "@" + V.CoordsToSquare(move.end); + } +}; diff --git a/client/src/variants/Pocketknight.js b/client/src/variants/Pocketknight.js new file mode 100644 index 00000000..6aa94aa5 --- /dev/null +++ b/client/src/variants/Pocketknight.js @@ -0,0 +1,263 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class PocketknightRules extends ChessRules { + hoverHighlight(x, y) { + // Testing move validity results in an infinite update loop. + // TODO: find a way to test validity anyway. + return (this.subTurn == 2 && this.board[x][y] == V.EMPTY); + } + + static IsGoodFlags(flags) { + // 4 for castle + 2 for knights + return !!flags.match(/^[a-z]{4,4}[01]{2,2}$/); + } + + setFlags(fenflags) { + super.setFlags(fenflags); //castleFlags + this.knightFlags = fenflags.substr(4).split("").map(e => e == "1"); + } + + aggregateFlags() { + return [this.castleFlags, this.knightFlags]; + } + + disaggregateFlags(flags) { + this.castleFlags = flags[0]; + this.knightFlags = flags[1]; + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + this.subTurn = 1; + } + + static GenRandInitFen(randomness) { + // Add 2 knight flags + return ChessRules.GenRandInitFen(randomness) + .slice(0, -2) + "11 -"; + } + + getFlagsFen() { + return ( + super.getFlagsFen() + this.knightFlags.map(e => e ? "1" : "0").join("") + ); + } + + getPotentialMovesFrom([x, y]) { + if (this.subTurn == 1) { + let moves = super.getPotentialMovesFrom([x, y]); + // If flag allow it, add "king capture" + if ( + this.knightFlags[this.turn == 'w' ? 0 : 1] && + this.getPiece(x, y) == V.KING + ) { + const kp = this.kingPos[V.GetOppCol(this.turn)]; + moves.push( + new Move({ + appear: [], + vanish: [], + start: { x: x, y: y }, + end: { x: kp[0], y: kp[1] } + }) + ); + } + return moves; + } + // subTurn == 2: a move is a click, not handled here + return []; + } + + filterValid(moves) { + if (this.subTurn == 2) return super.filterValid(moves); + const color = this.turn; + return moves.filter(m => { + this.play(m); + let res = false; + if (m.appear.length > 0) + // Standard check: + res = !this.underCheck(color); + else { + // "Capture king": find landing square not resulting in check + outerLoop: for (let i=0; i<8; i++) { + for (let j=0; j<8; j++) { + if (this.board[i][j] == V.EMPTY) { + const tMove = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: color, + p: V.KNIGHT + }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + this.play(tMove); + const moveOk = !this.underCheck(color); + this.undo(tMove); + if (moveOk) { + res = true; + break outerLoop; + } + } + } + } + } + this.undo(m); + return res; + }); + } + + getAllValidMoves() { + if (this.subTurn == 1) return super.getAllValidMoves(); + // Subturn == 2: only knight landings + let moves = []; + const color = this.turn; + for (let i=0; i<8; i++) { + for (let j=0; j<8; j++) { + if (this.board[i][j] == V.EMPTY) { + const tMove = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: color, + p: V.KNIGHT + }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + this.play(tMove); + const moveOk = !this.underCheck(color); + this.undo(tMove); + if (moveOk) moves.push(tMove); + } + } + } + return moves; + } + + doClick(square) { + if (isNaN(square[0])) return null; + // If subTurn == 2 && square is empty && !underCheck, then drop + if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) { + const color = this.turn; + const tMove = new Move({ + appear: [ + new PiPo({ + x: square[0], + y: square[1], + c: color, + p: V.KNIGHT + }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + this.play(tMove); + const moveOk = !this.underCheck(color); + this.undo(tMove); + if (moveOk) return tMove; + } + return null; + } + + play(move) { + move.flags = JSON.stringify(this.aggregateFlags()); + if (move.appear.length > 0) { + // Usual case or knight landing + if (move.vanish.length > 0) this.epSquares.push(this.getEpSquare(move)); + else this.subTurn = 1; + this.turn = V.GetOppCol(this.turn); + this.movesCount++; + V.PlayOnBoard(this.board, move); + if (move.vanish.length > 0) this.postPlay(move); + } + else { + // "king capture" + this.subTurn = 2; + this.knightFlags[this.turn == 'w' ? 0 : 1] = false; + } + } + + undo(move) { + this.disaggregateFlags(JSON.parse(move.flags)); + if (move.appear.length > 0) { + if (move.vanish.length > 0) this.epSquares.pop(); + else this.subTurn = 2; + this.turn = V.GetOppCol(this.turn); + this.movesCount--; + V.UndoOnBoard(this.board, move); + if (move.vanish.length > 0) this.postUndo(move); + } + else this.subTurn = 1; + } + + getComputerMove() { + let moves = this.getAllValidMoves(); + if (moves.length == 0) return null; + // Custom "search" at depth 1 (for now. TODO?) + const maxeval = V.INFINITY; + const color = this.turn; + const initEval = this.evalPosition(); + moves.forEach(m => { + this.play(m); + m.eval = (color == "w" ? -1 : 1) * maxeval; + if (m.appear.length == 0) { + const moves2 = this.getAllValidMoves(); + m.next = moves2[0]; + moves2.forEach(m2 => { + this.play(m2); + const score = this.getCurrentScore(); + let mvEval = 0; + if (["1-0", "0-1"].includes(score)) + mvEval = (score == "1-0" ? 1 : -1) * maxeval; + else if (score == "*") + // Add small fluctuations to avoid dropping pieces always on the + // first square available. + mvEval = initEval + 0.05 - Math.random() / 10; + if ( + (color == 'w' && mvEval > m.eval) || + (color == 'b' && mvEval < m.eval) + ) { + m.eval = mvEval; + m.next = m2; + } + this.undo(m2); + }); + } + else { + const score = this.getCurrentScore(); + if (score != "1/2") { + if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval; + else m.eval = this.evalPosition(); + } + } + this.undo(m); + }); + moves.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++) + candidates.push(i); + const mIdx = candidates[randInt(candidates.length)]; + if (!moves[mIdx].next) return moves[mIdx]; + const move2 = moves[mIdx].next; + delete moves[mIdx]["next"]; + return [moves[mIdx], move2]; + } + + getNotation(move) { + if (move.vanish.length > 0) + return super.getNotation(move); + if (move.appear.length == 0) + // "king capture" + return "-"; + // Knight landing: + return "N@" + V.CoordsToSquare(move.end); + } +}; diff --git a/client/src/variants/Takenmake.js b/client/src/variants/Takenmake.js index 72268636..6cea420c 100644 --- a/client/src/variants/Takenmake.js +++ b/client/src/variants/Takenmake.js @@ -131,7 +131,7 @@ export class TakenmakeRules extends ChessRules { this.kingPos[c][0] = move.appear[0].x; this.kingPos[c][1] = move.appear[0].y; } - super.updateCastleFlags(move, piece); + super.updateCastleFlags(move, piece, c); } undo(move) { diff --git a/client/src/variants/Teleport.js b/client/src/variants/Teleport.js index 6cf14a80..eadaf13b 100644 --- a/client/src/variants/Teleport.js +++ b/client/src/variants/Teleport.js @@ -135,13 +135,6 @@ export class TeleportRules extends ChessRules { return super.underCheck(color); } - getCurrentScore() { - if (this.subTurn == 2) - // Move not over - return "*"; - return super.getCurrentScore(); - } - doClick(square) { if (isNaN(square[0])) return null; // If subTurn == 2 && square is empty && !underCheck, then teleport @@ -228,28 +221,8 @@ export class TeleportRules extends ChessRules { } } } - else { - // Normal move - const firstRank = (c == "w" ? V.size.x - 1 : 0); - const oppCol = V.GetOppCol(c); - const oppFirstRank = V.size.x - 1 - firstRank; - if (move.vanish[0].p == V.KING && move.appear.length > 0) - this.castleFlags[c] = [V.size.y, V.size.y]; - else if ( - move.start.x == firstRank && - this.castleFlags[c].includes(move.start.y) - ) { - const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1); - this.castleFlags[c][flagIdx] = V.size.y; - } - if ( - move.end.x == oppFirstRank && - this.castleFlags[oppCol].includes(move.end.y) - ) { - const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1); - this.castleFlags[oppCol][flagIdx] = V.size.y; - } - } + // Normal check: + super.updateCastleFlags(move, move.vanish[0].p, c); } undo(move) { diff --git a/server/db/populate.sql b/server/db/populate.sql index 60578f83..a44a3ab5 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -63,6 +63,7 @@ insert or ignore into Variants (name, description) values ('Koopa', 'Stun & kick pieces'), ('Koth', 'King of the Hill'), ('Losers', 'Get strong at self-mate'), + ('Madhouse', 'Rearrange enemy pieces'), ('Madrasi', 'Paralyzed pieces'), ('Magnetic', 'Laws of attraction'), ('Makruk', 'Thai Chess'), @@ -78,6 +79,7 @@ insert or ignore into Variants (name, description) values ('Parachute', 'Landing on the board'), ('Pawns', 'Reach the last rank'), ('Perfect', 'Powerful pieces'), + ('Pocketknight', 'Knight in pocket'), ('Racingkings', 'Kings cross the 8x8 board'), ('Rampage', 'Move under cover'), ('Rifle', 'Shoot pieces'), -- 2.44.0