From 4f3a08234f754abcb74f369067f960a8269557a3 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 14 Jan 2021 20:10:14 +0100 Subject: [PATCH] Add (real) Football chess --- TODO | 7 +- client/public/images/pieces/Football/SOURCE | 1 + client/public/images/pieces/Football/ball.svg | 215 ++++++++++ client/src/translations/en.js | 3 +- client/src/translations/es.js | 3 +- client/src/translations/fr.js | 3 +- client/src/translations/rules/Football/en.pug | 61 ++- client/src/translations/rules/Football/es.pug | 65 ++- client/src/translations/rules/Football/fr.pug | 64 ++- .../rules/{Squatter => Squatter1}/en.pug | 0 .../rules/{Squatter => Squatter1}/es.pug | 0 .../rules/{Squatter => Squatter1}/fr.pug | 0 .../src/translations/rules/Squatter2/en.pug | 21 + .../src/translations/rules/Squatter2/es.pug | 21 + .../src/translations/rules/Squatter2/fr.pug | 21 + client/src/translations/variants/en.pug | 3 +- client/src/translations/variants/es.pug | 3 +- client/src/translations/variants/fr.pug | 3 +- client/src/variants/Ball.js | 12 +- client/src/variants/Football.js | 377 ++++++++++++++++-- .../variants/{Squatter.js => Squatter1.js} | 2 +- client/src/variants/Squatter2.js | 88 ++++ server/db/populate.sql | 3 +- 23 files changed, 883 insertions(+), 93 deletions(-) create mode 100644 client/public/images/pieces/Football/SOURCE create mode 100644 client/public/images/pieces/Football/ball.svg rename client/src/translations/rules/{Squatter => Squatter1}/en.pug (100%) rename client/src/translations/rules/{Squatter => Squatter1}/es.pug (100%) rename client/src/translations/rules/{Squatter => Squatter1}/fr.pug (100%) create mode 100644 client/src/translations/rules/Squatter2/en.pug create mode 100644 client/src/translations/rules/Squatter2/es.pug create mode 100644 client/src/translations/rules/Squatter2/fr.pug rename client/src/variants/{Squatter.js => Squatter1.js} (98%) create mode 100644 client/src/variants/Squatter2.js diff --git a/TODO b/TODO index 4bf0b488..53cace86 100644 --- a/TODO +++ b/TODO @@ -3,9 +3,6 @@ Embedded rules language not updated when language is set (in Analyse, Game and P If new live game starts in background, "new game" notify OK but not first move. NEW VARIANTS: -https://www.chessvariants.com/crossover.dir/football.html - ballon vers n'importe laquelle de nos pièces = 'j'ai terminé mes passes, à toi' - Also: rename current Football into Squatter2 (and Squatter into Squatter1) https://www.pychess.org/variant/shogun https://www.chessvariants.com/incinf.dir/bario.html https://www.chessvariants.com/index/listcomments.php?order=DESC&itemid=Bario @@ -13,6 +10,6 @@ https://www.chessvariants.com/incinf.dir/bario.html https://le-cdn.website-editor.net/20ef5f800ea646c29f6852cfc5ceda07/dms3rep/multi/opt/BAR028-e15a849c-960w.jpg Non-chess: ( won't add draughts: https://lidraughts.org/ ) -Avalam, Fanorona https://fr.wikipedia.org/wiki/Fanorona +Gomoku, Konane +Fanorona https://fr.wikipedia.org/wiki/Fanorona Yoté https://fr.wikipedia.org/wiki/Yot%C3%A9 http://www.zillionsofgames.com/cgi-bin/zilligames/submissions.cgi/92187?do=show;id=960 -Qoridor: reserve moves to place the walls, convention from left / from top diff --git a/client/public/images/pieces/Football/SOURCE b/client/public/images/pieces/Football/SOURCE new file mode 100644 index 00000000..3c339cb7 --- /dev/null +++ b/client/public/images/pieces/Football/SOURCE @@ -0,0 +1 @@ +https://commons.wikimedia.org/wiki/File:Soccer_ball.svg diff --git a/client/public/images/pieces/Football/ball.svg b/client/public/images/pieces/Football/ball.svg new file mode 100644 index 00000000..7b579186 --- /dev/null +++ b/client/public/images/pieces/Football/ball.svg @@ -0,0 +1,215 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 6d79e55a..bf58f253 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -293,7 +293,8 @@ export const translations = { "Shoot pieces": "Shoot pieces", "Spartan versus Persians": "Spartan versus Persians", "Squares disappear": "Squares disappear", - "Squat last rank": "Squat last rank", + "Squat last rank (v1)": "Squat last rank (v1)", + "Squat last rank (v2)": "Squat last rank (v2)", "Standard rules": "Standard rules", "Stun & kick pieces": "Stun & kick pieces", "Thai Chess (v1)": "Thai Chess (v1)", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index a50a9791..f9a9d99a 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -293,7 +293,8 @@ export const translations = { "Shoot pieces": "Tirar de las piezas", "Spartan versus Persians": "Espartanos contra Persas", "Squares disappear": "Las casillas desaparecen", - "Squat last rank": "Ocupa la última fila", + "Squat last rank (v1)": "Ocupa la última fila (v1)", + "Squat last rank (v2)": "Ocupa la última fila (v2)", "Standard rules": "Reglas estandar", "Stun & kick pieces": "Aturdir & patear piezas", "Thai Chess (v1)": "Ajedrez tailandés (v1)", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 925f1412..101436d6 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -293,7 +293,8 @@ export const translations = { "Shoot pieces": "Tirez sur les pièces", "Spartan versus Persians": "Spartiates contre Perses", "Squares disappear": "Les cases disparaissent", - "Squat last rank": "Occupez la dernière rangée", + "Squat last rank (v1)": "Occupez la dernière rangée (v1)", + "Squat last rank (v2)": "Occupez la dernière rangée (v2)", "Standard rules": "Règles usuelles", "Stun & kick pieces": "Étourdissez & frappez les pièces", "Thai Chess": "Échecs thai", diff --git a/client/src/translations/rules/Football/en.pug b/client/src/translations/rules/Football/en.pug index 34fb52b9..4f20b61f 100644 --- a/client/src/translations/rules/Football/en.pug +++ b/client/src/translations/rules/Football/en.pug @@ -1,21 +1,54 @@ -p.boxed. - Win by playing a move on the middle of last rank. - The king has no royal status. +p.boxed + | Pieces don't capture but can kick a ball. + | The object of the game is to send the ball into the enemy goal. p. - The goal for White (resp. Black) are the squares d8 and e8 (resp. d1 and - e1) in the middle of the opposing rank. The first player to settle a - piece there wins, even if it is attacked. + At each turn, a player can first make a normal (non capturing) move. + If none of his pieces are adjacent to the ball, such move is mandatory. + But, if any of his pieces stand next to the ball he may then or right + away kick it, if the step defined by the arrow piece --> ball is + compatible with the piece's movement. + +p. + To play a kick, click on the ball and then on the desired square. + The knight send the ball at any knight-step away from its initial position, + except on squares adjacent to the knight. + However, when the ball is in a corner an exception to this rule is allowed: + on the following diagram "kick from g1 to h3" is allowed". figure.diagram-container - .diagram - | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3: - figcaption 1...Nc3 and 2...N(x)d1# wins for Black. + .diagram.diag12 + | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/9/7N1/RNB2K1Ba: + .diagram.diag22 + | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/7a1/7N1/RNB2K1B1: + figcaption Before and after (kick) g1-h3 + +h4 Some restrictions. + +p. + To avoid infinite cycles, each piece can kick the ball at most + once during a turn. + +p Pieces cannot stand in any goal square. -h3 Source +p The ball may never reach or cross the goal horizontally. + +h4 Complete a move on the interface. + +p. + If at the end of an initial or intermediate move one of your pieces is + adjacent to the ball, you'll need to "capture" any of your pieces + with the ball to indicate that the move is over. + +h3 More information p - | This variant is mentioned - a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football") - | on this page - | . It isn't specified if the winning piece needs to be safe from attacks. + | See the + a(href="https://www.chessvariants.com/crossover.dir/football.html") + | chessvariants page + | , for example, and also the + a(href="https://www.jsbeasley.co.uk/encyc.htm") + | Classified Encyclopedia of Chess Variants + | . + +p Inventor: Joseph Boyer (1951) diff --git a/client/src/translations/rules/Football/es.pug b/client/src/translations/rules/Football/es.pug index 2ca6e47a..1f346b60 100644 --- a/client/src/translations/rules/Football/es.pug +++ b/client/src/translations/rules/Football/es.pug @@ -1,21 +1,58 @@ -p.boxed. - Gana haciendo un movimiento en el centro de la última fila. - El rey no tiene estatus real. +p.boxed + | Las piezas no capturan pero pueden golpear una bola. + | El objetivo del juego es enviar el balón a la portería contraria. p. - El objetivo de las blancas (resp. negras) son las casillas d8 y e8 - (resp. d1 y e1) en el medio de la fila opuesta. - El primer jugador que instala una pieza ahí gana, incluso si es atacada. + En cada ronda, un jugador puede realizar primero un movimiento normal + (sin captura). Si ninguna de sus piezas está al lado de la bola, tal + movimiento es obligatorio. + Pero, si una de sus piezas toque la pelota, entonces puede golpearla + después de un desplazamiento inicial o inmediatamente, siempre que el paso + definido por la flecha pieza --> globo es compatible con los + movimientos de este último. + +p. + Para mover la bola, haga clic en ella y luego en la casilla de llegada. + El caballo puede enviar la pelota a cualquier casilla dentro + distancia del saltador desde la posición inicial del balón, excepto en las + casillas adyacentes al caballo. + Sin embargo, cuando la pelota está en una esquina permitimos una excepción + a esto regla: en el siguiente diagrama, el caballo puede enviar la pelota + de g1 a h3 por ejemplo. figure.diagram-container - .diagram - | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3: - figcaption 1...Nc3 y 2...N(x)d1# gana para las negras. + .diagram.diag12 + | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/9/7N1/RNB2K1Ba: + .diagram.diag22 + | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/7a1/7N1/RNB2K1B1: + figcaption Antes y después del golpe g1-h3 + +h4 Algunas restricciones + +p. + Para evitar ciclos interminables, cada pieza puede golpear la pelota + como máximo una vez durante un turno. + +p Ninguna pieza puede estar en una casilla portería. -h3 Fuente +p El balón nunca puede alcanzar o cruzar la portería horizontalmente. + +h4 Completa una jugada en la interfaz. + +p. + Si al final de un movimiento inicial o intermedio una de tus piezas es al + lado de la bola, entonces tienes que "capturar" una de tus piezas con la + bola para indicar el final de la ronda. + +h3 Más información p - | Esta variante se menciona - a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football") - | en esta pagina - | . No está claro si la pieza debe estar a salvo de los ataques. + | Ver la + a(href="https://www.chessvariants.com/crossover.dir/football.html") + | página chessvariants.com + | , por ejemplo, y también la + a(href="https://www.jsbeasley.co.uk/encyc.htm") + | Classified Encyclopedia of Chess Variants + | . + +p Inventor: Joseph Boyer (1951) diff --git a/client/src/translations/rules/Football/fr.pug b/client/src/translations/rules/Football/fr.pug index 3af7b57d..f06bc90c 100644 --- a/client/src/translations/rules/Football/fr.pug +++ b/client/src/translations/rules/Football/fr.pug @@ -1,21 +1,57 @@ -p.boxed. - Gagnez en jouant un coup au centre de la dernière rangée. - Le roi n'a pas de statut royal. +p.boxed + | Les pièces ne capturent pas mais peuvent frapper un ballon. + | L'objectif du jeu est d'envoyer le ballon dans le but adverse. p. - Le but des blancs (resp. des noirs) sont les cases d8 et e8 (resp. d1 et e1) - au milieu de la rangée opposée. Le premier joueur à y installer une pièce - gagne, même si celle-ci est attaquée. + À chaque tour, un jour peut d'abord effectuer un coup normal (non capturant). + Si aucune de ses pièces n'est à côté du ballon, un tel coup est obligatoire. + Mais, si une de ses pièces touche le ballon, alors il peut le frapper soit + après un déplacement initial soit tout de suite, à condition que le pas + défini par la flèche pièce --> ballon soit compatible avec les + mouvements de cette dernière. + +p. + Pour déplacer la balle, cliquez dessus puis sur la case d'arrivée. + Le cavalier peut envoyer le ballon vers n'importe quelle case située à + distance de cavalier de la position initiale du ballon, sauf sur les + cases adjacentes au cavalier. + Cependant, quand la balle est dans un coin on autorise une exception à cette + règle : sur le diagramme suivant, le cavalier peut envoyer le ballon + de g1 à h3 par exemple. figure.diagram-container - .diagram - | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3: - figcaption 1...Nc3 et 2...N(x)d1# gagne pour les noirs. + .diagram.diag12 + | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/9/7N1/RNB2K1Ba: + .diagram.diag22 + | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/7a1/7N1/RNB2K1B1: + figcaption Avant et après la frappe g1-h3 + +h4 Quelques restrictions + +p. + Afin d'éviter des cycles infinis, chaque pièce peut frapper le ballon + au plus une fois pendant un tour. + +p Aucune pièce ne peut se trouver sur une case but. -h3 Source +p La balle ne peut jamais atteindre ou traverser le but horizontalement. + +h4 Compléter un coup sur l'interface. + +p. + Si à la fin d'un coup initial ou intermédiaire une de vos pièces est à côté + du ballon, alors vous devez "capturer" une de vos pièces avec la balle + pour indiquer la fin du tour. + +h3 Plus d'information p - | Cette variante est mentionnée - a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football") - | sur cette page - | . Il n'est pas précisé si la pièce doit être à l'abri des attaques. + | Voir la + a(href="https://www.chessvariants.com/crossover.dir/football.html") + | page chessvariants.com + | , par exemple, et également la + a(href="https://www.jsbeasley.co.uk/encyc.htm") + | Classified Encyclopedia of Chess Variants + | . + +p Inventeur : Joseph Boyer (1951) diff --git a/client/src/translations/rules/Squatter/en.pug b/client/src/translations/rules/Squatter1/en.pug similarity index 100% rename from client/src/translations/rules/Squatter/en.pug rename to client/src/translations/rules/Squatter1/en.pug diff --git a/client/src/translations/rules/Squatter/es.pug b/client/src/translations/rules/Squatter1/es.pug similarity index 100% rename from client/src/translations/rules/Squatter/es.pug rename to client/src/translations/rules/Squatter1/es.pug diff --git a/client/src/translations/rules/Squatter/fr.pug b/client/src/translations/rules/Squatter1/fr.pug similarity index 100% rename from client/src/translations/rules/Squatter/fr.pug rename to client/src/translations/rules/Squatter1/fr.pug diff --git a/client/src/translations/rules/Squatter2/en.pug b/client/src/translations/rules/Squatter2/en.pug new file mode 100644 index 00000000..34fb52b9 --- /dev/null +++ b/client/src/translations/rules/Squatter2/en.pug @@ -0,0 +1,21 @@ +p.boxed. + Win by playing a move on the middle of last rank. + The king has no royal status. + +p. + The goal for White (resp. Black) are the squares d8 and e8 (resp. d1 and + e1) in the middle of the opposing rank. The first player to settle a + piece there wins, even if it is attacked. + +figure.diagram-container + .diagram + | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3: + figcaption 1...Nc3 and 2...N(x)d1# wins for Black. + +h3 Source + +p + | This variant is mentioned + a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football") + | on this page + | . It isn't specified if the winning piece needs to be safe from attacks. diff --git a/client/src/translations/rules/Squatter2/es.pug b/client/src/translations/rules/Squatter2/es.pug new file mode 100644 index 00000000..2ca6e47a --- /dev/null +++ b/client/src/translations/rules/Squatter2/es.pug @@ -0,0 +1,21 @@ +p.boxed. + Gana haciendo un movimiento en el centro de la última fila. + El rey no tiene estatus real. + +p. + El objetivo de las blancas (resp. negras) son las casillas d8 y e8 + (resp. d1 y e1) en el medio de la fila opuesta. + El primer jugador que instala una pieza ahí gana, incluso si es atacada. + +figure.diagram-container + .diagram + | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3: + figcaption 1...Nc3 y 2...N(x)d1# gana para las negras. + +h3 Fuente + +p + | Esta variante se menciona + a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football") + | en esta pagina + | . No está claro si la pieza debe estar a salvo de los ataques. diff --git a/client/src/translations/rules/Squatter2/fr.pug b/client/src/translations/rules/Squatter2/fr.pug new file mode 100644 index 00000000..3af7b57d --- /dev/null +++ b/client/src/translations/rules/Squatter2/fr.pug @@ -0,0 +1,21 @@ +p.boxed. + Gagnez en jouant un coup au centre de la dernière rangée. + Le roi n'a pas de statut royal. + +p. + Le but des blancs (resp. des noirs) sont les cases d8 et e8 (resp. d1 et e1) + au milieu de la rangée opposée. Le premier joueur à y installer une pièce + gagne, même si celle-ci est attaquée. + +figure.diagram-container + .diagram + | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3: + figcaption 1...Nc3 et 2...N(x)d1# gagne pour les noirs. + +h3 Source + +p + | Cette variante est mentionnée + a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football") + | sur cette page + | . Il n'est pas précisé si la pièce doit être à l'abri des attaques. diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug index 66c14938..55c778d6 100644 --- a/client/src/translations/variants/en.pug +++ b/client/src/translations/variants/en.pug @@ -425,7 +425,8 @@ p Orthodox rules, but the goal is not checkmate (or not only). "Threechecks", "Kinglet", "Koth", - "Squatter" + "Squatter1", + "Squatter2" ] ul for v in varlist diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug index 39eda808..7a3dea34 100644 --- a/client/src/translations/variants/es.pug +++ b/client/src/translations/variants/es.pug @@ -435,7 +435,8 @@ p Reglas ortodoxas, pero el objetivo no es jaque mate (o no solo). "Threechecks", "Kinglet", "Koth", - "Squatter" + "Squatter1", + "Squatter2" ] ul for v in varlist diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug index 9cf10ed9..9673a53e 100644 --- a/client/src/translations/variants/fr.pug +++ b/client/src/translations/variants/fr.pug @@ -433,7 +433,8 @@ p Règles orthodoxes, mais le but n'est pas de mater (ou pas seulement). "Threechecks", "Kinglet", "Koth", - "Squatter" + "Squatter1", + "Squatter2" ] ul for v in varlist diff --git a/client/src/variants/Ball.js b/client/src/variants/Ball.js index 04ad1feb..5de871d7 100644 --- a/client/src/variants/Ball.js +++ b/client/src/variants/Ball.js @@ -36,7 +36,7 @@ export class BallRules extends ChessRules { } static get BALL() { - // Ball is already taken: + // 'b' is already taken: return "aa"; } @@ -95,17 +95,18 @@ export class BallRules extends ChessRules { const rows = position.split("/"); if (rows.length != V.size.x) return false; let pieces = { "w": 0, "b": 0 }; - const withBall = Object.keys(V.HAS_BALL_DECODE).concat([V.BALL]); + const withBall = Object.keys(V.HAS_BALL_DECODE).concat(['a']); let ballCount = 0; for (let row of rows) { let sumElts = 0; for (let i = 0; i < row.length; i++) { const lowerRi = row[i].toLowerCase(); if (V.PIECES.includes(lowerRi)) { - if (lowerRi != V.BALL) pieces[row[i] == lowerRi ? "b" : "w"]++; + if (lowerRi != 'a') pieces[row[i] == lowerRi ? "b" : "w"]++; if (withBall.includes(lowerRi)) ballCount++; sumElts++; - } else { + } + else { const num = parseInt(row[i], 10); if (isNaN(num)) return false; sumElts += num; @@ -337,7 +338,8 @@ export class BallRules extends ChessRules { }) ); } - } else if (mv.vanish[1].c == mv.vanish[0].c) { + } + else if (mv.vanish[1].c == mv.vanish[0].c) { // Pass the ball: the passing unit does not disappear mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0]))); mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p]; diff --git a/client/src/variants/Football.js b/client/src/variants/Football.js index 430e31ac..b1630eac 100644 --- a/client/src/variants/Football.js +++ b/client/src/variants/Football.js @@ -1,47 +1,55 @@ import { ChessRules } from "@/base_rules"; -import { SuicideRules } from "@/variants/Suicide"; +import { randInt } from "@/utils/alea"; export class FootballRules extends ChessRules { + static get HasEnpassant() { + return false; + } + static get HasFlags() { return false; } - static get PawnSpecs() { - return Object.assign( - {}, - ChessRules.PawnSpecs, - { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) } - ); + static get size() { + return { x: 9, y: 9 }; } static get Lines() { return [ // White goal: - [[0, 3], [0, 5]], + [[0, 4], [0, 5]], [[0, 5], [1, 5]], - [[1, 5], [1, 3]], - [[1, 3], [0, 3]], + [[1, 4], [0, 4]], // Black goal: - [[8, 3], [8, 5]], - [[8, 5], [7, 5]], - [[7, 5], [7, 3]], - [[7, 3], [8, 3]] + [[9, 4], [9, 5]], + [[9, 5], [8, 5]], + [[8, 4], [9, 4]] ]; } + static get BALL() { + // 'b' is already taken: + return "aa"; + } + + // Check that exactly one ball is on the board + // + at least one piece per color. static IsGoodPosition(position) { if (position.length == 0) return false; const rows = position.split("/"); if (rows.length != V.size.x) return false; - // Just check that at least one piece of each color is there: let pieces = { "w": 0, "b": 0 }; + let ballCount = 0; for (let row of rows) { let sumElts = 0; for (let i = 0; i < row.length; i++) { const lowerRi = row[i].toLowerCase(); - if (V.PIECES.includes(lowerRi)) { - pieces[row[i] == lowerRi ? "b" : "w"]++; + if (!!lowerRi.match(/^[a-z]$/)) { + if (V.PIECES.includes(lowerRi)) + pieces[row[i] == lowerRi ? "b" : "w"]++; + else if (lowerRi == 'a') ballCount++; + else return false; sumElts++; } else { @@ -52,37 +60,340 @@ export class FootballRules extends ChessRules { } if (sumElts != V.size.y) return false; } - if (Object.values(pieces).some(v => v == 0)) return false; + if (ballCount != 1 || Object.values(pieces).some(v => v == 0)) + return false; return true; } - scanKings() {} + static board2fen(b) { + if (b == V.BALL) return 'a'; + return ChessRules.board2fen(b); + } - filterValid(moves) { + static fen2board(f) { + if (f == 'a') return V.BALL; + return ChessRules.fen2board(f); + } + + getPpath(b) { + if (b == V.BALL) return "Football/ball"; + return b; + } + + canIplay(side, [x, y]) { + return ( + side == this.turn && + (this.board[x][y] == V.BALL || this.getColor(x, y) == side) + ); + } + + // No checks or king tracking etc. But, track ball + setOtherVariables() { + // Stack of "kicked by" coordinates, to avoid infinite loops + this.kickedBy = [ {} ]; + this.subTurn = 1; + this.ballPos = [-1, -1]; + for (let i=0; i < V.size.x; i++) { + for (let j=0; j< V.size.y; j++) { + if (this.board[i][j] == V.BALL) { + this.ballPos = [i, j]; + return; + } + } + } + } + + static GenRandInitFen(randomness) { + if (randomness == 0) + return "rnbq1knbr/9/9/9/4a4/9/9/9/RNBQ1KNBR w 0"; + + // TODO: following is mostly copy-paste from Suicide variant + let pieces = { w: new Array(8), b: new Array(8) }; + for (let c of ["w", "b"]) { + if (c == 'b' && randomness == 1) { + pieces['b'] = pieces['w']; + break; + } + + // Get random squares for every piece, totally freely + let positions = shuffle(ArrayFun.range(8)); + const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q']; + const rem2 = positions[0] % 2; + if (rem2 == positions[1] % 2) { + // Fix bishops (on different colors) + for (let i=2; i<8; i++) { + if (positions[i] % 2 != rem2) + [positions[1], positions[i]] = [positions[i], positions[1]]; + } + } + for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i]; + } + const piecesB = pieces["b"].join("") ; + const piecesW = pieces["w"].join("").toUpperCase(); + return ( + piecesB.substr(0, 4) + "1" + piecesB.substr(4) + + "/9/9/9/4a4/9/9/9/" + + piecesW.substr(0, 4) + "1" + piecesW.substr(4) + + " w 0" + ); + } + + tryKickFrom([x, y]) { + const bp = this.ballPos; + const emptySquare = (i, j) => { + return V.OnBoard(i, j) && this.board[i][j] == V.EMPTY; + }; + // Kick the (adjacent) ball from x, y with current turn: + const step = [bp[0] - x, bp[1] - y]; + const piece = this.getPiece(x, y); + let moves = []; + if (piece == V.KNIGHT) { + // The knight case is particular + V.steps[V.KNIGHT].forEach(s => { + const [i, j] = [bp[0] + s[0], bp[1] + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] == V.EMPTY && + ( + // In a corner? The, allow all ball moves + ([0, 8].includes(bp[0]) && [0, 8].includes(bp[1])) || + // Do not end near the knight + (Math.abs(i - x) >= 2 || Math.abs(j - y) >= 2) + ) + ) { + moves.push(super.getBasicMove(bp, [i, j])); + } + }); + } + else { + let compatible = false, + oneStep = false; + switch (piece) { + case V.ROOK: + compatible = (step[0] == 0 || step[1] == 0); + break; + case V.BISHOP: + compatible = (step[0] != 0 && step[1] != 0); + break; + case V.QUEEN: + compatible = true; + break; + case V.KING: + compatible = true; + oneStep = true; + break; + } + if (!compatible) return []; + let [i, j] = [bp[0] + step[0], bp[1] + step[1]]; + const horizontalStepOnGoalRow = + ([0, 8].includes(bp[0]) && step.some(s => s == 0)); + if (emptySquare(i, j) && (!horizontalStepOnGoalRow || j != 4)) { + moves.push(super.getBasicMove(bp, [i, j])); + if (!oneStep) { + do { + i += step[0]; + j += step[1]; + if (!emptySquare(i, j)) break; + if (!horizontalStepOnGoalRow || j != 4) + moves.push(super.getBasicMove(bp, [i, j])); + } while (true); + } + } + } + const kickedFrom = x + "-" + y; + moves.forEach(m => m.by = kickedFrom) return moves; } + getPotentialMovesFrom([x, y], computer) { + if (V.PIECES.includes(this.getPiece(x, y))) { + if (this.subTurn > 1) return []; + return ( + super.getPotentialMovesFrom([x, y]) + .filter(m => m.end.y != 4 || ![0, 8].includes(m.end.x)) + ); + } + // Kicking the ball: look for adjacent pieces. + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + const c = this.turn; + let moves = []; + for (let s of steps) { + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == c + ) { + Array.prototype.push.apply(moves, this.tryKickFrom([i, j])); + } + } + // And, always add the "end" move. For computer, keep only one + outerLoop: for (let i=0; i < V.size.x; i++) { + for (let j=0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == c) { + moves.push(super.getBasicMove([x, y], [i, j])); + if (!!computer) break outerLoop; + } + } + } + return moves; + } + + // No captures: + getSlideNJumpMoves([x, y], steps, oneStep) { + let moves = []; + outerLoop: for (let step of steps) { + let i = x + step[0]; + let j = y + step[1]; + let stepCount = 1; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + moves.push(this.getBasicMove([x, y], [i, j])); + if (!!oneStep) continue outerLoop; + i += step[0]; + j += step[1]; + stepCount++; + } + } + return moves; + } + + // Extra arg "computer" to avoid trimming all redundant pass moves: + getAllPotentialMoves(computer) { + const color = this.turn; + let potentialMoves = []; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + Array.prototype.push.apply( + potentialMoves, + this.getPotentialMovesFrom([i, j], computer) + ); + } + } + } + return potentialMoves; + } + + getAllValidMoves() { + return this.filterValid(this.getAllPotentialMoves("computer")); + } + + filterValid(moves) { + const L = this.kickedBy.length; + const kb = this.kickedBy[L-1]; + return moves.filter(m => !m.by || !kb[m.by]); + } + getCheckSquares() { return []; } - // No variables update because no royal king + no castling - prePlay() {} - postPlay() {} - preUndo() {} - postUndo() {} + allowAnotherPass(color) { + // Two cases: a piece moved, or the ball moved. + // In both cases, check our pieces and ball proximity, + // so the move played doesn't matter (if ball position updated) + const bp = this.ballPos; + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let s of steps) { + const [i, j] = [this.ballPos[0] + s[0], this.ballPos[1] + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color + ) { + return true; //potentially... + } + } + return false; + } + + prePlay(move) { + if (move.appear[0].p == 'a') + this.ballPos = [move.appear[0].x, move.appear[0].y]; + } + + play(move) { + // Special message saying "passes are over" + const passesOver = (move.vanish.length == 2); + if (!passesOver) { + this.prePlay(move); + V.PlayOnBoard(this.board, move); + } + move.turn = [this.turn, this.subTurn]; //easier undo + if (passesOver || !this.allowAnotherPass(this.turn)) { + this.turn = V.GetOppCol(this.turn); + this.subTurn = 1; + this.movesCount++; + this.kickedBy.push( {} ); + } + else { + this.subTurn++; + if (!!move.by) { + const L = this.kickedBy.length; + this.kickedBy[L-1][move.by] = true; + } + } + } + + undo(move) { + const passesOver = (move.vanish.length == 2); + if (move.turn[0] != this.turn) { + [this.turn, this.subTurn] = move.turn; + this.movesCount--; + this.kickedBy.pop(); + } + else { + this.subTurn--; + if (!!move.by) { + const L = this.kickedBy.length; + delete this.kickedBy[L-1][move.by]; + } + } + if (!passesOver) { + V.UndoOnBoard(this.board, move); + this.postUndo(move); + } + } + + postUndo(move) { + if (move.vanish[0].p == 'a') + this.ballPos = [move.vanish[0].x, move.vanish[0].y]; + } getCurrentScore() { - const oppCol = V.GetOppCol(this.turn); - const goal = (oppCol == 'w' ? 0 : 7); - if (this.board[goal].slice(3, 5).some(b => b[0] == oppCol)) - return oppCol == 'w' ? "1-0" : "0-1"; - if (this.atLeastOneMove()) return "*"; - return "1/2"; + if (this.board[0][4] == V.BALL) return "1-0"; + if (this.board[8][4] == V.BALL) return "0-1"; + return "*"; } - static GenRandInitFen(randomness) { - return SuicideRules.GenRandInitFen(randomness); + getComputerMove() { + let initMoves = this.getAllValidMoves(); + if (initMoves.length == 0) return null; + let moves = JSON.parse(JSON.stringify(initMoves)); + let mvArray = []; + let mv = null; + // Just play random moves (for now at least. TODO?) + const c = this.turn; + while (moves.length > 0) { + mv = moves[randInt(moves.length)]; + mvArray.push(mv); + this.play(mv); + if (mv.vanish.length == 1 && this.allowAnotherPass(c)) + // Potential kick + moves = this.getPotentialMovesFrom(this.ballPos); + else break; + } + for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); + return (mvArray.length > 1 ? mvArray : mvArray[0]); + } + + // NOTE: evalPosition() is wrong, but unused since bot plays at random + + getNotation(move) { + if (move.vanish.length == 2) return "pass"; + if (move.vanish[0].p != 'a') return super.getNotation(move); + // Kick: simple notation (TODO?) + return V.CoordsToSquare(move.end); } }; diff --git a/client/src/variants/Squatter.js b/client/src/variants/Squatter1.js similarity index 98% rename from client/src/variants/Squatter.js rename to client/src/variants/Squatter1.js index f9976790..a4aafd3a 100644 --- a/client/src/variants/Squatter.js +++ b/client/src/variants/Squatter1.js @@ -1,6 +1,6 @@ import { ChessRules } from "@/base_rules"; -export class SquatterRules extends ChessRules { +export class Squatter1Rules extends ChessRules { static get Lines() { return [ diff --git a/client/src/variants/Squatter2.js b/client/src/variants/Squatter2.js new file mode 100644 index 00000000..15caaf5b --- /dev/null +++ b/client/src/variants/Squatter2.js @@ -0,0 +1,88 @@ +import { ChessRules } from "@/base_rules"; +import { SuicideRules } from "@/variants/Suicide"; + +export class Squatter2Rules extends ChessRules { + + static get HasFlags() { + return false; + } + + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) } + ); + } + + static get Lines() { + return [ + // White goal: + [[0, 3], [0, 5]], + [[0, 5], [1, 5]], + [[1, 5], [1, 3]], + [[1, 3], [0, 3]], + // Black goal: + [[8, 3], [8, 5]], + [[8, 5], [7, 5]], + [[7, 5], [7, 3]], + [[7, 3], [8, 3]] + ]; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + // Just check that at least one piece of each color is there: + let pieces = { "w": 0, "b": 0 }; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + const lowerRi = row[i].toLowerCase(); + if (V.PIECES.includes(lowerRi)) { + pieces[row[i] == lowerRi ? "b" : "w"]++; + sumElts++; + } + else { + const num = parseInt(row[i], 10); + if (isNaN(num)) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + if (Object.values(pieces).some(v => v == 0)) return false; + return true; + } + + scanKings() {} + + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + // No variables update because no royal king + no castling + prePlay() {} + postPlay() {} + preUndo() {} + postUndo() {} + + getCurrentScore() { + const oppCol = V.GetOppCol(this.turn); + const goal = (oppCol == 'w' ? 0 : 7); + if (this.board[goal].slice(3, 5).some(b => b[0] == oppCol)) + return oppCol == 'w' ? "1-0" : "0-1"; + if (this.atLeastOneMove()) return "*"; + return "1/2"; + } + + static GenRandInitFen(randomness) { + return SuicideRules.GenRandInitFen(randomness); + } + +}; diff --git a/server/db/populate.sql b/server/db/populate.sql index 13f2e8a7..3a5e5171 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -131,7 +131,8 @@ insert or ignore into Variants (name, description) values ('Shogi', 'Japanese Chess'), ('Sittuyin', 'Burmese Chess'), ('Spartan', 'Spartan versus Persians'), - ('Squatter', 'Squat last rank'), + ('Squatter1', 'Squat last rank (v1)'), + ('Squatter2', 'Squat last rank (v2)'), ('Suicide', 'Lose all pieces'), ('Suction', 'Attract opposite king'), ('Swap', 'Dangerous captures'), -- 2.44.0