From: Benjamin Auder Date: Sat, 21 Mar 2020 05:14:22 +0000 (+0100) Subject: Draft Ball variant + some fixes, enhancements and code cleaning X-Git-Url: https://git.auder.net/%7B%7B%20asset%28%27mixstore/images/doc/html/common.css?a=commitdiff_plain;h=6f2f94374f1e73c375edf732d9425e575e81fff7;p=vchess.git Draft Ball variant + some fixes, enhancements and code cleaning --- diff --git a/client/public/images/pieces/Ball/aa.svg b/client/public/images/pieces/Ball/aa.svg new file mode 100644 index 00000000..8ba364b5 --- /dev/null +++ b/client/public/images/pieces/Ball/aa.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/public/images/pieces/Ball/bc.svg b/client/public/images/pieces/Ball/bc.svg new file mode 100644 index 00000000..03c42738 --- /dev/null +++ b/client/public/images/pieces/Ball/bc.svg @@ -0,0 +1,94 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/bl.svg b/client/public/images/pieces/Ball/bl.svg new file mode 100644 index 00000000..965d8dec --- /dev/null +++ b/client/public/images/pieces/Ball/bl.svg @@ -0,0 +1,119 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/bo.svg b/client/public/images/pieces/Ball/bo.svg new file mode 100644 index 00000000..a3e425a5 --- /dev/null +++ b/client/public/images/pieces/Ball/bo.svg @@ -0,0 +1,99 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/bs.svg b/client/public/images/pieces/Ball/bs.svg new file mode 100644 index 00000000..fab7c213 --- /dev/null +++ b/client/public/images/pieces/Ball/bs.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/bt.svg b/client/public/images/pieces/Ball/bt.svg new file mode 100644 index 00000000..311d5fd7 --- /dev/null +++ b/client/public/images/pieces/Ball/bt.svg @@ -0,0 +1,109 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/bu.svg b/client/public/images/pieces/Ball/bu.svg new file mode 100644 index 00000000..bce3eed1 --- /dev/null +++ b/client/public/images/pieces/Ball/bu.svg @@ -0,0 +1,94 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/bw.svg b/client/public/images/pieces/Ball/bw.svg new file mode 100644 index 00000000..6d4f9604 --- /dev/null +++ b/client/public/images/pieces/Ball/bw.svg @@ -0,0 +1,135 @@ + +Madeby Gridsimage/svg+xml \ No newline at end of file diff --git a/client/public/images/pieces/Ball/by.svg b/client/public/images/pieces/Ball/by.svg new file mode 100644 index 00000000..06324791 --- /dev/null +++ b/client/public/images/pieces/Ball/by.svg @@ -0,0 +1,159 @@ + +Madeby Gridsimage/svg+xml \ No newline at end of file diff --git a/client/public/images/pieces/Ball/wc.svg b/client/public/images/pieces/Ball/wc.svg new file mode 100644 index 00000000..b51dea5a --- /dev/null +++ b/client/public/images/pieces/Ball/wc.svg @@ -0,0 +1,126 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/wl.svg b/client/public/images/pieces/Ball/wl.svg new file mode 100644 index 00000000..322c54c3 --- /dev/null +++ b/client/public/images/pieces/Ball/wl.svg @@ -0,0 +1,124 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/wo.svg b/client/public/images/pieces/Ball/wo.svg new file mode 100644 index 00000000..0e3bd47e --- /dev/null +++ b/client/public/images/pieces/Ball/wo.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/ws.svg b/client/public/images/pieces/Ball/ws.svg new file mode 100644 index 00000000..dfcd791e --- /dev/null +++ b/client/public/images/pieces/Ball/ws.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/wt.svg b/client/public/images/pieces/Ball/wt.svg new file mode 100644 index 00000000..e2a5a230 --- /dev/null +++ b/client/public/images/pieces/Ball/wt.svg @@ -0,0 +1,204 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/wu.svg b/client/public/images/pieces/Ball/wu.svg new file mode 100644 index 00000000..b2bfd221 --- /dev/null +++ b/client/public/images/pieces/Ball/wu.svg @@ -0,0 +1,128 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Ball/ww.svg b/client/public/images/pieces/Ball/ww.svg new file mode 100644 index 00000000..561449c4 --- /dev/null +++ b/client/public/images/pieces/Ball/ww.svg @@ -0,0 +1,114 @@ + +Madeby Gridsimage/svg+xml \ No newline at end of file diff --git a/client/public/images/pieces/Ball/wy.svg b/client/public/images/pieces/Ball/wy.svg new file mode 100644 index 00000000..e86bfe40 --- /dev/null +++ b/client/public/images/pieces/Ball/wy.svg @@ -0,0 +1,162 @@ + +Madeby Gridsimage/svg+xml \ No newline at end of file diff --git a/client/src/App.vue b/client/src/App.vue index b83c0329..15e2f101 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -312,6 +312,10 @@ div.board8 width: 12.5% padding-bottom: 12.5% +div.board9 + width: 11.1% + padding-bottom: 11.1% + div.board10 width: 10% padding-bottom: 10% diff --git a/client/src/base_rules.js b/client/src/base_rules.js index b83a409b..f9574004 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -128,11 +128,11 @@ export const ChessRules = class ChessRules { if (position.length == 0) return false; const rows = position.split("/"); if (rows.length != V.size.x) return false; - let kings = {}; + let kings = { "k": 0, "K": 0 }; for (let row of rows) { let sumElts = 0; for (let i = 0; i < row.length; i++) { - if (['K','k'].includes(row[i])) kings[row[i]] = true; + if (['K','k'].includes(row[i])) kings[row[i]]++; if (V.PIECES.includes(row[i].toLowerCase())) sumElts++; else { const num = parseInt(row[i]); @@ -142,8 +142,8 @@ export const ChessRules = class ChessRules { } if (sumElts != V.size.y) return false; } - // Both kings should be on board: - if (Object.keys(kings).length != 2) return false; + // Both kings should be on board. Exactly one per color. + if (Object.values(kings).some(v => v != 1)) return false; return true; } @@ -367,6 +367,13 @@ export const ChessRules = class ChessRules { // Position part of the FEN string getBaseFen() { + const format = (count) => { + // if more than 9 consecutive free spaces, break the integer, + // otherwise FEN parsing will fail. + if (count <= 9) return count; + // Currently only boards of size up to 11 or 12: + return "9" + (count - 9); + }; let position = ""; for (let i = 0; i < V.size.x; i++) { let emptyCount = 0; @@ -375,7 +382,7 @@ export const ChessRules = class ChessRules { else { if (emptyCount > 0) { // Add empty squares in-between - position += emptyCount; + position += format(emptyCount); emptyCount = 0; } position += V.board2fen(this.board[i][j]); @@ -383,7 +390,7 @@ export const ChessRules = class ChessRules { } if (emptyCount > 0) { // "Flush remainder" - position += emptyCount; + position += format(emptyCount); } if (i < V.size.x - 1) position += "/"; //separate rows } @@ -672,7 +679,7 @@ export const ChessRules = class ChessRules { enpassantMove.vanish.push({ x: x, y: epSquare.y, - p: "p", + p: this.getPiece(x, epSquare.y), c: this.getColor(x, epSquare.y) }); } @@ -1187,7 +1194,7 @@ export const ChessRules = class ChessRules { return V.INFINITY; } - // Search depth: 2 for high branching factor, 4 for small (Loser chess, eg.) + // Search depth: 1,2 for high branching factor, 4 for small (Loser chess, eg.) static get SEARCH_DEPTH() { return 3; } diff --git a/client/src/translations/en.js b/client/src/translations/en.js index af674cc4..90d7daf5 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -192,6 +192,7 @@ export const translations = { "Reuse pieces": "Reuse pieces", "Reverse captures": "Reverse captures", "Run forward": "Run forward", + "Score a goal": "Score a goal", "Shared pieces": "Shared pieces", "Shoot pieces": "Shoot pieces", "Squares disappear": "Squares disappear", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 183bd6f3..6894fcab 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -192,6 +192,7 @@ export const translations = { "Reuse pieces": "Reutilizar piezas", "Reverse captures": "Capturas invertidas", "Run forward": "Correr hacia adelante", + "Score a goal": "Marcar una meta", "Shared pieces": "Piezas compartidas", "Shoot pieces": "Tirar de las piezas", "Squares disappear": "Las casillas desaparecen", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index b317bb41..02b168de 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -192,6 +192,7 @@ export const translations = { "Reuse pieces": "Réutiliser les pièces", "Reverse captures": "Captures inversées", "Run forward": "Courir vers l'avant", + "Score a goal": "Marquer un but", "Shared pieces": "Pièces partagées", "Shoot pieces": "Tirez sur les pièces", "Squares disappear": "Les cases disparaissent", diff --git a/client/src/translations/rules/Ball/en.pug b/client/src/translations/rules/Ball/en.pug new file mode 100644 index 00000000..475a1609 --- /dev/null +++ b/client/src/translations/rules/Ball/en.pug @@ -0,0 +1,52 @@ +p.boxed + | Capture the ball, pass it among your pieces and bring it to the + | center of the last rank. + +p. + At the beginning, there is a ball in the center of the 9x9 board. + It doesn't move. The first piece to capture it will then hold the ball. + +ul + li. + Capturing an enemy unit holding the ball both make the piece + disappear and grab the ball. + li "Capturing" a friendly unit pass the ball to it. + +figure.diagram-container + .diagram.diag12 + | fen:rnbqkwbnr/ppppp1ppp/9/5p3/4a4/9/2N3N2/PPPPPPPPP/R1BQKWB1R: + .diagram.diag22 + | fen:rnbqkwbnr/ppppp1ppp/9/9/4s4/9/2N3N2/PPPPPPPPP/R1BQKWB1R: + figcaption Left: before ...fxe5 (taking ball). Right: after ...fxe5. + +p. + The piece sitting to the right of the king is a wildebeest (W). + It moves by jumping in L like a knight, or a little bit further: + three squares in one direction then one aside. + +h3 End of the game + +p. + The goal is to bring a piece holding the ball in one of the three + central squares of the opposite side (files d, e or f). + +figure.diagram-container + .diagram.diag12 + | fen:rqnkb1nb1/pp1pppp1r/2p5p/4wW1p1/4S4/1P5B1/9/P1PPP1PPP/RQNK2NBR: + .diagram.diag22 + | fen:rqnkb1nb1/pp1pppp1r/2p5p/4wY1p1/4P4/1P5B1/9/P1PPP1PPP/RQNK2NBR: + figcaption. + Left: before e5Pf6 (passing the ball). + Right: after the move. Then Wxe9# cannot be prevented. + +p. + White pass the ball from the pawn at e5 to the wildebeest at f6. + Then, since the black bishop on e8 cannot move white will win by taking it: Wxe9#. + +h3 Source + +p. + A friend and I talked some day (in 2019) about a variant where pieces + would hit a ball to bring it to the opposite side. + This version is inspired by these preliminary trials, + because hitting the ball led to somewhat blocked situations. diff --git a/client/src/translations/rules/Ball/es.pug b/client/src/translations/rules/Ball/es.pug new file mode 100644 index 00000000..68940c7f --- /dev/null +++ b/client/src/translations/rules/Ball/es.pug @@ -0,0 +1,54 @@ +p.boxed + | Captura la pelota, pásala entre tus piezas y tráela + | en el centro de la última fila. + +p. + Al principio, hay una pelota en el medio del tablero 9x9. + No se mueve. La primera pieza que capturarla sostendrá la pelota. + +ul + li. + Capturar una pieza enemiga con el globo lo roba + mientras lo hace desaparecer. + li "Capturar" una pieza amiga le pasa la pelota. + +figure.diagram-container + .diagram.diag12 + | fen:rnbqkwbnr/ppppp1ppp/9/5p3/4a4/9/2N3N2/PPPPPPPPPP/R1BQKWB1R: + .diagram.diag22 + | fen:rnbqkwbnr/ppppp1ppp/9/9/4s4/9/2N3N2/PPPPPPPPPP/R1BQKWB1R: + figcaption. + Izquierda: antes de ...fxe5 (tomando la pelota). + Derecha: después de ...fxe5. + +p. + La pieza a la derecha del rey es un ñu (W). + Se mueve realizando saltos en forma de L como el caballo, + o un poco más alargado: tres casillas y luego una en el lateral. + +h3 Fin de la partida + +p. + El objetivo es traer una pieza en posesión de la pelota en uno de los + tres casillas centrales en la última fila (columnas d, e o f). + +figure.diagram-container + .diagram.diag12 + | fen:rqnkb1nb1/pp1pppp1r/2p5p/4wW1p1/4S4/1P5B1/9/P1PPP1PPP/RQNK2NBR: + .diagram.diag22 + | fen:rqnkb1nb1/pp1pppp1r/2p5p/4wY1p1/4P4/1P5B1/9/P1PPP1PPP/RQNK2NBR: + figcaption. + Izquierda: antes de e5Pf6 (pasando el globo). + Derecha: después del movimiento. Entonces Wxe9# no se puede prevenir. + +p. + Las blancas pasan la pelota del peón e5 al ñu en f6. + Entonces, como el alfil negro no puede moverse, + la victoria está asegurada capturándolo con Wxe9#. + +h3 Fuente + +p. + Esta variante está inspirada por una idea que tuvimos con una amiga en 2019, + lo que implicaba golpear la pelota para enviarla desde el otro lado del tablero. + La versión actual evita ciertos bloqueos observados entonces. diff --git a/client/src/translations/rules/Ball/fr.pug b/client/src/translations/rules/Ball/fr.pug new file mode 100644 index 00000000..97a1a78c --- /dev/null +++ b/client/src/translations/rules/Ball/fr.pug @@ -0,0 +1,52 @@ +p.boxed + | Capturez la balle, faites la passer entre vos pièces et amenez la + | au centre de la dernière rangée. + +p. + Au début, il y a un ballon au milieu de l'échiquier 9x9. + Il ne bouge pas. La première pièce à le capturer détiendra alors le ballon. + +ul + li. + Capturer une pièce ennemie possédant le ballon lui vole + tout en la faisant disparaître. + li "Capturer" une pièce amie lui passe le ballon. + +figure.diagram-container + .diagram.diag12 + | fen:rnbqkwbnr/ppppp1ppp/9/5p3/4a4/9/2N3N2/PPPPPPPPP/R1BQKWB1R: + .diagram.diag22 + | fen:rnbqkwbnr/ppppp1ppp/9/9/4s4/9/2N3N2/PPPPPPPPP/R1BQKWB1R: + figcaption Gauche : avant ...fxe5 (prenant le ballon). Droite : après ...fxe5. + +p. + La pièce située à droite du roi est un gnou (W). + Il se déplace en effectuant des sauts en L comme le cavalier, + ou bien un peu plus allongés : trois cases puis une sur le côté. + +h3 Fin de la partie + +p. + L'objectif est d'amener une pièce en possession du ballon sur une des + trois cases centrales de la dernière rangée (colonnes d, e ou f). + +figure.diagram-container + .diagram.diag12 + | fen:rqnkb1nb1/pp1pppp1r/2p5p/4wW1p1/4S4/1P5B1/9/P1PPP1PPP/RQNK2NBR: + .diagram.diag22 + | fen:rqnkb1nb1/pp1pppp1r/2p5p/4wY1p1/4P4/1P5B1/9/P1PPP1PPP/RQNK2NBR: + figcaption. + Gauche : avant e5Pf6 (passant le ballon). + Droite : après le coup. Ensuite Wxe9# ne peut être empêché. + +p. + Les blancs passent la balle du pion e5 vers le gnou en f6. + Ensuite, puisque le fou noir ne peut pas bouger la victoire est assurée + en le capturant par Wxe9€. + +h3 Source + +p. + Cette variante s'inspire d'une idée qu'on a eue avec une amie en 2019, qui + consistait à frapper la balle pour l'envoyer de l'autre côté de l'échiquier. + La version actuelle évite certains bloquages observés alors. diff --git a/client/src/variants/Antiking1.js b/client/src/variants/Antiking1.js index 3aa02d35..ac28c31c 100644 --- a/client/src/variants/Antiking1.js +++ b/client/src/variants/Antiking1.js @@ -28,6 +28,19 @@ export class Antiking1Rules extends BerolinaRules { return b[1] == "a" ? "Antiking/" + b : b; } + static IsGoodPosition(position) { + if (!ChessRules.IsGoodPosition(position)) return false; + const rows = position.split("/"); + // Check that exactly one antiking of each color is there: + let antikings = { 'a': 0, 'A': 0 }; + for (let row of rows) { + for (let i = 0; i < row.length; i++) + if (['A','a'].includes(row[i])) antikings[row[i]]++; + } + if (Object.values(antikings).some(v => v != 1)) return false; + return true; + } + setOtherVariables(fen) { super.setOtherVariables(fen); this.antikingPos = { w: [-1, -1], b: [-1, -1] }; diff --git a/client/src/variants/Antiking2.js b/client/src/variants/Antiking2.js index 087dce35..2dad5e49 100644 --- a/client/src/variants/Antiking2.js +++ b/client/src/variants/Antiking2.js @@ -15,6 +15,19 @@ export class Antiking2Rules extends ChessRules { return b[1] == "a" ? "Antiking/" + b : b; } + static IsGoodPosition(position) { + if (!ChessRules.IsGoodPosition(position)) return false; + const rows = position.split("/"); + // Check that exactly one antiking of each color is there: + let antikings = { 'a': 0, 'A': 0 }; + for (let row of rows) { + for (let i = 0; i < row.length; i++) + if (['A','a'].includes(row[i])) antikings[row[i]]++; + } + if (Object.values(antikings).some(v => v != 1)) return false; + return true; + } + setOtherVariables(fen) { super.setOtherVariables(fen); this.antikingPos = { w: [-1, -1], b: [-1, -1] }; diff --git a/client/src/variants/Arena.js b/client/src/variants/Arena.js index b2b18ee6..33940b83 100644 --- a/client/src/variants/Arena.js +++ b/client/src/variants/Arena.js @@ -13,6 +13,37 @@ export class ArenaRules extends ChessRules { ); } + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + // At most and at least one king or queen per color + let royals = { "k": 0, "K": 0, "q": 0, "Q": 0 }; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (['K','k','Q','q'].includes(row[i])) royals[row[i]]++; + if (V.PIECES.includes(row[i].toLowerCase())) sumElts++; + else { + const num = parseInt(row[i]); + if (isNaN(num)) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + if ( + Object.values(royals).some(v => v >= 2) || + royals['K'] + royals['Q'] == 0 || + royals['k'] + royals['q'] == 0 + ) { + return false; + } + return true; + } + + scanKings() {} + static GenRandInitFen(randomness) { return ChessRules.GenRandInitFen(randomness).slice(0, -6) + "-"; } diff --git a/client/src/variants/Ball.js b/client/src/variants/Ball.js new file mode 100644 index 00000000..cf50a809 --- /dev/null +++ b/client/src/variants/Ball.js @@ -0,0 +1,351 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { WildebeestRules } from "@/variants/Wildebeest"; +import { ArrayFun } from "@/utils/array"; +import { shuffle } from "@/utils/alea"; + +export class BallRules extends ChessRules { + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { promotions: ChessRules.PawnSpecs.promotions.concat([V.WILDEBEEST]) } + ); + } + + static get HasFlags() { + return false; + } + static get HasCastle() { + return false; + } + + static get WILDEBEEST() { + return 'w'; + } + + static get BALL() { + // 'b' is already taken: + return "aa"; + } + + // Special code for "something to fill space" (around goals) + // --> If goal is outside the board (current prototype: it's inside) +// static get FILL() { +// return "ff"; +// } + + static get HAS_BALL_CODE() { + return { + 'p': 's', + 'r': 'u', + 'n': 'o', + 'b': 'c', + 'q': 't', + 'k': 'l', + 'w': 'y' + }; + } + + static get HAS_BALL_DECODE() { + return { + 's': 'p', + 'u': 'r', + 'o': 'n', + 'c': 'b', + 't': 'q', + 'l': 'k', + 'y': 'w' + }; + } + + static get PIECES() { + return ChessRules.PIECES + .concat([V.WILDEBEEST]) + .concat(Object.keys(V.HAS_BALL_DECODE)) + .concat(['a']); + } + + static board2fen(b) { + if (b == V.BALL) return 'a'; + return ChessRules.board2fen(b); + } + + static fen2board(f) { + if (f == 'a') return V.BALL; + return ChessRules.fen2board(f); + } + + // 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; + let pieces = { "w": 0, "b": 0 }; + const withBall = Object.keys(V.HAS_BALL_DECODE).concat([V.BALL]); + 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 (withBall.includes(lowerRi)) ballCount++; + sumElts++; + } else { + const num = parseInt(row[i]); + if (isNaN(num)) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + if (ballCount != 1 || Object.values(pieces).some(v => v == 0)) + return false; + return true; + } + + getPpath(b) { + let prefix = ""; + const withPrefix = + Object.keys(V.HAS_BALL_DECODE) + .concat([V.WILDEBEEST]) + .concat(['a']); + if (withPrefix.includes(b[1])) prefix = "Ball/"; + return prefix + b; + } + + canTake([x1, y1], [x2, y2]) { + // Capture enemy or pass ball to friendly pieces + return ( + this.getColor(x1, y1) !== this.getColor(x2, y2) || + Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1)) + ); + } + + getCheckSquares(color) { + return []; + } + + static GenRandInitFen(randomness) { + if (randomness == 0) + return "rnbqkwbnr/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/RNBQKWBNR w 0 -"; + + let pieces = { w: new Array(9), b: new Array(9) }; + 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(9)); + const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q', 'w']; + const rem2 = positions[0] % 2; + if (rem2 == positions[1] % 2) { + // Fix bishops (on different colors) + for (let i=2; i<9; i++) { + if (positions[i] % 2 != rem2) + [positions[1], positions[i]] = [positions[i], positions[1]]; + } + } + for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i]; + } + return ( + pieces["b"].join("") + + "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + // En-passant allowed, but no flags + " w 0 -" + ); + } + + scanKings(fen) {} + + static get size() { + return { x: 9, y: 9 }; + } + + getPiece(i, j) { + const p = this.board[i][j].charAt(1); + if (Object.keys(V.HAS_BALL_DECODE).includes(p)) + return V.HAS_BALL_DECODE[p]; + return p; + } + + static get steps() { + return WildebeestRules.steps; + } + + // Because of the ball, getPiece() could be wrong: + // use board[x][y][1] instead (always valid). + getBasicMove([sx, sy], [ex, ey], tr) { + const initColor = this.getColor(sx, sy); + const initPiece = this.board[sx][sy].charAt(1); + let mv = new Move({ + appear: [ + new PiPo({ + x: ex, + y: ey, + c: tr ? tr.c : initColor, + p: tr ? tr.p : initPiece + }) + ], + vanish: [ + new PiPo({ + x: sx, + y: sy, + c: initColor, + p: initPiece + }) + ] + }); + + // Fix "ball holding" indication in case of promotions: + if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece)) + mv.appear[0].p = V.HAS_BALL_CODE[tr.p]; + + // The opponent piece disappears if we take it + if (this.board[ex][ey] != V.EMPTY) { + mv.vanish.push( + new PiPo({ + x: ex, + y: ey, + c: this.getColor(ex, ey), + p: this.board[ex][ey].charAt(1) + }) + ); + } + + // Post-processing: maybe the ball was taken, or a piece + ball + if (mv.vanish.length == 2) { + if ( + // Take the ball? + mv.vanish[1].c == 'a' || + // Capture a ball-holding piece? + Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p) + ) { + mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p]; + } 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]; + mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p]; + } + // Else: standard capture + } + + return mv; + } + + // NOTE: if a pawn is captured en-passant, he doesn't hold the ball + // So base implementation is fine. + + getPotentialMovesFrom([x, y]) { + if (this.getPiece(x, y) == V.WILDEBEEST) + return this.getPotentialWildebeestMoves([x, y]); + return super.getPotentialMovesFrom([x, y]); + } + + getPotentialWildebeestMoves(sq) { + return this.getSlideNJumpMoves( + sq, + V.steps[V.KNIGHT].concat(V.steps[WildebeestRules.CAMEL]), + "oneStep" + ); + } + + filterValid(moves) { + return moves; + } + + // isAttacked: unused here (no checks) + + postPlay() {} + postUndo() {} + + getCurrentScore() { + // Turn has changed: + const color = V.GetOppCol(this.turn); + const lastRank = (color == "w" ? 0 : 8); + if ([3,4,5].some( + i => { + return ( + Object.keys(V.HAS_BALL_DECODE).includes( + this.board[lastRank][i].charAt(1)) && + this.getColor(lastRank, i) == color + ); + } + )) { + // Goal scored! + return color == "w" ? "1-0" : "0-1"; + } + if (this.atLeastOneMove()) return "*"; + // Stalemate (quite unlikely?) + return "1/2"; + } + + static get VALUES() { + return { + p: 1, + r: 5, + n: 3, + b: 3, + q: 9, + w: 7, + k: 5, + a: 0 //ball: neutral + }; + } + + static get SEARCH_DEPTH() { + return 2; + } + + evalPosition() { + // Count material: + let evaluation = super.evalPosition(); + if (this.board[4][4] == V.BALL) + // Ball not captured yet + return evaluation; + // Ponder depending on ball position + for (let i=0; i<9; i++) { + for (let j=0; j<9; j++) { + if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1])) + return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i); + } + } + return 0; //never reached + } + + getNotation(move) { + const finalSquare = V.CoordsToSquare(move.end); + if (move.appear.length == 2) + // A pass: special notation + return V.CoordsToSquare(move.start) + "P" + finalSquare; + const piece = this.getPiece(move.start.x, move.start.y); + if (piece == V.PAWN) { + // Pawn move + let notation = ""; + if (move.vanish.length > move.appear.length) { + // Capture + const startColumn = V.CoordToColumn(move.start.y); + notation = startColumn + "x" + finalSquare; + } + else notation = finalSquare; + if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) { + // Promotion + const promotePiece = + V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p; + notation += "=" + promotePiece.toUpperCase(); + } + return notation; + } + // Piece movement + return ( + piece.toUpperCase() + + (move.vanish.length > move.appear.length ? "x" : "") + + finalSquare + ); + } +}; diff --git a/client/src/variants/Cannibal.js b/client/src/variants/Cannibal.js index 7860ab04..a08f1975 100644 --- a/client/src/variants/Cannibal.js +++ b/client/src/variants/Cannibal.js @@ -33,6 +33,34 @@ export class CannibalRules extends ChessRules { return (Object.keys(V.KING_DECODE).includes(b[1]) ? "Cannibal/" : "") + b; } + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + let kings = { "w": 0, "b": 0 }; + const allPiecesCodes = V.PIECES.concat(Object.keys(V.KING_DECODE)); + const kingBlackCodes = Object.keys(V.KING_DECODE).concat(['k']); + const kingWhiteCodes = + Object.keys(V.KING_DECODE).map(k => k.toUpperCase()).concat(['K']); + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (kingBlackCodes.includes(row[i])) kings['b']++; + else if (kingWhiteCodes.includes(row[i])) kings['w']++; + if (allPiecesCodes.includes(row[i].toLowerCase())) sumElts++; + else { + const num = parseInt(row[i]); + if (isNaN(num)) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + // Both kings should be on board, only one of each color: + if (Object.values(kings).some(v => v != 1)) return false; + return true; + } + // Kings may be disguised: setOtherVariables(fen) { super.setOtherVariables(fen); diff --git a/client/src/variants/Checkered.js b/client/src/variants/Checkered.js index d48d6322..882a32ac 100644 --- a/client/src/variants/Checkered.js +++ b/client/src/variants/Checkered.js @@ -394,9 +394,10 @@ export class CheckeredRules extends ChessRules { } static ParseFen(fen) { - return Object.assign({}, ChessRules.ParseFen(fen), { - cmove: fen.split(" ")[5] - }); + return Object.assign( + ChessRules.ParseFen(fen), + { cmove: fen.split(" ")[5] } + ); } getFen() { diff --git a/client/src/variants/Coregal.js b/client/src/variants/Coregal.js index f0fc3801..2e494070 100644 --- a/client/src/variants/Coregal.js +++ b/client/src/variants/Coregal.js @@ -4,7 +4,7 @@ import { randInt, sample } from "@/utils/alea"; export class CoregalRules extends ChessRules { static IsGoodPosition(position) { - if (!super.IsGoodPosition(position)) return false; + if (!ChessRules.IsGoodPosition(position)) return false; const rows = position.split("/"); // Check that at least one queen of each color is there: let queens = {}; diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js index 321bf4cf..2307eaff 100644 --- a/client/src/variants/Crazyhouse.js +++ b/client/src/variants/Crazyhouse.js @@ -22,10 +22,13 @@ export class CrazyhouseRules extends ChessRules { static ParseFen(fen) { const fenParts = fen.split(" "); - return Object.assign(ChessRules.ParseFen(fen), { - reserve: fenParts[5], - promoted: fenParts[6] - }); + return Object.assign( + ChessRules.ParseFen(fen), + { + reserve: fenParts[5], + promoted: fenParts[6] + } + ); } getEpSquare(moveOrSquare) { diff --git a/client/src/variants/Extinction.js b/client/src/variants/Extinction.js index 746809f6..59620f95 100644 --- a/client/src/variants/Extinction.js +++ b/client/src/variants/Extinction.js @@ -10,8 +10,7 @@ export class ExtinctionRules extends ChessRules { } static IsGoodPosition(position) { - if (!ChessRules.IsGoodPosition(position)) - return false; + if (!ChessRules.IsGoodPosition(position)) return false; // Also check that each piece type is present const rows = position.split("/"); let pieces = {}; @@ -21,11 +20,12 @@ export class ExtinctionRules extends ChessRules { pieces[row[i]] = true; } } - if (Object.keys(pieces).length != 12) - return false; + if (Object.keys(pieces).length != 12) return false; return true; } + scanKings() {} + setOtherVariables(fen) { super.setOtherVariables(fen); const pos = V.ParseFen(fen).position; diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js index f4a96ce1..0809a65d 100644 --- a/client/src/variants/Grand.js +++ b/client/src/variants/Grand.js @@ -28,7 +28,10 @@ export class GrandRules extends ChessRules { static ParseFen(fen) { const fenParts = fen.split(" "); - return Object.assign(ChessRules.ParseFen(fen), { captured: fenParts[5] }); + return Object.assign( + ChessRules.ParseFen(fen), + { captured: fenParts[5] } + ); } getPpath(b) { @@ -326,15 +329,17 @@ export class GrandRules extends ChessRules { static GenRandInitFen(randomness) { if (randomness == 0) { // No castling in the official initial setup - return "r8r/1nbqkmcbn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKMCBN1/R8R " + + return "r8r/1nbqkmcbn1/pppppppppp/91/91/91/91/PPPPPPPPPP/1NBQKMCBN1/R8R " + "w 0 zzzz - 00000000000000"; } let pieces = { w: new Array(10), b: new Array(10) }; + let flags = ""; // Shuffle pieces on first and last rank for (let c of ["w", "b"]) { if (c == 'b' && randomness == 1) { pieces['b'] = pieces['w']; + flags += flags; break; } @@ -389,12 +394,13 @@ export class GrandRules extends ChessRules { pieces[c][bishop2Pos] = "b"; pieces[c][knight2Pos] = "n"; pieces[c][rook2Pos] = "r"; + flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(rook2Pos); } return ( pieces["b"].join("") + - "/pppppppppp/10/10/10/10/10/10/PPPPPPPPPP/" + + "/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/" + pieces["w"].join("").toUpperCase() + - " w 0 1111 - 00000000000000" + " w 0 " + flags + " - 00000000000000" ); } }; diff --git a/client/src/variants/Hidden.js b/client/src/variants/Hidden.js index c060310c..ef5b0bb7 100644 --- a/client/src/variants/Hidden.js +++ b/client/src/variants/Hidden.js @@ -54,7 +54,7 @@ export class HiddenRules extends ChessRules { } static get PIECES() { - return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE)); + return ChessRules.PIECES.concat(Object.keys(V.HIDDEN_DECODE)); } // Pieces can be hidden :) diff --git a/client/src/variants/Hiddenqueen.js b/client/src/variants/Hiddenqueen.js index 8db5a50e..f3b5e3c8 100644 --- a/client/src/variants/Hiddenqueen.js +++ b/client/src/variants/Hiddenqueen.js @@ -13,7 +13,7 @@ export class HiddenqueenRules extends ChessRules { } static get PIECES() { - return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE)); + return ChessRules.PIECES.concat([V.HIDDEN_QUEEN]); } getPiece(i, j) { diff --git a/client/src/variants/Knightrelay1.js b/client/src/variants/Knightrelay1.js index f032e21e..4f9f7e35 100644 --- a/client/src/variants/Knightrelay1.js +++ b/client/src/variants/Knightrelay1.js @@ -5,6 +5,8 @@ export class Knightrelay1Rules extends ChessRules { return false; } + // TODO: IsGoodPosition to check that 2 knights are on the board... + getPotentialMovesFrom([x, y]) { let moves = super.getPotentialMovesFrom([x, y]); diff --git a/client/src/variants/Recycle.js b/client/src/variants/Recycle.js index 8980c721..8b070fac 100644 --- a/client/src/variants/Recycle.js +++ b/client/src/variants/Recycle.js @@ -21,9 +21,10 @@ export class RecycleRules extends ChessRules { static ParseFen(fen) { const fenParts = fen.split(" "); - return Object.assign(ChessRules.ParseFen(fen), { - reserve: fenParts[5] - }); + return Object.assign( + ChessRules.ParseFen(fen), + { reserve: fenParts[5] } + ); } getEpSquare(moveOrSquare) { diff --git a/client/src/variants/Royalrace.js b/client/src/variants/Royalrace.js index 9b6575ac..39ee02aa 100644 --- a/client/src/variants/Royalrace.js +++ b/client/src/variants/Royalrace.js @@ -25,7 +25,7 @@ export class RoyalraceRules extends ChessRules { static GenRandInitFen(randomness) { if (randomness == 0) - return "11/11/11/11/11/11/11/11/11/qrbnp1PNBRQ/krbnp1PNBRK w 0"; + return "92/92/92/92/92/92/92/92/92/qrbnp1PNBRQ/krbnp1PNBRK w 0"; let pieces = { w: new Array(10), b: new Array(10) }; // Shuffle pieces on first and second rank @@ -96,7 +96,7 @@ export class RoyalraceRules extends ChessRules { const whiteFen = pieces["w"].join("").toUpperCase(); const blackFen = pieces["b"].join(""); return ( - "11/11/11/11/11/11/11/11/11/" + + "92/92/92/92/92/92/92/92/92/" + blackFen.substr(5).split("").reverse().join("") + "1" + whiteFen.substr(5).split("").join("") + diff --git a/client/src/variants/Rugby.js b/client/src/variants/Rugby.js index 04306c26..209def87 100644 --- a/client/src/variants/Rugby.js +++ b/client/src/variants/Rugby.js @@ -14,6 +14,8 @@ export class RugbyRules extends ChessRules { ); } + scanKings() {} + getPotentialMovesFrom(sq) { // There are only pawns: return this.getPotentialPawnMoves(sq); @@ -44,6 +46,10 @@ export class RugbyRules extends ChessRules { preUndo() {} postUndo() {} + getCheckSquares() { + return []; + } + getCurrentScore() { // Turn has changed: const color = V.GetOppCol(this.turn); diff --git a/client/src/variants/Suction.js b/client/src/variants/Suction.js index ac3aeb02..2038e4f8 100644 --- a/client/src/variants/Suction.js +++ b/client/src/variants/Suction.js @@ -20,9 +20,10 @@ export class SuctionRules extends ChessRules { } static ParseFen(fen) { - return Object.assign({}, ChessRules.ParseFen(fen), { - cmove: fen.split(" ")[4] - }); + return Object.assign( + ChessRules.ParseFen(fen), + { cmove: fen.split(" ")[4] } + ); } static IsGoodFen(fen) { diff --git a/client/src/variants/Suicide.js b/client/src/variants/Suicide.js index 1304ba98..c81d3551 100644 --- a/client/src/variants/Suicide.js +++ b/client/src/variants/Suicide.js @@ -19,6 +19,33 @@ export class SuicideRules extends ChessRules { ); } + 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]); + 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() {} + // Trim all non-capturing moves (not the most efficient, but easy) static KeepCaptures(moves) { return moves.filter(m => m.vanish.length == 2); @@ -116,7 +143,6 @@ export class SuicideRules extends ChessRules { return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 -"; let pieces = { w: new Array(8), b: new Array(8) }; - // Shuffle pieces on first and last rank for (let c of ["w", "b"]) { if (c == 'b' && randomness == 1) { pieces['b'] = pieces['w']; diff --git a/client/src/variants/Wildebeest.js b/client/src/variants/Wildebeest.js index 5bb50e81..fe2a0c04 100644 --- a/client/src/variants/Wildebeest.js +++ b/client/src/variants/Wildebeest.js @@ -20,7 +20,9 @@ export class WildebeestRules extends ChessRules { static get steps() { return Object.assign( - ChessRules.steps, //add camel moves: + {}, + ChessRules.steps, + // Add camel moves: { c: [ [-3, -1], @@ -244,9 +246,9 @@ export class WildebeestRules extends ChessRules { static GenRandInitFen(randomness) { if (!randomness) randomness = 2; if (randomness == 0) - return "rnccwkqbbnr/ppppppppppp/11/11/11/11/11/11/PPPPPPPPPPP/RNBBQKWCCNR w 0 akak -"; + return "rnccwkqbbnr/ppppppppppp/92/92/92/92/92/92/PPPPPPPPPPP/RNBBQKWCCNR w 0 akak -"; - let pieces = { w: new Array(10), b: new Array(10) }; + let pieces = { w: new Array(11), b: new Array(11) }; let flags = ""; for (let c of ["w", "b"]) { if (c == 'b' && randomness == 1) { @@ -311,7 +313,7 @@ export class WildebeestRules extends ChessRules { } return ( pieces["b"].join("") + - "/ppppppppppp/11/11/11/11/11/11/PPPPPPPPPPP/" + + "/ppppppppppp/92/92/92/92/92/92/PPPPPPPPPPP/" + pieces["w"].join("").toUpperCase() + " w 0 " + flags + " -" ); diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index d0cabb46..9bb913c4 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -73,7 +73,10 @@ main ) fieldset(v-if="st.user.id > 0") label(for="selectPlayers") {{ st.tr["Play with"] }} - select#selectPlayersInList(v-model="newchallenge.to") + select#selectPlayersInList( + v-model="newchallenge.to" + @change="changeChallTarget()" + ) option(value="") option( v-for="p in Object.values(people)" @@ -453,6 +456,13 @@ export default { if (!!this.curChallToAccept.fen) return { "margin-top": "10px" }; return {}; }, + changeChallTarget: function() { + if (!this.newchallenge.to) { + // Reset potential FEN + diagram + this.newchallenge.fen = ""; + this.newchallenge.diag = ""; + } + }, cadenceFocusIfOpened: function() { if (event.target.checked) document.getElementById("cadence").focus(); @@ -903,7 +913,7 @@ export default { position: parsedFen.position, orientation: parsedFen.turn }); - } + } else this.newchallenge.diag = ""; }, newChallFromPreset(pchall) { this.partialResetNewchallenge(); diff --git a/client/src/views/Problems.vue b/client/src/views/Problems.vue index 9ce2b3fe..f1c2d08a 100644 --- a/client/src/views/Problems.vue +++ b/client/src/views/Problems.vue @@ -273,6 +273,7 @@ export default { this.loadVariant(prob.vid, () => { // Set FEN if possible (might not be correct yet) if (V.IsGoodFen(prob.fen)) this.setDiagram(prob); + else prob.diag = ""; }); }, loadVariant: async function(vid, cb) { @@ -291,6 +292,7 @@ export default { // variant could not be ready, or not defined if (prob.vid > 0 && this.loadedVar == prob.vid && V.IsGoodFen(prob.fen)) this.setDiagram(prob); + else prob.diag = ""; }, setDiagram: function(prob) { // Condition: prob.fen is correct and global V is ready diff --git a/server/db/populate.sql b/server/db/populate.sql index a842d3e2..a0d4c843 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -9,6 +9,7 @@ insert or ignore into Variants (name,description) values ('Antimatter', 'Dangerous collisions'), ('Arena', 'Middle battle'), ('Atomic', 'Explosive captures'), + ('Ball', 'Score a goal'), ('Baroque', 'Exotic captures'), ('Benedict', 'Change colors'), ('Berolina', 'Pawns move diagonally'), diff --git a/server/models/Game.js b/server/models/Game.js index 5f332b75..85119722 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -454,7 +454,7 @@ const GameModel = "SELECT gid, count(*) AS nbMoves, MAX(played) AS lastMaj " + "FROM Moves " + "GROUP BY gid"; - db.get(query, (err2, mstats) => { + db.all(query, (err2, mstats) => { // Reorganize moves data to avoid too many array lookups: let movesGroups = {}; mstats.forEach(ms => { @@ -464,18 +464,18 @@ const GameModel = }; }); // Remove games still not really started, - // with no action in the last 3 months: + // with no action in the last 2 weeks: let toRemove = []; games.forEach(g => { if ( ( !movesGroups[g.id] && - tsNow - g.created > 91*day + tsNow - g.created > 14*day ) || ( movesGroups[g.id].nbMoves == 1 && - tsNow - movesGroups[g.id].lastMaj > 91*day + tsNow - movesGroups[g.id].lastMaj > 14*day ) ) { toRemove.push(g.id);