From 78d64531113d4b5045ff588dd43f301a332ebae8 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Sat, 22 Feb 2020 23:45:13 +0100 Subject: [PATCH] Draft Recycle variant --- client/src/base_rules.js | 11 +- client/src/translations/en.js | 1 + client/src/translations/es.js | 1 + client/src/translations/fr.js | 1 + .../src/translations/rules/Crazyhouse/en.pug | 2 +- client/src/translations/rules/Recycle/en.pug | 29 ++ client/src/translations/rules/Recycle/es.pug | 32 ++ client/src/translations/rules/Recycle/fr.pug | 32 ++ client/src/variants/Antiking.js | 1 - client/src/variants/Baroque.js | 4 +- client/src/variants/Crazyhouse.js | 8 +- client/src/variants/Enpassant.js | 70 ++--- client/src/variants/Recycle.js | 278 ++++++++++++++++++ server/db/populate.sql | 1 + 14 files changed, 418 insertions(+), 53 deletions(-) create mode 100644 client/src/translations/rules/Recycle/en.pug create mode 100644 client/src/translations/rules/Recycle/es.pug create mode 100644 client/src/translations/rules/Recycle/fr.pug create mode 100644 client/src/variants/Recycle.js diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 5bf47f76..7e4057bb 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -968,6 +968,8 @@ export const ChessRules = class ChessRules { // After move is played, update variables + flags updateVariables(move) { let piece = undefined; + // TODO: update variables before move is played, and just use this.turn ? + // (doesn't work in general, think MarseilleChess) let c = undefined; if (move.vanish.length >= 1) { // Usual case, something is moved @@ -978,9 +980,8 @@ export const ChessRules = class ChessRules { piece = move.appear[0].p; c = move.appear[0].c; } - if (c == "c") { - //if (!["w","b"].includes(c)) - // 'c = move.vanish[0].c' doesn't work for Checkered + if (!['w','b'].includes(c)) { + // Checkered, for example c = V.GetOppCol(this.turn); } const firstRank = c == "w" ? V.size.x - 1 : 0; @@ -1265,10 +1266,10 @@ export const ChessRules = class ChessRules { // Capture const startColumn = V.CoordToColumn(move.start.y); notation = startColumn + "x" + finalSquare; - } //no capture + } else notation = finalSquare; if (move.appear.length > 0 && move.appear[0].p != V.PAWN) - //promotion + // Promotion notation += "=" + move.appear[0].p.toUpperCase(); return notation; } diff --git a/client/src/translations/en.js b/client/src/translations/en.js index f44b82f5..93a0865f 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -143,6 +143,7 @@ export const translations = { "Lose all pieces": "Lose all pieces", "Move twice": "Move twice", "Pawns move diagonally": "Pawns move diagonally", + "Reuse pieces": "Reuse pieces", "Reverse captures": "Reverse captures", "Shared pieces": "Shared pieces", "Standard rules": "Standard rules" diff --git a/client/src/translations/es.js b/client/src/translations/es.js index ea576c17..792a208c 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -143,6 +143,7 @@ export const translations = { "Lose all pieces": "Perder todas las piezas", "Move twice": "Mover dos veces", "Pawns move diagonally": "Peones se mueven en diagonal", + "Reuse pieces": "Reutilizar piezas", "Reverse captures": "Capturas invertidas", "Shared pieces": "Piezas compartidas", "Standard rules": "Reglas estandar" diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 0f09a092..a79597b8 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -143,6 +143,7 @@ export const translations = { "Lose all pieces": "Perdez toutes les pièces", "Move twice": "Jouer deux coups", "Pawns move diagonally": "Les pions vont en diagonale", + "Reuse pieces": "Réutiliser les pièces", "Reverse captures": "Captures inversées", "Shared pieces": "Pièces partagées", "Standard rules": "Règles usuelles" diff --git a/client/src/translations/rules/Crazyhouse/en.pug b/client/src/translations/rules/Crazyhouse/en.pug index fee3cd93..adb00340 100644 --- a/client/src/translations/rules/Crazyhouse/en.pug +++ b/client/src/translations/rules/Crazyhouse/en.pug @@ -27,5 +27,5 @@ h3 More information p | This variant is very popular, a possible starting point is - a(href="https://www.chessvariants.com/other.dir/crazyhouse.html") lichess.org + a(href="https://lichess.org/variant/crazyhouse") lichess.org | . diff --git a/client/src/translations/rules/Recycle/en.pug b/client/src/translations/rules/Recycle/en.pug new file mode 100644 index 00000000..b7754dc1 --- /dev/null +++ b/client/src/translations/rules/Recycle/en.pug @@ -0,0 +1,29 @@ +p.boxed + | Capturing your own pieces is possible: captured units + | can be landed anywhere instead of moving a piece. + +p + | Orthodox rules apply, with only one change: + | at each turn you can choose to eat one of your pieces, augmenting your + | "reserve" of waiting pieces with this figure. At every move, you may + | choose to land one of your reserve pieces anywhere on the board + | (except first and last ranks for pawns), instead of playing a regular move. + +p. + Move notation: an arobase '@' indicate piece landing. For example B@d4 means + a bishop rebirth on d4 square, and @f3 is a pawn landing on f3. + +figure.diagram-container + .diagram + | fen:rnbqkb1r/ppppnppp/8/4N3/3p4/8/PPPPPPPP/1RBQKBNR: + figcaption After 1.Rxb1 Nxe7 2.N@e5 @d4 + +p. + Note: when a pawn reaches the 8th(1st) rank, it will get removed from the board. + It will not be replaced by another piece. It does not promote. + +h3 Source + +p + a(href="https://www.chessvariants.com/difftaking.dir/recyclechess.html") Recycle chess + |  on chessvariants.com. diff --git a/client/src/translations/rules/Recycle/es.pug b/client/src/translations/rules/Recycle/es.pug new file mode 100644 index 00000000..775c57f0 --- /dev/null +++ b/client/src/translations/rules/Recycle/es.pug @@ -0,0 +1,32 @@ +p.boxed + | Puedes capturar tus propias piezas: estas pueden ser + | colocándo a cualquier casilla en lugar de moverlo. + +p. + Se aplican reglas ortodoxas, con un solo cambio: + cada turno puedes elegir comer una de tus propias piezas, + aumentando su "reserva" de piezas de espera con esta unidad. + Con cada movimiento puedes elegir poner una de estas piezas en reserva + (en cualquier lugar, excepto las filas 1 y 8 para los peones) en lugar + de jugar una jugada habitual. + +p. + Notación: un signo '@' indica la llegada de una pieza de reserva. + Por ejemplo, B@d4 significa que un alfil se mete en d4, y @f3 significa + renacimiento de peón en f3. + +figure.diagram-container + .diagram + | fen:rnbqkb1r/ppppnppp/8/4N3/3p4/8/PPPPPPPP/1RBQKBNR: + figcaption Después de 1.Rxb1 Nxe7 2.N@e5 @d4 + +p. + Nota: cuando un peón alcanza la 8va(1ra) fila, se retira del juego. + No se reemplaza por otra pieza. No hay promoción + +h3 Fuente + +p + | La + a(href="https://www.chessvariants.com/difftaking.dir/recyclechess.html") variante Reciclaje + |  en chessvariants.com. diff --git a/client/src/translations/rules/Recycle/fr.pug b/client/src/translations/rules/Recycle/fr.pug new file mode 100644 index 00000000..679fd79d --- /dev/null +++ b/client/src/translations/rules/Recycle/fr.pug @@ -0,0 +1,32 @@ +p.boxed + | Vous pouvez capturer vos propres pièces : celles-ci peuvent ensuite être + | parachutées n'importe où au lieu d'effectuer un déplacement. + +p. + Les règles orthodoxes s'appliquent, avec un seul changement : + à chaque tour vous pouvez choisir de manger une de vos propres pièces, + augmentant votre "réserve" de pièces en attente avec cette unité. + À chaque coup vous pouvez choisir de poser une de ces pièces en réserve + (n'importe où, sauf rangées 1 et 8 pour les pions) au lieu de jouer un + coup habituel. + +p. + Notation : une arobase '@' indique l'arrivée d'une pièce de réserve. + Par exemple, B@d4 signifie un parachutage de fou en d4, et @f3 une + renaissance de pion en f3. + +figure.diagram-container + .diagram + | fen:rnbqkb1r/ppppnppp/8/4N3/3p4/8/PPPPPPPP/1RBQKBNR: + figcaption After 1.Rxb1 Nxe7 2.N@e5 @d4 + +p. + Note : quand un pion atteint la 8eme(1ere) rangée, il est retiré du jeu. + Il n'est pas remplacé par une autre pièce. Il n'y a pas de promotion. + +h3 Source + +p + | La + a(href="https://www.chessvariants.com/difftaking.dir/recyclechess.html") variante Recyclage + |  sur chessvariants.com. diff --git a/client/src/variants/Antiking.js b/client/src/variants/Antiking.js index a13247a1..3b1b36e3 100644 --- a/client/src/variants/Antiking.js +++ b/client/src/variants/Antiking.js @@ -131,7 +131,6 @@ export const VariantRules = class AntikingRules extends ChessRules { getCurrentScore() { if (this.atLeastOneMove()) - // game not over return "*"; const color = this.turn; diff --git a/client/src/variants/Baroque.js b/client/src/variants/Baroque.js index 79fc73ce..5687e22c 100644 --- a/client/src/variants/Baroque.js +++ b/client/src/variants/Baroque.js @@ -203,7 +203,6 @@ export const VariantRules = class BaroqueRules extends ChessRules { return moves; } - // Long-leaper getKnightCaptures(startSquare, byChameleon) { // Look in every direction for captures const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); @@ -271,6 +270,7 @@ export const VariantRules = class BaroqueRules extends ChessRules { return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq)); } + // Chameleon getPotentialBishopMoves([x, y]) { let moves = super .getPotentialQueenMoves([x, y]) @@ -297,7 +297,6 @@ export const VariantRules = class BaroqueRules extends ChessRules { return moves; } - // Withdrawer addQueenCaptures(moves, byChameleon) { if (moves.length == 0) return; const [x, y] = [moves[0].start.x, moves[0].start.y]; @@ -341,6 +340,7 @@ export const VariantRules = class BaroqueRules extends ChessRules { }); } + // Withdrawer getPotentialQueenMoves(sq) { let moves = super.getPotentialQueenMoves(sq); this.addQueenCaptures(moves); diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js index 89b6f830..d7d82b8c 100644 --- a/client/src/variants/Crazyhouse.js +++ b/client/src/variants/Crazyhouse.js @@ -208,8 +208,9 @@ export const VariantRules = class CrazyhouseRules extends ChessRules { } static get SEARCH_DEPTH() { + // High branching factor return 2; - } //high branching factor + } evalPosition() { let evaluation = super.evalPosition(); @@ -229,9 +230,4 @@ export const VariantRules = class CrazyhouseRules extends ChessRules { move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : ""; return piece + "@" + V.CoordsToSquare(move.end); } - - getLongNotation(move) { - if (move.vanish.length > 0) return super.getLongNotation(move); - return "@" + V.CoordsToSquare(move.end); - } }; diff --git a/client/src/variants/Enpassant.js b/client/src/variants/Enpassant.js index 50f97387..374a620c 100644 --- a/client/src/variants/Enpassant.js +++ b/client/src/variants/Enpassant.js @@ -74,53 +74,47 @@ export const VariantRules = class EnpassantRules extends ChessRules { let moves = []; const [sizeX, sizeY] = [V.size.x, V.size.y]; const shiftX = color == "w" ? -1 : 1; - const firstRank = color == "w" ? sizeX - 1 : 0; const startRank = color == "w" ? sizeX - 2 : 1; const lastRank = color == "w" ? 0 : sizeX - 1; - const pawnColor = this.getColor(x, y); //can be different for checkered - // NOTE: next condition is generally true (no pawn on last rank) - if (x + shiftX >= 0 && x + shiftX < sizeX) { - const finalPieces = - x + shiftX == lastRank - ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] - : [V.PAWN]; - // One square forward - if (this.board[x + shiftX][y] == V.EMPTY) { + const finalPieces = + x + shiftX == lastRank + ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] + : [V.PAWN]; + // One square forward + if (this.board[x + shiftX][y] == V.EMPTY) { + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y], { + c: color, + p: piece + }) + ); + } + if ( + x == startRank && + this.board[x + 2 * shiftX][y] == V.EMPTY + ) { + // Two squares jump + moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); + } + } + // Captures + for (let shiftY of [-1, 1]) { + if ( + y + shiftY >= 0 && + y + shiftY < sizeY && + this.board[x + shiftX][y + shiftY] != V.EMPTY && + this.canTake([x, y], [x + shiftX, y + shiftY]) + ) { for (let piece of finalPieces) { moves.push( - this.getBasicMove([x, y], [x + shiftX, y], { - c: pawnColor, + this.getBasicMove([x, y], [x + shiftX, y + shiftY], { + c: color, p: piece }) ); } - // Next condition because pawns on 1st rank can generally jump - if ( - [startRank, firstRank].includes(x) && - this.board[x + 2 * shiftX][y] == V.EMPTY - ) { - // Two squares jump - moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); - } - } - // Captures - for (let shiftY of [-1, 1]) { - if ( - y + shiftY >= 0 && - y + shiftY < sizeY && - this.board[x + shiftX][y + shiftY] != V.EMPTY && - this.canTake([x, y], [x + shiftX, y + shiftY]) - ) { - for (let piece of finalPieces) { - moves.push( - this.getBasicMove([x, y], [x + shiftX, y + shiftY], { - c: pawnColor, - p: piece - }) - ); - } - } } } diff --git a/client/src/variants/Recycle.js b/client/src/variants/Recycle.js new file mode 100644 index 00000000..d99228cd --- /dev/null +++ b/client/src/variants/Recycle.js @@ -0,0 +1,278 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; + +export const VariantRules = class RecycleRules extends ChessRules { + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 5) Check reserves + if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{10,10}$/)) + return false; + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign(ChessRules.ParseFen(fen), { + reserve: fenParts[5], + }); + } + + static GenRandInitFen() { + return ChessRules.GenRandInitFen() + " 0000000000"; + } + + getFen() { + return ( + super.getFen() + " " + this.getReserveFen() + ); + } + + getReserveFen() { + let counts = new Array(10); + for ( + let i = 0; + i < V.PIECES.length - 1; + i++ //-1: no king reserve + ) { + counts[i] = this.reserve["w"][V.PIECES[i]]; + counts[5 + i] = this.reserve["b"][V.PIECES[i]]; + } + return counts.join(""); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + const fenParsed = V.ParseFen(fen); + // Also init reserves (used by the interface to show landable pieces) + this.reserve = { + w: { + [V.PAWN]: parseInt(fenParsed.reserve[0]), + [V.ROOK]: parseInt(fenParsed.reserve[1]), + [V.KNIGHT]: parseInt(fenParsed.reserve[2]), + [V.BISHOP]: parseInt(fenParsed.reserve[3]), + [V.QUEEN]: parseInt(fenParsed.reserve[4]) + }, + b: { + [V.PAWN]: parseInt(fenParsed.reserve[5]), + [V.ROOK]: parseInt(fenParsed.reserve[6]), + [V.KNIGHT]: parseInt(fenParsed.reserve[7]), + [V.BISHOP]: parseInt(fenParsed.reserve[8]), + [V.QUEEN]: parseInt(fenParsed.reserve[9]) + } + }; + } + + getColor(i, j) { + if (i >= V.size.x) return i == V.size.x ? "w" : "b"; + return this.board[i][j].charAt(0); + } + + getPiece(i, j) { + if (i >= V.size.x) return V.RESERVE_PIECES[j]; + return this.board[i][j].charAt(1); + } + + // Used by the interface: + getReservePpath(color, index) { + return color + V.RESERVE_PIECES[index]; + } + + // Ordering on reserve pieces + static get RESERVE_PIECES() { + return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]; + } + + getReserveMoves([x, y]) { + const color = this.turn; + const p = V.RESERVE_PIECES[y]; + if (this.reserve[color][p] == 0) return []; + let moves = []; + const pawnShift = p == V.PAWN ? 1 : 0; + for (let i = pawnShift; i < V.size.x - pawnShift; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] == V.EMPTY) { + let mv = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: color, + p: p + }) + ], + vanish: [], + start: { x: x, y: y }, //a bit artificial... + end: { x: i, y: j } + }); + moves.push(mv); + } + } + } + return moves; + } + + getPotentialMovesFrom([x, y]) { + if (x >= V.size.x) { + // Reserves, outside of board: x == sizeX(+1) + return this.getReserveMoves([x, y]); + } + // Standard moves + return super.getPotentialMovesFrom([x, y]); + } + + getPotentialPawnMoves([x, y]) { + const color = this.turn; + let moves = []; + const [sizeX, sizeY] = [V.size.x, V.size.y]; + const shiftX = color == "w" ? -1 : 1; + const startRank = color == "w" ? sizeX - 2 : 1; + const lastRank = color == "w" ? 0 : sizeX - 1; + + // One square forward + if (this.board[x + shiftX][y] == V.EMPTY) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y]) + ); + // Next condition because pawns on 1st rank can generally jump + if ( + x == startRank && + this.board[x + 2 * shiftX][y] == V.EMPTY + ) { + // Two squares jump + moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); + } + } + // Captures + for (let shiftY of [-1, 1]) { + if ( + y + shiftY >= 0 && + y + shiftY < sizeY && + this.board[x + shiftX][y + shiftY] != V.EMPTY && + this.canTake([x, y], [x + shiftX, y + shiftY]) + ) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y + shiftY]) + ); + } + } + + // En passant + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep - 1]; //always at least one element + if ( + !!epSquare && + epSquare.x == x + shiftX && + Math.abs(epSquare.y - y) == 1 + ) { + let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]); + enpassantMove.vanish.push({ + x: x, + y: epSquare.y, + p: "p", + c: this.getColor(x, epSquare.y) + }); + moves.push(enpassantMove); + } + + // Post-processing: remove falling pawns + if (x + shiftX == lastRank) { + moves.forEach(m => { + m.appear.pop(); + }); + } + + return moves; + } + + getAllValidMoves() { + let moves = super.getAllValidMoves(); + const color = this.turn; + for (let i = 0; i < V.RESERVE_PIECES.length; i++) + moves = moves.concat( + this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i]) + ); + return this.filterValid(moves); + } + + atLeastOneMove() { + if (!super.atLeastOneMove()) { + // Search one reserve move + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + let moves = this.filterValid( + this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i]) + ); + if (moves.length > 0) return true; + } + return false; + } + return true; + } + + canTake([x1, y1], [x2, y2]) { + // Self-captures allowed, except for the king: + return this.getPiece(x2, y2) != V.KING; + } + + updateVariables(move) { + super.updateVariables(move); + if (move.vanish.length == 2 && move.appear.length == 2) return; //skip castle + const color = V.GetOppCol(this.turn); + if (move.vanish.length == 0) { + this.reserve[color][move.appear[0].p]--; + return; + } + else if (move.vanish.length == 2 && move.vanish[1].c == color) { + // Self-capture + this.reserve[color][move.vanish[1].p]++; + } + } + + unupdateVariables(move) { + super.unupdateVariables(move); + if (move.vanish.length == 2 && move.appear.length == 2) return; + const color = this.turn; + if (move.vanish.length == 0) { + this.reserve[color][move.appear[0].p]++; + return; + } + else if (move.vanish.length == 2 && move.vanish[1].c == color) { + this.reserve[color][move.vanish[1].p]--; + } + } + + static get SEARCH_DEPTH() { + return 2; + } + + evalPosition() { + let evaluation = super.evalPosition(); + // Add reserves: + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + const p = V.RESERVE_PIECES[i]; + evaluation += this.reserve["w"][p] * V.VALUES[p]; + evaluation -= this.reserve["b"][p] * V.VALUES[p]; + } + return evaluation; + } + + getNotation(move) { + const finalSquare = V.CoordsToSquare(move.end); + if (move.vanish.length > 0) { + if (move.appear.length > 0) { + // Standard move + return super.getNotation(move); + } else { + // Pawn fallen: capturing or not + let res = ""; + if (move.vanish.length == 2) + res += V.CoordToColumn(move.start.y) + "x"; + return res + finalSquare; + } + } + // Rebirth: + const piece = + move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : ""; + return piece + "@" + V.CoordsToSquare(move.end); + } +}; diff --git a/server/db/populate.sql b/server/db/populate.sql index a9386d6a..6a847d28 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -18,6 +18,7 @@ insert or ignore into Variants (name,description) values ('Losers', 'Lose all pieces'), ('Magnetic', 'Laws of attraction'), ('Marseille', 'Move twice'), + ('Recycle', 'Reuse pieces'), ('Upsidedown', 'Board upside down'), ('Wildebeest', 'Balanced sliders & leapers'), ('Zen', 'Reverse captures'); -- 2.44.0