From 6e0f28425075e6d2d79cab6d30bca6ce6d55f19d Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Sun, 17 May 2020 23:01:05 +0200 Subject: [PATCH] Add Koopa chess, fix Apocalypse and Dice variants --- client/public/images/pieces/Koopa/bc.svg | 1 + client/public/images/pieces/Koopa/bl.svg | 1 + client/public/images/pieces/Koopa/bo.svg | 1 + client/public/images/pieces/Koopa/bs.svg | 1 + client/public/images/pieces/Koopa/bt.svg | 1 + client/public/images/pieces/Koopa/bu.svg | 1 + client/public/images/pieces/Koopa/wc.svg | 1 + client/public/images/pieces/Koopa/wl.svg | 1 + client/public/images/pieces/Koopa/wo.svg | 1 + client/public/images/pieces/Koopa/ws.svg | 1 + client/public/images/pieces/Koopa/wt.svg | 1 + client/public/images/pieces/Koopa/wu.svg | 1 + client/src/components/Board.vue | 3 + client/src/translations/rules/Dice/en.pug | 6 +- client/src/translations/rules/Dice/es.pug | 6 +- client/src/translations/rules/Dice/fr.pug | 6 +- client/src/translations/rules/Koopa/en.pug | 54 +++- client/src/translations/rules/Koopa/es.pug | 54 ++++ client/src/translations/rules/Koopa/fr.pug | 54 ++++ client/src/utils/array.js | 2 +- client/src/variants/Alice.js | 3 +- client/src/variants/Apocalypse.js | 19 +- client/src/variants/Dice.js | 229 ++++++++-------- client/src/variants/Grand.js | 26 +- client/src/variants/Koopa.js | 291 ++++++++++++++++++--- client/src/views/Faq.vue | 1 + client/src/views/MyGames.vue | 2 +- server/models/User.js | 2 +- 28 files changed, 580 insertions(+), 190 deletions(-) create mode 120000 client/public/images/pieces/Koopa/bc.svg create mode 120000 client/public/images/pieces/Koopa/bl.svg create mode 120000 client/public/images/pieces/Koopa/bo.svg create mode 120000 client/public/images/pieces/Koopa/bs.svg create mode 120000 client/public/images/pieces/Koopa/bt.svg create mode 120000 client/public/images/pieces/Koopa/bu.svg create mode 120000 client/public/images/pieces/Koopa/wc.svg create mode 120000 client/public/images/pieces/Koopa/wl.svg create mode 120000 client/public/images/pieces/Koopa/wo.svg create mode 120000 client/public/images/pieces/Koopa/ws.svg create mode 120000 client/public/images/pieces/Koopa/wt.svg create mode 120000 client/public/images/pieces/Koopa/wu.svg diff --git a/client/public/images/pieces/Koopa/bc.svg b/client/public/images/pieces/Koopa/bc.svg new file mode 120000 index 00000000..b30a26ad --- /dev/null +++ b/client/public/images/pieces/Koopa/bc.svg @@ -0,0 +1 @@ +../Alice/bc.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/bl.svg b/client/public/images/pieces/Koopa/bl.svg new file mode 120000 index 00000000..a10d9e0a --- /dev/null +++ b/client/public/images/pieces/Koopa/bl.svg @@ -0,0 +1 @@ +../Alice/bl.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/bo.svg b/client/public/images/pieces/Koopa/bo.svg new file mode 120000 index 00000000..1200186b --- /dev/null +++ b/client/public/images/pieces/Koopa/bo.svg @@ -0,0 +1 @@ +../Alice/bo.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/bs.svg b/client/public/images/pieces/Koopa/bs.svg new file mode 120000 index 00000000..e8cf23a8 --- /dev/null +++ b/client/public/images/pieces/Koopa/bs.svg @@ -0,0 +1 @@ +../Alice/bs.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/bt.svg b/client/public/images/pieces/Koopa/bt.svg new file mode 120000 index 00000000..c517549b --- /dev/null +++ b/client/public/images/pieces/Koopa/bt.svg @@ -0,0 +1 @@ +../Alice/bt.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/bu.svg b/client/public/images/pieces/Koopa/bu.svg new file mode 120000 index 00000000..09e6ea3e --- /dev/null +++ b/client/public/images/pieces/Koopa/bu.svg @@ -0,0 +1 @@ +../Alice/bu.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/wc.svg b/client/public/images/pieces/Koopa/wc.svg new file mode 120000 index 00000000..d23af91d --- /dev/null +++ b/client/public/images/pieces/Koopa/wc.svg @@ -0,0 +1 @@ +../Alice/wc.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/wl.svg b/client/public/images/pieces/Koopa/wl.svg new file mode 120000 index 00000000..51c1893a --- /dev/null +++ b/client/public/images/pieces/Koopa/wl.svg @@ -0,0 +1 @@ +../Alice/wl.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/wo.svg b/client/public/images/pieces/Koopa/wo.svg new file mode 120000 index 00000000..4a85712d --- /dev/null +++ b/client/public/images/pieces/Koopa/wo.svg @@ -0,0 +1 @@ +../Alice/wo.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/ws.svg b/client/public/images/pieces/Koopa/ws.svg new file mode 120000 index 00000000..659b2de0 --- /dev/null +++ b/client/public/images/pieces/Koopa/ws.svg @@ -0,0 +1 @@ +../Alice/ws.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/wt.svg b/client/public/images/pieces/Koopa/wt.svg new file mode 120000 index 00000000..447fc4fe --- /dev/null +++ b/client/public/images/pieces/Koopa/wt.svg @@ -0,0 +1 @@ +../Alice/wt.svg \ No newline at end of file diff --git a/client/public/images/pieces/Koopa/wu.svg b/client/public/images/pieces/Koopa/wu.svg new file mode 120000 index 00000000..c1403b33 --- /dev/null +++ b/client/public/images/pieces/Koopa/wu.svg @@ -0,0 +1 @@ +../Alice/wu.svg \ No newline at end of file diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index a83be0cc..5ed351a7 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -64,6 +64,9 @@ export default { lmHighlights[m.start.x + sizeX * m.start.y] = true; if (!m.end.noHighlight && V.OnBoard(m.end.x, m.end.y)) lmHighlights[m.end.x + sizeX * m.end.y] = true; + if (!!m.start.toplay) + // For Dice variant (at least?) + lmHighlights[m.start.toplay[0] + sizeX * m.start.toplay[1]] = true; }); } const showLight = ( diff --git a/client/src/translations/rules/Dice/en.pug b/client/src/translations/rules/Dice/en.pug index 91be2d1a..f2f51a19 100644 --- a/client/src/translations/rules/Dice/en.pug +++ b/client/src/translations/rules/Dice/en.pug @@ -2,9 +2,9 @@ p.boxed. Play the piece type determined by a dice roll. p. - At each turn, click on any empty square first: you will see a piece type - written in the moves list, and a piece of this nature will be highlighted on - the board. You then have to play a move with this piece's type. + The first white move is chosen freely. Then, at each turn, you must play a + piece of the highlighted type on the board. + The piece's type to play is indicated in the moves list as well. p There is no check or checkmate: the goal is to capture the king. diff --git a/client/src/translations/rules/Dice/es.pug b/client/src/translations/rules/Dice/es.pug index 1799c531..ca70e8a2 100644 --- a/client/src/translations/rules/Dice/es.pug +++ b/client/src/translations/rules/Dice/es.pug @@ -2,9 +2,9 @@ p.boxed. Juega la pieza cuyo tipo está determinado por una tirada de dado. p. - En cada turno, haga clic en una casilla vacía: verá un tipo de pieza - escrito en la lista de jugadas, y se indicará una pieza de esta naturaleza - en el tablero. Luego debes jugar un movimiento con una pieza de este tipo. + El primer movimiento blanco se elige libremente. Luego, en cada ronda, usted + debe jugar una pieza del tipo resaltado en el tablero de ajedrez. + El tipo de pieza a jugar también se indica en la lista de movimientos. p No hay jaque ni jaque mate: el objetivo es capturar al rey. diff --git a/client/src/translations/rules/Dice/fr.pug b/client/src/translations/rules/Dice/fr.pug index a5d852f5..d637792b 100644 --- a/client/src/translations/rules/Dice/fr.pug +++ b/client/src/translations/rules/Dice/fr.pug @@ -2,9 +2,9 @@ p.boxed. Jouez la pièce dont le type est déterminé par un lancer de dé. p. - À chaque tour, cliquez sur une case vide : vous verrez un type de pièce - écrit dans la liste des coups, et une pièce de cette nature sera indiquée - sur l'échiquier. Vous devez ensuite jouer un coup avec une pièce de ce type. + Le premier coup blanc est choisi librement. Ensuite, à chaque tour, vous + devez jouer une pièce du type mis en valeur sur l'échiquier. + Le type de pièce à jouer est également indiqué dans la liste des coups. p Il n'y a ni échec ni mat : l'objectif est de capturer le roi. diff --git a/client/src/translations/rules/Koopa/en.pug b/client/src/translations/rules/Koopa/en.pug index 25cdf1d7..ae7169b8 100644 --- a/client/src/translations/rules/Koopa/en.pug +++ b/client/src/translations/rules/Koopa/en.pug @@ -1,3 +1,51 @@ -// TODO -// Pritchard middle page 45, section 3.6 -//https://www.chessvariants.com/crossover.dir/koopachess.html +p.boxed + | Captured pieces are stunned. They disappear if you capture them again. + +p + | This variant is inspired by the + a(href="https://en.wikipedia.org/wiki/Super_Mario") Super Mario + |  universe. + | When a piece captures another, it "bounce" on it until the next square in + | the movement's direction. If this next square is occupied, then it keeps + | bouncing and reach the following square, until either a free square or the + | edge of the board is met. In this last case, the capturer is lost. + +p. + Pieces bounced over are then "stunned" for two moves (four half-moves): + they cannot move during this period. If they get captured again while being + stunned, they are "kicked" out of the board, and all pieces standing on their + way vanish as well. + +figure.diagram-container + .diagram.diag12 + | fen:nrkqnrbb/ppp2ppp/3p4/4p3/1P3P2/8/P1PPP1PP/BNQRNBKR: + .diagram.diag22 + | fen:nukqnrbb/pps2ppp/3s4/4s3/1P6/8/P1PPP1PP/BNQRNBKR: + figcaption Before and after 1.fxe5 + +p. + After the move 1.fxe5 on the diagram, the red pieces are stunned for two + moves. So white can then play 2.Bxe5, kicking the pawn and capturing (for + real) at least the h8 bishop. An option for black may be 1...Qg5, such + that after 2.Bxe5 Qxe5 black threaten to kick the white stunned bishop out. + +figure.diagram-container + .diagram.diag12 + | fen:nuk1nrb1/pps2p1p/3s4/4B1q1/1P6/8/P1PPP1PP/1NQRNBKR: + .diagram.diag22 + | fen:nuk1nrb1/pps2p1p/3s4/3qC3/1P6/8/P1PPP1PP/1NQRNBKR: + figcaption After 1...Qg5 2.Bxe5 Qxe5: white bishop is stunned. + +p. + The goal is to capture the enemy king. Moves which kick your own king + out are forbidden, but stunning him is allowed. + +h3 Source + +p + a(href="https://www.jsbeasley.co.uk/encyc.htm") + | The Classified Encyclopedia of Chess Variants + | , section 3.6. This variant is also listed on chessvariants.com: + a(href="https://www.chessvariants.com/crossover.dir/koopachess.html") + | Koopa chess + | . diff --git a/client/src/translations/rules/Koopa/es.pug b/client/src/translations/rules/Koopa/es.pug index e69de29b..e02cdfd2 100644 --- a/client/src/translations/rules/Koopa/es.pug +++ b/client/src/translations/rules/Koopa/es.pug @@ -0,0 +1,54 @@ +p.boxed + | Las piezas capturadas quedan aturdidas. + | Desaparecerán si los captura de nuevo. + +p + | Esta variante está inspirada en el universo + a(href="https://fr.wikipedia.org/wiki/Super_Mario") Super Mario + | . Cuando una pieza captura a otra, "rebota" sobre ella hasta que + | siguiente casilla en la dirección del viaje. Si está ocupada + | entonces la pieza continúa rebotando en la misma dirección, hasta que + | encontrarse con una casilla vacía o el borde del tablero. En este ultimo + | caso, la pieza de captura se pierde. + +p. + Las piezas que rebotan son aturdidas por dos jugadas + (cuatro medios movimientos): no pueden moverse durante este período. + Si son capturados nuevamente durante su aturdimiento, entonces + son enviados fuera del tablero de ajedrez, y todas las piezas en camino + también desaparecen. + +figure.diagram-container + .diagram.diag12 + | fen:nrkqnrbb/ppp2ppp/3p4/4p3/1P3P2/8/P1PPP1PP/BNQRNBKR: + .diagram.diag22 + | fen:nukqnrbb/pps2ppp/3s4/4s3/1P6/8/P1PPP1PP/BNQRNBKR: + figcaption Antes y después de 1.fxe5 + +p. + Después del movimiento 1.fxe5 en el diagrama, las piezas rojas quedan + aturdidas por dos jugadas. Entonces las blancas pueden jugar 2.Bxe5, + sacando a relucir el peón que lleva al menos el loco h8 con él. + Una posibilidad para las negras serían jugar 1...Qg5, de modo que después + de 2.Bxe5 Qxe5 las negras amenacen para sacar el alfil mareado. + +figure.diagram-container + .diagram.diag12 + | fen:nuk1nrb1/pps2p1p/3s4/4B1q1/1P6/8/P1PPP1PP/1NQRNBKR: + .diagram.diag22 + | fen:nuk1nrb1/pps2p1p/3s4/3qC3/1P6/8/P1PPP1PP/1NQRNBKR: + figcaption Después de 1...Qg5 2.Bxe5 Qxe5: el alfil blanco está mareado. + +p. + El objetivo es capturar al rey contrario. Los movimientos que hacen sacar + tu propio rey están prohibidos. + +h3 Fuente + +p + a(href="https://www.jsbeasley.co.uk/encyc.htm") + | The Classified Encyclopedia of Chess Variants + | , sección 3.6. Esta variante también aparece en chessvariants.com: + a(href="https://www.chessvariants.com/crossover.dir/koopachess.html") + | Koopa chess + | . diff --git a/client/src/translations/rules/Koopa/fr.pug b/client/src/translations/rules/Koopa/fr.pug index e69de29b..b0be96d2 100644 --- a/client/src/translations/rules/Koopa/fr.pug +++ b/client/src/translations/rules/Koopa/fr.pug @@ -0,0 +1,54 @@ +p.boxed + | Les pièces capturées sont étourdies. + | Elles disparaissent si vous les capturez à nouveau. + +p + | Cette variante est inspirée de l'univers + a(href="https://fr.wikipedia.org/wiki/Super_Mario") Super Mario + | . Quand une pièce en capture une autre, elle "rebondit" dessus jusqu'à la + | case suivante dans la direction du déplacement. Si celle-ci est occupée, + | alors la pièce continue de rebondir dans la même direction, jusqu'à + | rencontrer une case vide ou bien le bord de l'échiquier. Dans ce dernier + | cas, la pièce capturante est perdue. + +p. + Les pièces sur lesquelles on rebondit sont étourdies pendant deux coups + (quatre demi-coups) : elles ne peuvent pas bouger pendant cette période. + Si elles sont capturées à nouveau pendant leur étourdissement, alors elles + sont envoyées en dehors de l'échiquier, et toutes les pièces sur leur chemin + disparaissent également. + +figure.diagram-container + .diagram.diag12 + | fen:nrkqnrbb/ppp2ppp/3p4/4p3/1P3P2/8/P1PPP1PP/BNQRNBKR: + .diagram.diag22 + | fen:nukqnrbb/pps2ppp/3s4/4s3/1P6/8/P1PPP1PP/BNQRNBKR: + figcaption Avant et après 1.fxe5 + +p. + Après le coup 1.fxe5 sur le diagramme, les pièces rouges sont étourdies + pendant deux coups. Ainsi les blancs peuvent jouer 2.Bxe5, faisant sortir + le pion qui emmène au moins le fou h8 avec lui. Une possibilité pour les + noirs serait de jouer 1...Qg5, pour qu'après 2.Bxe5 Qxe5 les noirs menacent + de faire sortir le fou étourdi. + +figure.diagram-container + .diagram.diag12 + | fen:nuk1nrb1/pps2p1p/3s4/4B1q1/1P6/8/P1PPP1PP/1NQRNBKR: + .diagram.diag22 + | fen:nuk1nrb1/pps2p1p/3s4/3qC3/1P6/8/P1PPP1PP/1NQRNBKR: + figcaption Après 1...Qg5 2.Bxe5 Qxe5 : le fou blanc est étourdi. + +p. + L'objectif est de capturer le roi adverse. Les coups qui font sortir votre + propre roi sont interdits. + +h3 Source + +p + a(href="https://www.jsbeasley.co.uk/encyc.htm") + | The Classified Encyclopedia of Chess Variants + | , section 3.6. Cette variante est également listée sur chessvariants.com : + a(href="https://www.chessvariants.com/crossover.dir/koopachess.html") + | Koopa chess + | . diff --git a/client/src/utils/array.js b/client/src/utils/array.js index 4d44db02..d02552c1 100644 --- a/client/src/utils/array.js +++ b/client/src/utils/array.js @@ -4,7 +4,7 @@ export const ArrayFun = { const index = arr.findIndex(rfun); if (index >= 0) { arr.splice(index, 1); - if (all) { + if (!!all) { // Reverse loop because of the splice below for (let i = arr.length - 1; i >= index; i--) { if (rfun(arr[i])) arr.splice(i, 1); diff --git a/client/src/variants/Alice.js b/client/src/variants/Alice.js index a3d3ff43..82ba6b79 100644 --- a/client/src/variants/Alice.js +++ b/client/src/variants/Alice.js @@ -361,10 +361,9 @@ export class AliceRules extends ChessRules { // Piece or pawn movement let notation = piece.toUpperCase() + pawnMark + captureMark + finalSquare; - if (["s", "p"].includes(piece) && !["s", "p"].includes(move.appear[0].p)) { + if (["s", "p"].includes(piece) && !["s", "p"].includes(move.appear[0].p)) // Promotion notation += "=" + move.appear[0].p.toUpperCase(); - } return notation; } }; diff --git a/client/src/variants/Apocalypse.js b/client/src/variants/Apocalypse.js index addf5dda..2439ae5d 100644 --- a/client/src/variants/Apocalypse.js +++ b/client/src/variants/Apocalypse.js @@ -150,8 +150,7 @@ export class ApocalypseRules extends ChessRules { start: this.whiteMove.start, end: this.whiteMove.end, appear: this.whiteMove.appear, - vanish: this.whiteMove.vanish, - illegal: this.whiteMove.illegal + vanish: this.whiteMove.vanish }); } @@ -181,7 +180,7 @@ export class ApocalypseRules extends ChessRules { const mHash = "m" + vm.start.x + vm.start.y + vm.end.x + vm.end.y; if (!moveSet[mHash]) { moveSet[mHash] = true; - vm.illegal = true; //potentially illegal! + vm.end.illegal = true; //potentially illegal! speculations.push(vm); } }); @@ -283,7 +282,7 @@ export class ApocalypseRules extends ChessRules { m.vanish[1].c != m.vanish[0].c || // Self-capture attempt ( - !other.illegal && + !other.end.illegal && other.end.x == m.end.x && other.end.y == m.end.y ) @@ -292,7 +291,7 @@ export class ApocalypseRules extends ChessRules { || ( m.vanish[0].p == V.PAWN && - !other.illegal && + !other.end.illegal && ( ( // Promotion attempt @@ -319,14 +318,14 @@ export class ApocalypseRules extends ChessRules { ) ); }; - if (!!m1.illegal && !isPossible(m1, m2)) { + if (!!m1.end.illegal && !isPossible(m1, m2)) { // Either an anticipated capture of something which didn't move // (or not to the right square), or a push through blocus. // ==> Just discard the move, and add a penalty point this.penaltyFlags[m1.vanish[0].c]++; m1.isNull = true; } - if (!!m2.illegal && !isPossible(m2, m1)) { + if (!!m2.end.illegal && !isPossible(m2, m1)) { this.penaltyFlags[m2.vanish[0].c]++; m2.isNull = true; } @@ -375,8 +374,8 @@ export class ApocalypseRules extends ChessRules { let remain = null; const p1 = m1.vanish[0].p; const p2 = m2.vanish[0].p; - if (!!m1.illegal && !m2.illegal) remain = { c: 'w', p: p1 }; - else if (!!m2.illegal && !m1.illegal) remain = { c: 'b', p: p2 }; + if (!!m1.end.illegal && !m2.end.illegal) remain = { c: 'w', p: p1 }; + else if (!!m2.end.illegal && !m1.end.illegal) remain = { c: 'b', p: p2 }; if (!remain) { // Either both are illegal or both are legal if (p1 == V.KNIGHT && p2 == V.PAWN) remain = { c: 'w', p: p1 }; @@ -478,7 +477,7 @@ export class ApocalypseRules extends ChessRules { let illegalMoves = []; moves.forEach(m => { // Warning: m might be illegal! - if (!m.illegal) { + if (!m.end.illegal) { V.PlayOnBoard(this.board, m); m.eval = this.evalPosition(); V.UndoOnBoard(this.board, m); diff --git a/client/src/variants/Dice.js b/client/src/variants/Dice.js index 68d80bb4..1b84be42 100644 --- a/client/src/variants/Dice.js +++ b/client/src/variants/Dice.js @@ -6,59 +6,139 @@ export class DiceRules extends ChessRules { return false; } - doClick(square) { - if ( - this.subTurn == 2 || - isNaN(square[0]) || - this.board[square[0]][square[1]] != V.EMPTY - ) { - return null; - } - // Announce the piece' type to be played: - return this.getRandPieceMove(); + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { toplay: fenParts[5] } + ); } - getPotentialMovesFrom([x, y]) { - if (this.subTurn == 1) return []; + setOtherVariables(fen) { + super.setOtherVariables(fen); + this.p2play = []; + const toplay = V.ParseFen(fen).toplay; + if (toplay != "-") this.p2play.push(toplay); + } + + getFen() { + return super.getFen() + " " + this.getToplayFen(); + } + + getFen() { + return super.getFenForRepeat() + "_" + this.getToplayFen(); + } + + getToplayFen() { const L = this.p2play.length; - const piece = this.getPiece(x, y); - if (piece == V.PAWN && this.p2play[L-1] != V.PAWN) { - // The piece must be a pawn about to promote. - const color = this.turn; - const beforeLastRank = (color == 'w' ? 1 : 0); + return (L > 0 ? this.p2play[L-1] : "-"); + } + + static GenRandInitFen(randomness) { + return ChessRules.GenRandInitFen(randomness) + " -"; + } + + canMove(piece, color, [x, y]) { + const oppCol = V.GetOppCol(color); + if (piece == V.PAWN) { const forward = (color == 'w' ? -1 : 1); - let moves = []; - if (this.board[x + forward][y] == V.EMPTY) { - moves.push( - this.getBasicMove( - [x, y], [x + forward], { c: color, p: this.p2play[L-1] }) - ); - } + if (this.board[x + forward][y] == V.EMPTY) return true; for (let shift of [-1, 1]) { const [i, j] = [x + forward, y + shift]; if ( V.OnBoard(i, j) && this.board[i][j] != V.EMPTY && - this.getColor(i, j) != color + this.getColor(i, j) == oppCol ) { + return true; + } + } + } + else { + const steps = + [V.KING, V.QUEEN].includes(piece) + ? V.steps[V.ROOK].concat(V.steps[V.BISHOP]) + : V.steps[piece]; + 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) == oppCol) + ) { + return true; + } + } + } + return false; + } + + getRandPiece(color) { + // Find pieces which can move and roll a dice + let canMove = {}; + for (let i=0; i<8; i++) { + for (let j=0; j<8; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + const piece = this.getPiece(i, j); + if (!canMove[piece] && this.canMove(piece, color, [i, j])) + canMove[piece] = [i, j]; + } + } + } + const options = Object.keys(canMove); + const randPiece = options[randInt(options.length)]; + return [randPiece, canMove[randPiece]]; + } + + getPotentialMovesFrom([x, y]) { + const color = this.turn; + let moves = undefined; + if (this.movesCount == 0) moves = super.getPotentialMovesFrom([x, y]); + else { + const L = this.p2play.length; //L is >= 1 + const piece = this.getPiece(x, y); + if ( + piece == V.PAWN && + this.p2play[L-1] != V.PAWN && + ((color == 'w' && x == 1) || (color == 'b' && x == 6)) + ) { + // The piece is a pawn about to promote + const destX = (color == 'w' ? 0 : 7); + moves = []; + if (this.board[destX][y] == V.EMPTY) { moves.push( this.getBasicMove( - [x, y], [i, j], { c: color, p: this.p2play[L-1] }) + [x, y], [destX, y], { c: color, p: this.p2play[L-1] }) ); } + for (let shift of [-1, 1]) { + const [i, j] = [destX, y + shift]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) != color + ) { + moves.push( + this.getBasicMove( + [x, y], [i, j], { c: color, p: this.p2play[L-1] }) + ); + } + } } - return moves; + else if (piece != this.p2play[L-1]) + // The piece type must match last p2play + return []; + else moves = super.getPotentialMovesFrom([x, y]); } - if (piece != this.p2play[L-1]) - // The piece type must match last p2play - return []; - return super.getPotentialMovesFrom([x, y]); - } - - setOtherVariables(fen) { - super.setOtherVariables(fen); - this.p2play = []; - this.subTurn = 1; + // Decide which piece the opponent will play: + const oppCol = V.GetOppCol(color); + moves.forEach(m => { + V.PlayOnBoard(this.board, m); + const [piece, square] = this.getRandPiece(oppCol); + m.start.toplay = square; + m.end.piece = piece; + V.UndoOnBoard(this.board, m); + }); + return moves; } filterValid(moves) { @@ -75,91 +155,26 @@ export class DiceRules extends ChessRules { return "*"; } - play(move) { - if (this.subTurn == 1) { - this.subTurn = 2; - this.p2play.push(move.appear[0].p); - return; - } - // Subturn == 2 means the (dice-constrained) move is played - move.flags = JSON.stringify(this.aggregateFlags()); - V.PlayOnBoard(this.board, move); - this.epSquares.push(this.getEpSquare(move)); - this.movesCount++; - this.turn = V.GetOppCol(this.turn); - this.subTurn = 1; - this.postPlay(move); - } - postPlay(move) { + this.p2play.push(move.end.piece); if (move.vanish.length == 2 && move.vanish[1].p == V.KING) this.kingPos[move.vanish[1].c] = [-1, -1]; // Castle flags for captured king won't be updated (not important...) super.postPlay(move); } - undo(move) { - if (this.subTurn == 2) { - this.subTurn = 1; - this.p2play.pop(); - return; - } - this.disaggregateFlags(JSON.parse(move.flags)); - V.UndoOnBoard(this.board, move); - this.epSquares.pop(); - this.movesCount--; - this.turn = V.GetOppCol(this.turn); - this.subTurn = 2; - this.postUndo(move); - } - postUndo(move) { + this.p2play.pop(); if (move.vanish.length == 2 && move.vanish[1].p == V.KING) this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y]; super.postUndo(move); } - getRandPieceMove() { - // For current turn, find pieces which can move and roll a dice - let canMove = {}; - const color = this.turn; - for (let i=0; i<8; i++) { - for (let j=0; j<8; j++) { - if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { - const piece = this.getPiece(i, j); - if ( - !canMove[piece] && - super.getPotentialMovesFrom([i, j]).length > 0 - ) { - canMove[piece] = [i, j]; - } - } - } - } - const options = Object.keys(canMove); - const randPiece = options[randInt(options.length)]; - return ( - new Move({ - appear: [{ p: randPiece }], - vanish: [], - start: { x: -1, y: -1 }, - end: { x: canMove[randPiece][0], y: canMove[randPiece][1] } - }) - ); - } - - // Random mover - getComputerMove() { - const toPlay = this.getRandPieceMove(); - this.play(toPlay); - const moves = this.getAllValidMoves(); - const choice = moves[randInt(moves.length)]; - this.undo(toPlay); - return [toPlay, choice]; + static get SEARCH_DEPTH() { + return 1; } getNotation(move) { - if (this.subTurn == 1) return move.appear[0].p.toUpperCase(); - return super.getNotation(move); + return super.getNotation(move) + "/" + move.end.piece.toUpperCase(); } }; diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js index 743f01f9..b88410c9 100644 --- a/client/src/variants/Grand.js +++ b/client/src/variants/Grand.js @@ -55,24 +55,24 @@ export class GrandRules extends ChessRules { setOtherVariables(fen) { super.setOtherVariables(fen); - const fenParsed = V.ParseFen(fen); + const captured = V.ParseFen(fen).captured.split("").map(parseInt); // Initialize captured pieces' counts from FEN this.captured = { w: { - [V.ROOK]: parseInt(fenParsed.captured[0]), - [V.KNIGHT]: parseInt(fenParsed.captured[1]), - [V.BISHOP]: parseInt(fenParsed.captured[2]), - [V.QUEEN]: parseInt(fenParsed.captured[3]), - [V.MARSHALL]: parseInt(fenParsed.captured[4]), - [V.CARDINAL]: parseInt(fenParsed.captured[5]) + [V.ROOK]: captured[0], + [V.KNIGHT]: captured[1], + [V.BISHOP]: captured[2], + [V.QUEEN]: captured[3], + [V.MARSHALL]: captured[4], + [V.CARDINAL]: captured[5] }, b: { - [V.ROOK]: parseInt(fenParsed.captured[6]), - [V.KNIGHT]: parseInt(fenParsed.captured[7]), - [V.BISHOP]: parseInt(fenParsed.captured[8]), - [V.QUEEN]: parseInt(fenParsed.captured[9]), - [V.MARSHALL]: parseInt(fenParsed.captured[10]), - [V.CARDINAL]: parseInt(fenParsed.captured[11]) + [V.ROOK]: captured[6], + [V.KNIGHT]: captured[7], + [V.BISHOP]: captured[8], + [V.QUEEN]: captured[9], + [V.MARSHALL]: captured[10], + [V.CARDINAL]: captured[11] } }; } diff --git a/client/src/variants/Koopa.js b/client/src/variants/Koopa.js index 9ca83848..f24e5b2f 100644 --- a/client/src/variants/Koopa.js +++ b/client/src/variants/Koopa.js @@ -1,22 +1,113 @@ -import { ChessRulesi, PiPo } from "@/base_rules"; +import { ChessRules, PiPo } from "@/base_rules"; export class KoopaRules extends ChessRules { static get HasEnpassant() { return false; } - // Between stun time and stun + 1 move - static get STUNNED_1() { + static get STUNNED() { return ['s', 'u', 'o', 'c', 't', 'l']; } - // Between stun + 1 move and stun + 2 moves - static get STUNNED_2() { - return ['v', 'x', 'a', 'd', 'w', 'm']; + static get PIECES() { + return ChessRules.PIECES.concat(V.STUNNED); } - static get PIECES() { - return ChessRules.PIECES.concat(V.STUNNED_1).concat(V.STUNNED_2); + static ParseFen(fen) { + let res = ChessRules.ParseFen(fen); + const fenParts = fen.split(" "); + res.stunned = fenParts[4]; + return res; + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 5) Check "stunned" + if ( + !fenParsed.stunned || + ( + fenParsed.stunned != "-" && + !fenParsed.stunned.match(/^([a-h][1-8][1-4],?)*$/) + ) + ) { + return false; + } + return true; + } + + getPpath(b) { + return (V.STUNNED.includes(b[1]) ? "Koopa/" : "") + b; + } + + getFen() { + return super.getFen() + " " + this.getStunnedFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getStunnedFen(); + } + + getStunnedFen() { + return ( + Object.keys(this.stunned) + .map(square => square + this.stunned[square]) + .join(",") + ); + } + + // Base GenRandInitFen() is fine because en-passant indicator will + // stand for stunned indicator. + + scanKings(fen) { + this.INIT_COL_KING = { w: -1, b: -1 }; + // Squares of white and black king: + this.kingPos = { w: [-1, -1], b: [-1, -1] }; + const fenRows = V.ParseFen(fen).position.split("/"); + const startRow = { 'w': V.size.x - 1, 'b': 0 }; + for (let i = 0; i < fenRows.length; i++) { + let k = 0; //column index on board + for (let j = 0; j < fenRows[i].length; j++) { + switch (fenRows[i].charAt(j)) { + case "k": + case "l": + this.kingPos["b"] = [i, k]; + this.INIT_COL_KING["b"] = k; + break; + case "K": + case "L": + this.kingPos["w"] = [i, k]; + this.INIT_COL_KING["w"] = k; + break; + default: { + const num = parseInt(fenRows[i].charAt(j)); + if (!isNaN(num)) k += num - 1; + } + } + k++; + } + } + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + let stunnedArray = []; + const stunnedFen = V.ParseFen(fen).stunned; + if (stunnedFen != "-") { + stunnedArray = + stunnedFen + .split(",") + .map(s => { + return { + square: s.substr(0, 2), + state: parseInt(s[2]) + }; + }); + } + this.stunned = {}; + stunnedArray.forEach(s => { + this.stunned[s.square] = s.state; + }); } getNormalizedStep(step) { @@ -31,13 +122,14 @@ export class KoopaRules extends ChessRules { getPotentialMovesFrom([x, y]) { let moves = super.getPotentialMovesFrom([x, y]); // Complete moves: stuns & kicks - const stun = V.STUNNED_1.concat(V.STUNNED_2); + let promoteAfterStun = []; + const color = this.turn; moves.forEach(m => { if (m.vanish.length == 2 && m.appear.length == 1) { const step = this.getNormalizedStep([m.end.x - m.start.x, m.end.y - m.start.y]); // "Capture" something: is target stunned? - if (stun.includes(m.vanish[1].p)) { + if (V.STUNNED.includes(m.vanish[1].p)) { // Kick it: continue movement in the same direction, // destroying all on its path. let [i, j] = [m.end.x + step[0], m.end.y + step[1]]; @@ -58,32 +150,35 @@ export class KoopaRules extends ChessRules { } else { // The piece is now stunned - m.appear.push(m.vanish.pop()); + m.appear.push(JSON.parse(JSON.stringify(m.vanish[1]))); const pIdx = ChessRules.PIECES.findIndex(p => p == m.appear[1].p); - m.appear[1].p = V.STUNNED_1[pIdx]; + m.appear[1].p = V.STUNNED[pIdx]; // And the capturer continue in the same direction until an empty // square or the edge of the board, maybe stunning other pieces. let [i, j] = [m.end.x + step[0], m.end.y + step[1]]; while (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) { const colIJ = this.getColor(i, j); const pieceIJ = this.getPiece(i, j); - m.vanish.push( - new PiPo({ - x: i, - y: j, - c: colIJ, - p: pieceIJ - }) - ); - const pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ); - m.appear.push( - new PiPo({ - x: i, - y: j, - c: colIJ, - p: V.STUNNED_1[pIdx] - }) - ); + let pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ); + if (pIdx >= 0) { + // The piece isn't already stunned + m.vanish.push( + new PiPo({ + x: i, + y: j, + c: colIJ, + p: pieceIJ + }) + ); + m.appear.push( + new PiPo({ + x: i, + y: j, + c: colIJ, + p: V.STUNNED[pIdx] + }) + ); + } i += step[0]; j += step[1]; } @@ -91,27 +186,36 @@ export class KoopaRules extends ChessRules { m.appear[0].x = i; m.appear[0].y = j; // Is it a pawn on last rank? + if ((color == 'w' && i == 0) || (color == 'b' && i == 7)) { + m.appear[0].p = V.ROOK; + for (let ppiece of [V.KNIGHT, V.BISHOP, V.QUEEN]) { + let mp = JSON.parse(JSON.stringify(m)); + mp.appear[0].p = ppiece; + promoteAfterStun.push(mp); + } + } } - else { + else // The piece is out m.appear.shift(); - } } } }); - return moves; - } - - static GenRandInitFen(randomness) { - // No en-passant: - return ChessRules.GenRandInitFen(randomness).slice(0, -2); + return moves.concat(promoteAfterStun); } filterValid(moves) { // Forbid kicking own king out const color = this.turn; return moves.filter(m => { - return m.vanish.every(v => v.c != color || !(['l','m'].includes(v.p))); + const kingAppear = m.appear.some(a => a.c == color && a.p == V.KING); + return m.vanish.every(v => { + return ( + v.c != color || + !["k", "l"].includes(v.p) || + (v.p == "k" && kingAppear) + ); + }); }); } @@ -127,13 +231,114 @@ export class KoopaRules extends ChessRules { } postPlay(move) { - // TODO: toutes les pièces "stunned" by me (turn) avancent d'un niveau - // --> alter board - move.wasStunned = array of stunned stage 2 pieces (just back to normal then) + // Base method is fine because a stunned king (which won't be detected) + // can still castle after going back to normal. + super.postPlay(move); + const kIdx = move.vanish.findIndex(v => v.p == "l"); + if (kIdx >= 0) + // A stunned king vanish (game over) + this.kingPos[move.vanish[kIdx].c] = [-1, -1]; + move.stunned = JSON.stringify(this.stunned); + // Array of stunned stage 1 pieces (just back to normal then) + Object.keys(this.stunned).forEach(square => { + // All (formerly) stunned pieces progress by 1 level, if still on board + const coords = V.SquareToCoords(square); + const [x, y] = [coords.x, coords.y]; + if (V.STUNNED.includes(this.board[x][y][1])) { + // Stunned piece still on board + this.stunned[square]--; + if (this.stunned[square] == 0) { + delete this.stunned[square]; + const color = this.getColor(x, y); + const piece = this.getPiece(x, y); + const pIdx = V.STUNNED.findIndex(p => p == piece); + this.board[x][y] = color + ChessRules.PIECES[pIdx]; + } + } + else delete this.stunned[square]; + }); + // Any new stunned pieces? + move.appear.forEach(a => { + if (V.STUNNED.includes(a.p)) + // Set to maximum stun level: + this.stunned[V.CoordsToSquare({ x: a.x, y: a.y })] = 4; + }); } postUndo(move) { - if (wasStunned - STUNNED_2 + super.postUndo(move); + const kIdx = move.vanish.findIndex(v => v.p == "l"); + if (kIdx >= 0) { + // A stunned king vanished + this.kingPos[move.vanish[kIdx].c] = + [move.vanish[kIdx].x, move.vanish[kIdx].y]; + } + this.stunned = JSON.parse(move.stunned); + for (let i=0; i<8; i++) { + for (let j=0; j<8; j++) { + const square = V.CoordsToSquare({ x: i, y: j }); + const pieceIJ = this.getPiece(i, j); + if (!this.stunned[square]) { + const pIdx = V.STUNNED.findIndex(p => p == pieceIJ); + if (pIdx >= 0) + this.board[i][j] = this.getColor(i, j) + ChessRules.PIECES[pIdx]; + } + else { + const pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ); + if (pIdx >= 0) + this.board[i][j] = this.getColor(i, j) + V.STUNNED[pIdx]; + } + } + } + } + + static get VALUES() { + return Object.assign( + { + s: 1, + u: 5, + o: 3, + c: 3, + t: 9, + l: 1000 + }, + ChessRules.VALUES + ); + } + + static get SEARCH_DEPTH() { + return 2; + } + + getNotation(move) { + if ( + move.appear.length == 2 && + move.vanish.length == 2 && + move.appear.concat(move.vanish).every( + av => ChessRules.PIECES.includes(av.p)) && + move.appear[0].p == V.KING + ) { + if (move.end.y < move.start.y) return "0-0-0"; + return "0-0"; + } + const finalSquare = V.CoordsToSquare(move.end); + const piece = this.getPiece(move.start.x, move.start.y); + const captureMark = move.vanish.length >= 2 ? "x" : ""; + let pawnMark = ""; + if (piece == 'p' && captureMark.length == 1) + pawnMark = V.CoordToColumn(move.start.y); //start column + // Piece or pawn movement + let notation = + (piece == V.PAWN ? pawnMark : piece.toUpperCase()) + + captureMark + finalSquare; + if ( + piece == 'p' && + move.appear[0].c == move.vanish[0].c && + move.appear[0].p != 'p' + ) { + // Promotion + notation += "=" + move.appear[0].p.toUpperCase(); + } + return notation; } }; diff --git a/client/src/views/Faq.vue b/client/src/views/Faq.vue index 3376afd2..71647173 100644 --- a/client/src/views/Faq.vue +++ b/client/src/views/Faq.vue @@ -50,6 +50,7 @@ export default {