From 71ef1664983cd58db3c3bbfdf6cb7c362474e9a5 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Tue, 25 Feb 2020 02:54:51 +0100 Subject: [PATCH] Fixes + implement Cylinder Chess --- client/src/base_rules.js | 11 +- client/src/components/BaseGame.vue | 21 +- client/src/components/Board.vue | 4 +- client/src/translations/en.js | 2 + client/src/translations/es.js | 2 + client/src/translations/fr.js | 2 + client/src/translations/rules/Circular/en.pug | 31 +-- client/src/translations/rules/Circular/es.pug | 29 ++- client/src/translations/rules/Circular/fr.pug | 29 ++- client/src/translations/rules/Cylinder/en.pug | 24 +++ client/src/translations/rules/Cylinder/es.pug | 27 +++ client/src/translations/rules/Cylinder/fr.pug | 27 +++ client/src/translations/rules/Hidden/en.pug | 6 +- client/src/translations/rules/Hidden/es.pug | 6 +- client/src/translations/rules/Hidden/fr.pug | 6 +- client/src/variants/Checkered.js | 4 +- client/src/variants/Circular.js | 194 +++++++++++------- client/src/variants/Cylinder.js | 147 +++++++++++++ client/src/variants/Dark.js | 11 +- client/src/variants/Hidden.js | 50 ++++- client/src/variants/Suction.js | 24 +++ server/db/populate.sql | 1 + 22 files changed, 520 insertions(+), 138 deletions(-) create mode 100644 client/src/translations/rules/Cylinder/en.pug create mode 100644 client/src/translations/rules/Cylinder/es.pug create mode 100644 client/src/translations/rules/Cylinder/fr.pug create mode 100644 client/src/variants/Cylinder.js diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 4b9a139f..d5849f3b 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -60,6 +60,14 @@ export const ChessRules = class ChessRules { return V.ShowMoves; } + // Some variants always show the same orientation + static get CanFlip() { + return true; + } + get canFlip() { + return V.CanFlip; + } + // Turn "wb" into "B" (for FEN) static board2fen(b) { return b[0] == "w" ? b[1].toUpperCase() : b[1]; @@ -378,7 +386,6 @@ export const ChessRules = class ChessRules { setFlags(fenflags) { // white a-castle, h-castle, black a-castle, h-castle this.castleFlags = { w: [true, true], b: [true, true] }; - if (!fenflags) return; for (let i = 0; i < 4; i++) this.castleFlags[i < 2 ? "w" : "b"][i % 2] = fenflags.charAt(i) == "1"; } @@ -627,8 +634,8 @@ export const ChessRules = class ChessRules { x + shiftX == lastRank ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] : [V.PAWN]; - // One square forward if (this.board[x + shiftX][y] == V.EMPTY) { + // One square forward for (let piece of finalPieces) { moves.push( this.getBasicMove([x, y], [x + shiftX, y], { diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 47776de2..3573643d 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -45,7 +45,7 @@ div#baseGame( #controls button(@click="gotoBegin()") << button(@click="undo()") < - button(@click="flip()") ⇅ + button(v-if="canFlip" @click="flip()") ⇅ button(@click="play()") > button(@click="gotoEnd()") >> #belowControls @@ -121,16 +121,28 @@ export default { : (this.vr ? this.vr.showMoves : "none"); }, showTurn: function() { - return this.game.score == '*' && this.vr && this.vr.showMoves != "all"; + return ( + this.game.score == '*' && + this.vr && + (this.vr.showMoves != "all" || !this.vr.canFlip) + ); }, turn: function() { - return this.vr - ? this.st.tr[(this.vr.turn == 'w' ? "White" : "Black") + " to move"] + if (!this.vr) + return ""; + if (this.vr.showMoves != "all") + return this.st.tr[(this.vr.turn == 'w' ? "White" : "Black") + " to move"] + // Cannot flip: racing king or circular chess + return this.vr.movesCount == 0 && this.game.mycolor == "w" + ? this.st.tr["It's your turn!"] : ""; }, canAnalyze: function() { return this.game.mode != "analyze" && this.vr && this.vr.canAnalyze; }, + canFlip: function() { + return this.vr && this.vr.canFlip; + }, allowDownloadPGN: function() { return this.game.score != "*" || (this.vr && this.vr.showMoves == "all"); } @@ -444,6 +456,7 @@ export default { #controls margin: 0 auto + text-align: center button display: inline-block width: 20% diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index d4423464..bf43051a 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -59,7 +59,7 @@ export default { } }, [...Array(sizeX).keys()].map(i => { - let ci = this.orientation == "w" ? i : sizeX - i - 1; + let ci = !V.CanFlip || this.orientation == "w" ? i : sizeX - i - 1; return h( "div", { @@ -69,7 +69,7 @@ export default { style: { opacity: this.choices.length > 0 ? "0.5" : "1" } }, [...Array(sizeY).keys()].map(j => { - let cj = this.orientation == "w" ? j : sizeY - j - 1; + let cj = !V.CanFlip || this.orientation == "w" ? j : sizeY - j - 1; let elems = []; if ( this.vr.board[ci][cj] != V.EMPTY && diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 09fa92fe..963380a7 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -42,6 +42,7 @@ export const translations = { "Highlight last move and checks?": "Highlight last move and checks?", Instructions: "Instructions", "Invalid email": "Invalid email", + "It's your turn!": "It's your turn!", "is not online": "is not online", Language: "Language", "Live challenges": "Live challenges", @@ -144,6 +145,7 @@ export const translations = { "Lose all pieces": "Lose all pieces", "Mate any piece": "Mate any piece", "Move twice": "Move twice", + "Neverending rows": "Neverending rows", "Pawns move diagonally": "Pawns move diagonally", "Reuse pieces": "Reuse pieces", "Reverse captures": "Reverse captures", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 286ae428..8b71fb4c 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -42,6 +42,7 @@ export const translations = { "Highlight last move and checks?": "¿Resaltar el último movimiento y jaques?", Instructions: "Instrucciones", "Invalid email": "Email inválido", + "It's your turn!": "¡Es su turno!", "is not online": "no está en línea", Language: "Idioma", "Live challenges": "Desafíos en vivo", @@ -144,6 +145,7 @@ export const translations = { "Lose all pieces": "Perder todas las piezas", "Mate any piece": "Matar cualquier pieza", "Move twice": "Mover dos veces", + "Neverending rows": "Filas interminables", "Pawns move diagonally": "Peones se mueven en diagonal", "Reuse pieces": "Reutilizar piezas", "Reverse captures": "Capturas invertidas", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 6746e708..6d0f82d6 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -42,6 +42,7 @@ export const translations = { "Highlight last move and checks?": "Mettre en valeur le dernier coup et les échecs ?", Instructions: "Instructions", "Invalid email": "Email invalide", + "It's your turn!": "À vous de jouer !", "is not online": "n'est pas en ligne", Language: "Langue", "Live challenges": "Défis en direct", @@ -144,6 +145,7 @@ export const translations = { "Lose all pieces": "Perdez toutes les pièces", "Mate any piece": "Mater n'importe quelle pièce", "Move twice": "Jouer deux coups", + "Neverending rows": "Rangées sans fin", "Pawns move diagonally": "Les pions vont en diagonale", "Reuse pieces": "Réutiliser les pièces", "Reverse captures": "Captures inversées", diff --git a/client/src/translations/rules/Circular/en.pug b/client/src/translations/rules/Circular/en.pug index 7e78b2f8..67639f66 100644 --- a/client/src/translations/rules/Circular/en.pug +++ b/client/src/translations/rules/Circular/en.pug @@ -1,26 +1,27 @@ -p https://www.chessvariants.com/d.betza/chessvar/race.html 8x8 race chess - -p See also: https://www.chessvariants.com/shape.dir/x_torus.html - p.boxed - | If a piece captures one of the same kind, both disappear. - -p. - The defensive power of pawns is thus increased, because they don't fear - captures (by other pawns). + | Ranks 1 and 8 communicate. Pawns all go the same way and never promote. p. - Endings are also affected quite a lot, and sometimes new threats occur: - on the diagram, 3.Bxg7 wins a pawn because 3...Bxg7 would make both - bishops disappear. + In the initial position, it is often possible to win in a few moves by + giving check first: that is why this is forbidden. figure.diagram-container .diagram - | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR: - figcaption After 1.b3 c5 2.Bb2 Nc6 + | fen:8/8/pppppppp/rkbrnnqb/8/8/PPPPPPPP/BBRNKQNR: + figcaption 1.Nc3+ would win quickly. + +p. + Note that since ranks 1 and 8 communicate, a black pawn on the 8th rank + threatens a piece on the first rank. For example pawn d6 to d8 would check + the white king. + +p. + Due to the unusual pawns rules (they all go the same way: up the board), + en passant captures do not exist. Castling is also disabled because + the king is vulnerable in both directions. h3 Source p - a(href="https://www.chessvariants.com/rules/antimatter-chess") Antimatter chess + a(href="https://www.chessvariants.com/d.betza/chessvar/race.html") 8x8 Race Chess |  on chessvariants.com. diff --git a/client/src/translations/rules/Circular/es.pug b/client/src/translations/rules/Circular/es.pug index b6adc808..dc119270 100644 --- a/client/src/translations/rules/Circular/es.pug +++ b/client/src/translations/rules/Circular/es.pug @@ -1,23 +1,30 @@ p.boxed - | Si una pieza captura otra del mismo tipo, las dos desaparecen. + | Las filas 1 y 8 se comunican. + | Todos los peones van en la misma dirección y nunca son promovidos. p. - El poder defensivo de los peones aumenta así, ya que no temen - más capturas (por otros peones). - -p. - Las finales también se ven muy afectadas y, a veces, nuevas amenazas - ocurren: en el diagrama, 3.Bxg7 gana un peón porque 3...Bxg7 causaría - la desaparición de los dos alfiles. + En la posición inicial, a menudo es posible ganar en unos pocos + movimientos que comienzan con jaque: por eso está prohibido. figure.diagram-container .diagram - | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR: - figcaption Después de 1.b3 c5 2.Bb2 Nc6 + | fen:8/8/pppppppp/rkbrnnqb/8/8/PPPPPPPP/BBRNKQNR: + figcaption 1.Nc3+ ganaría rápidamente. + +p. + Tenga en cuenta que dado que las filas 1 y 8 se comunican, un peón negro + en el octavo fila amenaza una pieza en el primero. + Por ejemplo, el peón d6 a d8 pondría en jaque el rey blanco. + +p. + Debido a las reglas de peones inusuales (todos van en la misma + dirección: arriba), no hay capturas en passant. + El enroque tampoco es posible porque el rey es vulnerable + en ambas direcciones. h3 Fuente p | La - a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimateria + a(href="https://www.chessvariants.com/d.betza/chessvar/race.html") variante 8x8 Race |  en chessvariants.com. diff --git a/client/src/translations/rules/Circular/fr.pug b/client/src/translations/rules/Circular/fr.pug index 7b1cdfae..8bc281fe 100644 --- a/client/src/translations/rules/Circular/fr.pug +++ b/client/src/translations/rules/Circular/fr.pug @@ -1,23 +1,30 @@ p.boxed - | Si une pièce en capture une autre du même type, les deux disparaissent. + | Les rangées 1 et 8 communiquent. + | Les pions vont tous dans le même sens et ne sont jamais promus. p. - Le pouvoir défensif des pions est ainsi augmenté, puisqu'ils ne craignent - plus les captures (par d'autres pions). - -p. - Les finales sont aussi beaucoup affectées, et parfois de nouvelles menaces - surviennent : sur le diagramme, 3.Bxg7 gagne un pion car 3...Bxg7 provoquerait - la disparition des deux fous. + Dans la position initiale, il est souvent possible de gagner en quelques + coups en commençant par donner échec : c'est pourquoi on l'interdit. figure.diagram-container .diagram - | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR: - figcaption After 1.b3 c5 2.Bb2 Nc6 + | fen:8/8/pppppppp/rkbrnnqb/8/8/PPPPPPPP/BBRNKQNR: + figcaption 1.Nc3+ gagnerait rapidement. + +p. + Notez que puisque les rangées 1 et 8 communiquent, un pion noir sur la 8eme + rangée menace une pièce sur la première. Par exemple pion d6 à d8 mettrait + en échec le roi blanc. + +p. + À cause des règles inhabituelles sur les pions (ils vont tous dans la même + direction : vers le haut), les captures en passant n'existent pas. + Le roque n'est pas possible non plus car le roi est vulnérable + dans les deux directions. h3 Source p | La - a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimatière + a(href="https://www.chessvariants.com/d.betza/chessvar/race.html") variante 8x8 Race |  sur chessvariants.com. diff --git a/client/src/translations/rules/Cylinder/en.pug b/client/src/translations/rules/Cylinder/en.pug new file mode 100644 index 00000000..0b436765 --- /dev/null +++ b/client/src/translations/rules/Cylinder/en.pug @@ -0,0 +1,24 @@ +p.boxed + | Columns 'a' and 'h' communicate. + +p. + Column 'a' is now just to the right of column 'h': a pawn on h2 can + take on a3, and a bishop on h3 can go to a4, and so on. + +p. + This allows a single piece to give a double check. On the following diagram + the bishop give check on b5 by two distinct paths: e2-d3-c4 and g2-h3-a4. + Covering both attacking lines is impossible so the king has to move. + +figure.diagram-container + .diagram.diag12 + | fen:8/2p5/3n4/1k6/8/8/2PP4/5B1K: + .diagram.diag22 + | fen:4k3/2p5/8/8/7N/8/3P4/7K a2,b3,b5,a6,g2,f3,f5,g6: + figcaption Left: double-check on b5. Right: possible knight moves from h4. + +h3 Source + +p + a(href="https://www.chessvariants.com/boardrules.dir/cylindrical.html") Cylinder Chess + |  on chessvariants.com. diff --git a/client/src/translations/rules/Cylinder/es.pug b/client/src/translations/rules/Cylinder/es.pug new file mode 100644 index 00000000..358a8e43 --- /dev/null +++ b/client/src/translations/rules/Cylinder/es.pug @@ -0,0 +1,27 @@ +p.boxed + | Las columnas 'a' y 'h' se comunican. + +p. + La columna 'a' está ahora a la derecha de la columna 'h': + un peón en h2 puede tomar a3, y un alfil en h3 puede ir en a4, etc. + +p. + Esto permite dobles jaques dadas por una sola pieza. En el diagrama + a continuación, el alfil da jaque por dos caminos diferentes: e2-d3-c4 y g2-h3-a4. + No es posible cubrir las dos líneas de ataque, por lo que el rey debe moverse. + +figure.diagram-container + .diagram.diag12 + | fen:8/2p5/3n4/1k6/8/8/2PP4/5B1K: + .diagram.diag22 + | fen:4k3/2p5/8/8/7N/8/3P4/7K a2,b3,b5,a6,g2,f3,f5,g6: + figcaption. + Izquierda: doble-jaque en b5. Derecha: posibles movimientos de caballo desde h4. + +h3 Fuente + +p + | La + a(href="https://www.chessvariants.com/boardrules.dir/cylindrical.html") + | variante cilíndrica + |  en chessvariants.com. diff --git a/client/src/translations/rules/Cylinder/fr.pug b/client/src/translations/rules/Cylinder/fr.pug new file mode 100644 index 00000000..cd411085 --- /dev/null +++ b/client/src/translations/rules/Cylinder/fr.pug @@ -0,0 +1,27 @@ +p.boxed + | Les colonnes 'a' et 'h' commniquent. + +p. + La colonne 'a' est maintenant juste à la droite de la colonne 'h' : + un pion en h2 peut prendre en a3, et un fou en h3 peut aller en a4, etc. + +p. + Ceci permet des double échecs donnés par une seule pièce. Sur le diagramme + suivant, le fou donne échec par deux chemins différents : e2-d3-c4 et g2-h3-a4. + Il n'est pas possible de couvrir les deux lignes d'attaque, donc le roi doit bouger. + +figure.diagram-container + .diagram.diag12 + | fen:8/2p5/3n4/1k6/8/8/2PP4/5B1K: + .diagram.diag22 + | fen:4k3/2p5/8/8/7N/8/3P4/7K a2,b3,b5,a6,g2,f3,f5,g6: + figcaption. + Gauche: double-échec en b5. Droite: possibles coups de cavalier depuis h4. + +h3 Source + +p + | La + a(href="https://www.chessvariants.com/boardrules.dir/cylindrical.html") + | variante cylindrique + |  sur chessvariants.com. diff --git a/client/src/translations/rules/Hidden/en.pug b/client/src/translations/rules/Hidden/en.pug index 88e112b4..e8ebbbbc 100644 --- a/client/src/translations/rules/Hidden/en.pug +++ b/client/src/translations/rules/Hidden/en.pug @@ -14,12 +14,14 @@ p The game is won by capturing the opposing King. figure.diagram-container .diagram - | fen:qbppnprp/prppbkpn/8/8/8/8/QBPPNPRP/PRPPBKPN: + | fen:qbppnprp/prpbpkpn/8/8/8/8/QBPPNPRP/PRPPKBPN: figcaption Possible starting position. p Notes ul - li The computer plays for now totally at random. + li. + The computer uses a basic strategy, way inferior to what a human could do + but still better than random play. li. Pieces are randomly set on the two first ranks. The king may be on second rank, and attacked by an enemy rook or queen. diff --git a/client/src/translations/rules/Hidden/es.pug b/client/src/translations/rules/Hidden/es.pug index c8c0601e..c480ad44 100644 --- a/client/src/translations/rules/Hidden/es.pug +++ b/client/src/translations/rules/Hidden/es.pug @@ -15,12 +15,14 @@ p La victoria se obtiene capturando al rey contrario. figure.diagram-container .diagram - | fen:qbppnprp/prppbkpn/8/8/8/8/QBPPNPRP/PRPPBKPN: + | fen:qbppnprp/prpbpkpn/8/8/8/8/QBPPNPRP/PRPPKBPN: figcaption Posible posición inicial. p Notas ul - li La computadora actualmente está jugando completamente al azar. + li. + La computadora usa una estrategia básica, mucho más baja de lo que + humano podría hacer pero mejor que un juego aleatorio. li. Las piezas se distribuyen al azar en las dos primeras filas. El rey podría estar en la segunda fila, atacado por una torre o dama enemiga. diff --git a/client/src/translations/rules/Hidden/fr.pug b/client/src/translations/rules/Hidden/fr.pug index 930400fd..f491100f 100644 --- a/client/src/translations/rules/Hidden/fr.pug +++ b/client/src/translations/rules/Hidden/fr.pug @@ -15,12 +15,14 @@ p La victoire s'obtient en capturant le roi adverse. figure.diagram-container .diagram - | fen:qbppnprp/prppbkpn/8/8/8/8/QBPPNPRP/PRPPBKPN: + | fen:qbppnprp/prpbpkpn/8/8/8/8/QBPPNPRP/PRPPKBPN: figcaption Possible position initiale. p Notes ul - li L'ordinateur joue pour l'instant totalement au hasard. + li. + L'ordinateur utilise une stratégie basique, nettement inférieure à ce qu'un + humain pourrait faire mais meilleure qu'un jeu aléatoire. li. Les pièces sont réparties au hasard sur les deux premières rangées. Le roi pourrait être sur la seconde rangée, attaqué par une tour ou dame ennemie. diff --git a/client/src/variants/Checkered.js b/client/src/variants/Checkered.js index d477a6d0..e930fb84 100644 --- a/client/src/variants/Checkered.js +++ b/client/src/variants/Checkered.js @@ -74,7 +74,6 @@ export const VariantRules = class CheckeredRules extends ChessRules { w: [...Array(8).fill(true)], //pawns can move 2 squares? b: [...Array(8).fill(true)] }; - if (!fenflags) return; const flags = fenflags.substr(4); //skip first 4 digits, for castle for (let c of ["w", "b"]) { for (let i = 0; i < 8; i++) @@ -112,7 +111,8 @@ export const VariantRules = class CheckeredRules extends ChessRules { getPotentialMovesFrom([x, y]) { let standardMoves = super.getPotentialMovesFrom([x, y]); const lastRank = this.turn == "w" ? 0 : 7; - if (this.getPiece(x, y) == V.KING) return standardMoves; //king has to be treated differently (for castles) + // King has to be treated differently (for castles) + if (this.getPiece(x, y) == V.KING) return standardMoves; let moves = []; standardMoves.forEach(m => { if (m.vanish[0].p == V.PAWN) { diff --git a/client/src/variants/Circular.js b/client/src/variants/Circular.js index a5a5d644..4db5a52a 100644 --- a/client/src/variants/Circular.js +++ b/client/src/variants/Circular.js @@ -3,20 +3,36 @@ import { ArrayFun } from "@/utils/array"; import { randInt, shuffle } from "@/utils/alea"; export const VariantRules = class CircularRules extends ChessRules { - static get HasFlags() { + static get HasEnpassant() { return false; } - static get HasEnpassant() { + static get CanFlip() { return false; } - // TODO: CanFlip --> also for racing kings (answer is false) + setFlags(fenflags) { + this.pawnFlags = { + w: [...Array(8).fill(true)], //pawns can move 2 squares? + b: [...Array(8).fill(true)] + }; + for (let c of ["w", "b"]) { + for (let i = 0; i < 8; i++) + this.pawnFlags[c][i] = fenflags.charAt((c == "w" ? 0 : 8) + i) == "1"; + } + } + + aggregateFlags() { + return this.pawnFlags; + } + + disaggregateFlags(flags) { + this.pawnFlags = flags; + } - // TODO: shuffle on 1st and 5th ranks static GenRandInitFen() { let pieces = { w: new Array(8), b: new Array(8) }; - // Shuffle pieces on first and last rank + // Shuffle pieces on first and fifth rank for (let c of ["w", "b"]) { let positions = ArrayFun.range(8); @@ -60,23 +76,32 @@ export const VariantRules = class CircularRules extends ChessRules { pieces[c][rook2Pos] = "r"; } return ( + "8/8/pppppppp/" + pieces["b"].join("") + - "/pppppppp/8/8/8/8/PPPPPPPP/" + + "/8/8/PPPPPPPP/" + pieces["w"].join("").toUpperCase() + - " w 0" + // 16 flags: can pawns advance 2 squares? + " w 0 1111111111111111" ); } - // TODO: adapt this for a circular board + // Output basically x % 8 (circular board) + static ComputeX(x) { + let res = x % V.size.x; + if (res < 0) + res += V.size.x; + return res; + } + getSlideNJumpMoves([x, y], steps, oneStep) { let moves = []; outerLoop: for (let step of steps) { - let i = x + step[0]; + let i = V.ComputeX(x + step[0]); let j = y + step[1]; while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { moves.push(this.getBasicMove([x, y], [i, j])); if (oneStep !== undefined) continue outerLoop; - i += step[0]; + i = V.ComputeX(i + step[0]); j += step[1]; } if (V.OnBoard(i, j) && this.canTake([x, y], [i, j])) @@ -85,66 +110,42 @@ export const VariantRules = class CircularRules extends ChessRules { return moves; } - // TODO: adapt: all pawns go in thz same direction! getPotentialPawnMoves([x, y]) { const color = this.turn; let moves = []; const [sizeX, sizeY] = [V.size.x, V.size.y]; - const shiftX = color == "w" ? -1 : 1; - const firstRank = color == "w" ? sizeX - 1 : 0; - const startRank = color == "w" ? sizeX - 2 : 1; - const lastRank = color == "w" ? 0 : sizeX - 1; - const pawnColor = this.getColor(x, y); //can be different for checkered - - // NOTE: next condition is generally true (no pawn on last rank) - if (x + shiftX >= 0 && x + shiftX < sizeX) { - const finalPieces = - x + shiftX == lastRank - ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] - : [V.PAWN]; - // One square forward - if (this.board[x + shiftX][y] == V.EMPTY) { - for (let piece of finalPieces) { - moves.push( - this.getBasicMove([x, y], [x + shiftX, y], { - c: pawnColor, - p: piece - }) - ); - } - // Next condition because pawns on 1st rank can generally jump - if ( - [startRank, firstRank].includes(x) && - this.board[x + 2 * shiftX][y] == V.EMPTY - ) { - // Two squares jump - moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); - } + // All pawns go in the same direction! + const shiftX = -1; + const startRank = color == "w" ? sizeX - 2 : 2; + + // One square forward + const nextRow = V.ComputeX(x + shiftX); + if (this.board[nextRow][y] == V.EMPTY) { + moves.push(this.getBasicMove([x, y], [nextRow, y])); + if ( + x == startRank && + this.pawnFlags[color][y] && + this.board[x + 2 * shiftX][y] == V.EMPTY + ) { + // Two squares jump + moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); } - // Captures - for (let shiftY of [-1, 1]) { - if ( - y + shiftY >= 0 && - y + shiftY < sizeY && - this.board[x + shiftX][y + shiftY] != V.EMPTY && - this.canTake([x, y], [x + shiftX, y + shiftY]) - ) { - for (let piece of finalPieces) { - moves.push( - this.getBasicMove([x, y], [x + shiftX, y + shiftY], { - c: pawnColor, - p: piece - }) - ); - } - } + } + // Captures + for (let shiftY of [-1, 1]) { + if ( + y + shiftY >= 0 && + y + shiftY < sizeY && + this.board[nextRow][y + shiftY] != V.EMPTY && + this.canTake([x, y], [nextRow, y + shiftY]) + ) { + moves.push(this.getBasicMove([x, y], [nextRow, y + shiftY])); } } return moves; } - // What are the king moves from square x,y ? getPotentialKingMoves(sq) { return this.getSlideNJumpMoves( sq, @@ -153,33 +154,45 @@ export const VariantRules = class CircularRules extends ChessRules { ); } - // TODO: check boundaries here as well + filterValid(moves) { + const filteredMoves = super.filterValid(moves); + // If at least one full move made, everything is allowed: + if (this.movesCount >= 2) + return filteredMoves; + // Else, forbid check: + const oppCol = V.GetOppCol(this.turn); + return filteredMoves.filter(m => { + this.play(m); + const res = !this.underCheck(oppCol); + this.undo(m); + return res; + }); + } + isAttackedByPawn([x, y], colors) { for (let c of colors) { - let pawnShift = c == "w" ? 1 : -1; - if (x + pawnShift >= 0 && x + pawnShift < V.size.x) { - for (let i of [-1, 1]) { - if ( - y + i >= 0 && - y + i < V.size.y && - this.getPiece(x + pawnShift, y + i) == V.PAWN && - this.getColor(x + pawnShift, y + i) == c - ) { - return true; - } + let pawnShift = 1; + const attackerRow = V.ComputeX(x + pawnShift); + for (let i of [-1, 1]) { + if ( + y + i >= 0 && + y + i < V.size.y && + this.getPiece(attackerRow, y + i) == V.PAWN && + this.getColor(attackerRow, y + i) == c + ) { + return true; } } } return false; } - // TODO: adapt this function isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) { for (let step of steps) { - let rx = x + step[0], + let rx = V.ComputeX(x + step[0]), ry = y + step[1]; while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) { - rx += step[0]; + rx = V.ComputeX(rx + step[0]); ry += step[1]; } if ( @@ -192,4 +205,37 @@ export const VariantRules = class CircularRules extends ChessRules { } return false; } + + getFlagsFen() { + // Return pawns flags + let flags = ""; + for (let c of ["w", "b"]) { + for (let i = 0; i < 8; i++) flags += this.pawnFlags[c][i] ? "1" : "0"; + } + return flags; + } + + updateVariables(move) { + const c = move.vanish[0].c; + const secondRank = {"w":6, "b":2}; + // Update king position + flags + if (move.vanish[0].p == V.KING && move.appear.length > 0) { + this.kingPos[c][0] = move.appear[0].x; + this.kingPos[c][1] = move.appear[0].y; + } + else if (move.vanish[0].p == V.PAWN && secondRank[c] == move.start.x) + // This move turns off a 2-squares pawn flag + this.pawnFlags[c][move.start.y] = false; + } + + static get VALUES() { + return { + p: 1, + r: 5, + n: 3, + b: 4, + q: 10, + k: 1000 + }; + } }; diff --git a/client/src/variants/Cylinder.js b/client/src/variants/Cylinder.js new file mode 100644 index 00000000..39a51851 --- /dev/null +++ b/client/src/variants/Cylinder.js @@ -0,0 +1,147 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; +import { randInt, shuffle } from "@/utils/alea"; + +export const VariantRules = class CylinderRules extends ChessRules { + // Output basically x % 8 (circular board) + static ComputeY(y) { + let res = y % V.size.y; + if (res < 0) + res += V.size.y; + return res; + } + + getSlideNJumpMoves([x, y], steps, oneStep) { + let moves = []; + outerLoop: for (let step of steps) { + let i = x + step[0]; + let j = V.ComputeY(y + step[1]); + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + moves.push(this.getBasicMove([x, y], [i, j])); + if (oneStep !== undefined) continue outerLoop; + i += step[0]; + j = V.ComputeY(j + step[1]); + } + if (V.OnBoard(i, j) && this.canTake([x, y], [i, j])) + moves.push(this.getBasicMove([x, y], [i, j])); + } + return moves; + } + + getPotentialPawnMoves([x, y]) { + const color = this.turn; + let moves = []; + const [sizeX, sizeY] = [V.size.x, V.size.y]; + const shiftX = color == "w" ? -1 : 1; + const startRank = color == "w" ? sizeX - 2 : 1; + const lastRank = color == "w" ? 0 : sizeX - 1; + + const finalPieces = + x + shiftX == lastRank + ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] + : [V.PAWN]; + if (this.board[x + shiftX][y] == V.EMPTY) { + // One square forward + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y], { + c: color, + p: piece + }) + ); + } + if ( + x == startRank && + this.board[x + 2 * shiftX][y] == V.EMPTY + ) { + // Two squares jump + moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); + } + } + // Captures + for (let shiftY of [-1, 1]) { + const nextFile = V.ComputeY(y + shiftY); + if ( + this.board[x + shiftX][nextFile] != V.EMPTY && + this.canTake([x, y], [x + shiftX, nextFile]) + ) { + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, nextFile], { + c: color, + p: piece + }) + ); + } + } + } + + // En passant + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep - 1]; //always at least one element + if ( + !!epSquare && + epSquare.x == x + shiftX && + Math.abs( (epSquare.y - y) % V.size.y ) == 1 + ) { + let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]); + enpassantMove.vanish.push({ + x: x, + y: epSquare.y, + p: "p", + c: this.getColor(x, epSquare.y) + }); + moves.push(enpassantMove); + } + + return moves; + } + + isAttackedByPawn([x, y], colors) { + for (let c of colors) { + let pawnShift = c == "w" ? 1 : -1; + if (x + pawnShift >= 0 && x + pawnShift < V.size.x) { + for (let i of [-1, 1]) { + const nextFile = V.ComputeY(y + i); + if ( + this.getPiece(x + pawnShift, nextFile) == V.PAWN && + this.getColor(x + pawnShift, nextFile) == c + ) { + return true; + } + } + } + } + return false; + } + + isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) { + for (let step of steps) { + let rx = x + step[0], + ry = V.ComputeY(y + step[1]); + while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) { + rx += step[0]; + ry = V.ComputeY(ry + step[1]); + } + if ( + V.OnBoard(rx, ry) && + this.getPiece(rx, ry) === piece && + colors.includes(this.getColor(rx, ry)) + ) { + return true; + } + } + return false; + } + + static get VALUES() { + return { + p: 1, + r: 5, + n: 3, + b: 4, + q: 10, + k: 1000 + }; + } +}; diff --git a/client/src/variants/Dark.js b/client/src/variants/Dark.js index 3944ca16..6cf3b678 100644 --- a/client/src/variants/Dark.js +++ b/client/src/variants/Dark.js @@ -223,19 +223,18 @@ export const VariantRules = class DarkRules extends ChessRules { // Can I take something ? If yes, do it if it seems good... if (move.vanish.length == 2 && move.vanish[1].c != color) { - //avoid castle + // OK this isn't a castling move const myPieceVal = V.VALUES[move.appear[0].p]; const hisPieceVal = V.VALUES[move.vanish[1].p]; - if (myPieceVal <= hisPieceVal) move.eval = hisPieceVal - myPieceVal + 2; - //favor captures + // Favor captures + if (myPieceVal <= hisPieceVal) move.eval = hisPieceVal - myPieceVal + 1; else { // Taking a pawn with minor piece, // or minor piece or pawn with a rook, // or anything but a queen with a queen, // or anything with a king. - // ==> Do it at random, although - // this is clearly inferior to what a human can deduce... - move.eval = Math.random() < 0.5 ? 1 : -1; + move.eval = hisPieceVal - myPieceVal; + //Math.random() < 0.5 ? 1 : -1; } } } diff --git a/client/src/variants/Hidden.js b/client/src/variants/Hidden.js index c8d98eb6..96822b93 100644 --- a/client/src/variants/Hidden.js +++ b/client/src/variants/Hidden.js @@ -214,9 +214,9 @@ export const VariantRules = class HiddenRules extends ChessRules { pieces[c][rook2Pos] = "u"; } let upFen = pieces["b"].join(""); - upFen = upFen.substr(0,8) + "/" + upFen.substr(8); + upFen = upFen.substr(0,8) + "/" + upFen.substr(8).split("").reverse().join(""); let downFen = pieces["b"].join("").toUpperCase(); - downFen = downFen.substr(0,8) + "/" + downFen.substr(8); + downFen = downFen.substr(0,8) + "/" + downFen.substr(8).split("").reverse().join(""); return upFen + "/8/8/8/8/" + downFen + " w 0"; } @@ -255,9 +255,49 @@ export const VariantRules = class HiddenRules extends ChessRules { } getComputerMove() { - // Just return a random move. TODO: something smarter... - const moves = this.getAllValidMoves(); - return moves[randInt(moves.length)]; + const color = this.turn; + let moves = this.getAllValidMoves(); + for (let move of moves) { + move.eval = 0; //a priori... + + // Can I take something ? If yes, do it with some probability + if (move.vanish.length == 2 && move.vanish[1].c != color) { + // OK this isn't a castling move + const myPieceVal = V.VALUES[move.appear[0].p]; + const hisPieceVal = Object.keys(V.HIDDEN_DECODE).includes(move.vanish[1].p) + ? undefined + : V.VALUES[move.vanish[1].p]; + if (!hisPieceVal) { + // Opponent's piece is unknown: do not take too much risk + move.eval = -myPieceVal + 1.5; //so that pawns always take + } + // Favor captures + else if (myPieceVal <= hisPieceVal) + move.eval = hisPieceVal - myPieceVal + 1; + else { + // Taking a pawn with minor piece, + // or minor piece or pawn with a rook, + // or anything but a queen with a queen, + // or anything with a king. + move.eval = hisPieceVal - myPieceVal; + } + } else { + // If no capture, favor small step moves, + // but sometimes move the knight anyway + const penalty = V.Decode(move.vanish[0].p) != V.KNIGHT + ? Math.abs(move.end.x - move.start.x) + Math.abs(move.end.y - move.start.y) + : (Math.random() < 0.5 ? 3 : 1); + move.eval -= penalty / (V.size.x + V.size.y - 1); + } + + // TODO: also favor movements toward the center? + } + + moves.sort((a, b) => b.eval - a.eval); + let candidates = [0]; + for (let j = 1; j < moves.length && moves[j].eval == moves[0].eval; j++) + candidates.push(j); + return moves[candidates[randInt(candidates.length)]]; } getNotation(move) { diff --git a/client/src/variants/Suction.js b/client/src/variants/Suction.js index 38ee5c2e..297eef75 100644 --- a/client/src/variants/Suction.js +++ b/client/src/variants/Suction.js @@ -236,4 +236,28 @@ export const VariantRules = class SuctionRules extends ChessRules { // Very simple criterion for now: kings position return this.kingPos["w"][0] + this.kingPos["b"][0]; } + + getNotation(move) { + // Translate final square + const finalSquare = V.CoordsToSquare(move.end); + + const piece = this.getPiece(move.start.x, move.start.y); + if (piece == V.PAWN) { + // Pawn move + let notation = ""; + if (move.vanish.length == 2) { + // Capture + const startColumn = V.CoordToColumn(move.start.y); + notation = startColumn + "x" + finalSquare; + } + else notation = finalSquare; + return notation; + } + // Piece movement + return ( + piece.toUpperCase() + + (move.vanish.length == 2 ? "x" : "") + + finalSquare + ); + } }; diff --git a/server/db/populate.sql b/server/db/populate.sql index a86a8740..e6291462 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -13,6 +13,7 @@ insert or ignore into Variants (name,description) values ('Chess960', 'Standard rules'), ('Circular', 'Run forward'), ('Crazyhouse', 'Captures reborn'), + ('Cylinder', 'Neverending rows'), ('Dark', 'In the shadow'), ('Enpassant', 'Capture en passant'), ('Extinction', 'Capture all of a kind'), -- 2.44.0