From: Benjamin Auder Date: Sun, 1 Mar 2020 15:47:20 +0000 (+0100) Subject: Draft Hiddenqueen, Grasshopper and Knightmate chess (rules unwritten) X-Git-Url: https://git.auder.net/%7B%7B%20asset%28%27mixstore/css/user/board.css%27%29%20%7D%7D?a=commitdiff_plain;h=a97bdbda4ecf83645d409b717e36828784d1450d;p=vchess.git Draft Hiddenqueen, Grasshopper and Knightmate chess (rules unwritten) --- diff --git a/TODO b/TODO index fb75f95d..7bcb36fd 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,18 @@ -newmove received on mygames page should be added to storage if gtype == "live" -and, on game page "mconnect" events => send newmove to them (better than current setup) -also, mygames page should ask lastate infos to connected players if any (where it's not my turn) -(maybe in component GameList, if g.type == "live" ...) +# Functionality: +On Game page "mconnect" events => + send lastate to them (because they have the game infos) or just "your turn" - if their turn + remember them to send next "newmove" (or just "it's your turn") later - if not their turn + (=> listen for "mdisconnect" as well) +From MyGames page: send "mconnect" to all online players (me included: potential multi-tabs) + When quit, send mdisconnect (relayed by server if no other MyGames tab). +And remove current "notify through newmove" on server in sockets.js +# Images: Color black wildebeest and camels pieces in white instead of transparent. +Color white grasshoppers as well. Adjust wormholes color and size. +Better "Check3" king images: just horizontal red bars maybe (1 to 3). +Center king image for Knightmate variant. +# Misc: Saw once a "double challenge" bug, one anonymous and a second one logged Both were asked a challenge probably, and both challenges added as different ones. diff --git a/client/public/images/pieces/Grasshopper/bg.svg b/client/public/images/pieces/Grasshopper/bg.svg new file mode 100644 index 00000000..5c238e36 --- /dev/null +++ b/client/public/images/pieces/Grasshopper/bg.svg @@ -0,0 +1 @@ +Icons8 \ No newline at end of file diff --git a/client/public/images/pieces/Grasshopper/wg.svg b/client/public/images/pieces/Grasshopper/wg.svg new file mode 100644 index 00000000..045fffea --- /dev/null +++ b/client/public/images/pieces/Grasshopper/wg.svg @@ -0,0 +1 @@ +Icons8 \ No newline at end of file diff --git a/client/public/images/pieces/Hiddenqueen/bt.svg b/client/public/images/pieces/Hiddenqueen/bt.svg new file mode 100644 index 00000000..ea17c33c --- /dev/null +++ b/client/public/images/pieces/Hiddenqueen/bt.svg @@ -0,0 +1,7 @@ + + + Layer 1 + + + + diff --git a/client/public/images/pieces/Hiddenqueen/wt.svg b/client/public/images/pieces/Hiddenqueen/wt.svg new file mode 100644 index 00000000..87c11cae --- /dev/null +++ b/client/public/images/pieces/Hiddenqueen/wt.svg @@ -0,0 +1,8 @@ + + + Layer 1 + + + + + diff --git a/client/public/images/pieces/Knightmate/bn.svg b/client/public/images/pieces/Knightmate/bk.svg similarity index 100% rename from client/public/images/pieces/Knightmate/bn.svg rename to client/public/images/pieces/Knightmate/bk.svg diff --git a/client/public/images/pieces/Knightmate/wn.svg b/client/public/images/pieces/Knightmate/wk.svg similarity index 100% rename from client/public/images/pieces/Knightmate/wn.svg rename to client/public/images/pieces/Knightmate/wk.svg diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 8b49436c..149bb4d2 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -1114,30 +1114,17 @@ export const ChessRules = class ChessRules { return 3; } - // NOTE: works also for extinction chess because depth is 3... getComputerMove() { const maxeval = V.INFINITY; const color = this.turn; // Some variants may show a bigger moves list to the human (Switching), // thus the argument "computer" below (which is generally ignored) - let moves1 = this.getAllValidMoves("computer"); + let moves1 = this.getAllValidMoves(); if (moves1.length == 0) // TODO: this situation should not happen return null; - // Can I mate in 1 ? (for Magnetic & Extinction) - for (let i of shuffle(ArrayFun.range(moves1.length))) { - this.play(moves1[i]); - let finish = Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE; - if (!finish) { - const score = this.getCurrentScore(); - if (["1-0", "0-1"].includes(score)) finish = true; - } - this.undo(moves1[i]); - if (finish) return moves1[i]; - } - // Rank moves using a min-max at depth 2 for (let i = 0; i < moves1.length; i++) { // Initial self evaluation is very low: "I'm checkmated" @@ -1149,7 +1136,7 @@ export const ChessRules = class ChessRules { // Initial enemy evaluation is very low too, for him eval2 = (color == "w" ? 1 : -1) * maxeval; // Second half-move: - let moves2 = this.getAllValidMoves("computer"); + let moves2 = this.getAllValidMoves(); for (let j = 0; j < moves2.length; j++) { this.play(moves2[j]); const score2 = this.getCurrentScore(); @@ -1185,6 +1172,7 @@ export const ChessRules = class ChessRules { moves1.sort((a, b) => { return (color == "w" ? 1 : -1) * (b.eval - a.eval); }); +// console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; })); let candidates = [0]; //indices of candidates moves for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++) @@ -1225,7 +1213,7 @@ export const ChessRules = class ChessRules { if (score != "*") return score == "1/2" ? 0 : (score == "1-0" ? 1 : -1) * maxeval; if (depth == 0) return this.evalPosition(); - const moves = this.getAllValidMoves("computer"); + const moves = this.getAllValidMoves(); let v = color == "w" ? -maxeval : maxeval; if (color == "w") { for (let i = 0; i < moves.length; i++) { diff --git a/client/src/translations/en.js b/client/src/translations/en.js index ff178cd4..c8074575 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -5,6 +5,7 @@ export const translations = { "Accept challenge?": "Accept challenge?", Analyse: "Analyse", "Analysis mode": "Analysis mode", + "Analysis disabled for this variant": "Analysis disabled for this variant", "Any player": "Any player", Apply: "Apply", "Are you sure?": "Are you sure?", @@ -52,7 +53,9 @@ export const translations = { Login: "Login", Logout: "Logout", "Logout successful!": "Logout successful!", + "Mispelled variant name": "Mispelled variant name", "Missing email": "Missing email", + "Missing FEN": "Missing FEN", "Missing instructions": "Missing instructions", "Missing name": "Missing name", "Missing solution": "Missing solution", @@ -89,7 +92,7 @@ export const translations = { "participant(s):": "participant(s):", Refuse: "Refuse", Register: "Register", - "Registration complete! Please check your emails": "Registration complete! Please check your emails", + "Registration complete! Please check your emails now": "Registration complete! Please check your emails now", "Remove game?": "Remove game?", Resign: "Resign", "Resign the game?": "Resign the game?", @@ -145,13 +148,16 @@ export const translations = { "Keep antiking in check": "Keep antiking in check", "King crosses the board": "King crosses the board", "Laws of attraction": "Laws of attraction", + "Long jumps over pieces": "Long jumps over pieces", "Lose all pieces": "Lose all pieces", "Mate any piece": "Mate any piece", + "Mate the knight": "Mate the knight", "Middle battle": "Middle battle", "Move like a knight": "Move like a knight", "Move twice": "Move twice", "Neverending rows": "Neverending rows", "Pawns move diagonally": "Pawns move diagonally", + "Queen disguised as a pawn": "Queen disguised as a pawn", "Reuse pieces": "Reuse pieces", "Reverse captures": "Reverse captures", "Run forward": "Run forward", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index eb08331d..3c63ffb7 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -5,6 +5,7 @@ export const translations = { "Accept challenge?": "¿Acceptar el desafío?", Analyse: "Analizar", "Analysis mode": "Modo análisis", + "Analysis disabled for this variant": "Análisis deshabilitado para esta variante", "Any player": "Cualquier jugador", Apply: "Aplicar", "Are you sure?": "¿Está usted seguro?", @@ -52,7 +53,9 @@ export const translations = { Login: "Login", Logout: "Logout", "Logout successful!": "¡Desconexión exitosa!", + "Mispelled variant name": "Variante mal escrita", "Missing email": "Email falta", + "Missing FEN": "FEN falta", "Missing instructions": "Instrucciones faltan", "Missing name": "Nombre falta", "Missing solution": "Solución falta", @@ -89,7 +92,7 @@ export const translations = { "participant(s):": "participante(s):", Refuse: "Rechazar", Register: "Registrarse", - "Registration complete! Please check your emails": "¡Registro completo! Por favor revise sus correos electrónicos", + "Registration complete! Please check your emails now": "¡Registro completo! Revise sus correos electrónicos ahora", "Remove game?": "¿Eliminar la partida?", Resign: "Abandonar", "Resign the game?": "¿Abandonar la partida?", @@ -145,13 +148,16 @@ export const translations = { "Keep antiking in check": "Mantener el antirey en jaque", "King crosses the board": "El rey cruza el tablero", "Laws of attraction": "Las leyes de las atracciones", + "Long jumps over pieces": "Saltos largos sobre las piezas", "Lose all pieces": "Perder todas las piezas", "Mate any piece": "Matar cualquier pieza", + "Mate the knight": "Matar el caballo", "Middle battle": "Batalla media", "Move like a knight": "Moverse como un caballo", "Move twice": "Mover dos veces", "Neverending rows": "Filas interminables", "Pawns move diagonally": "Peones se mueven en diagonal", + "Queen disguised as a pawn": "Reina disfrazada de peón", "Reuse pieces": "Reutilizar piezas", "Reverse captures": "Capturas invertidas", "Run forward": "Correr hacia adelante", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 6e37e605..c7246814 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -5,6 +5,7 @@ export const translations = { "Accept challenge?": "Accepter le défi ?", Analyse: "Analyser", "Analysis mode": "Mode analyse", + "Analysis disabled for this variant": "Analyse désactivée pour cette variante", "Any player": "N'importe qui", Apply: "Appliquer", "Authentication successful!": "Authentification réussie !", @@ -52,7 +53,9 @@ export const translations = { Login: "Login", Logout: "Logout", "Logout successful!": "Déconnection réussie !", + "Mispelled variant name": "Variante mal orthographiée", "Missing email": "Email manquant", + "Missing FEN": "FEN manquante", "Missing instructions": "Instructions manquantes", "Missing name": "Nom manquant", "Missing solution": "Solution manquante", @@ -89,7 +92,7 @@ export const translations = { "participant(s):": "participant(s) :", Refuse: "Refuser", Register: "S'enregistrer", - "Registration complete! Please check your emails": "Enregistrement terminé ! Allez voir vos emails", + "Registration complete! Please check your emails now": "Enregistrement terminé ! Allez voir vos emails maintenant", "Remove game?": "Supprimer la partie ?", Resign: "Abandonner", "Resign the game?": "Abandonner la partie ?", @@ -145,13 +148,16 @@ export const translations = { "Keep antiking in check": "Gardez l'antiroi en échec", "King crosses the board": "Le roi traverse l'échiquier", "Laws of attraction": "Les lois de l'attraction", + "Long jumps over pieces": "Sauts longs par dessus les pièces", "Lose all pieces": "Perdez toutes les pièces", - "Mate any piece": "Mater n'importe quelle pièce", + "Mate any piece": "Matez n'importe quelle pièce", + "Mate the knight": "Matez le cavalier", "Middle battle": "Bataille du milieu", "Move like a knight": "Bougez comme un cavalier", "Move twice": "Jouer deux coups", "Neverending rows": "Rangées sans fin", "Pawns move diagonally": "Les pions vont en diagonale", + "Queen disguised as a pawn": "Reine déguisée en pion", "Reuse pieces": "Réutiliser les pièces", "Reverse captures": "Captures inversées", "Run forward": "Courir vers l'avant", diff --git a/client/src/translations/rules/Enpassant/en.pug b/client/src/translations/rules/Enpassant/en.pug index 7ec2cc2e..c2136cf0 100644 --- a/client/src/translations/rules/Enpassant/en.pug +++ b/client/src/translations/rules/Enpassant/en.pug @@ -29,6 +29,17 @@ figure.diagram-container Possible knightrider moves after 1.d3 g6 2.Nc3 Bg7. If 3.Nxd7, 3...Bxe5 e.p. is possible. +p. + Due to the extended knight movement, a capture, check or even checkmate + could be played at move 1. On the following diagram 1.Nxe7 is mate, + and black in turn could play 1...Nxg2 trapping the queen. + Consequently, captures are disabled at move 1. + +figure.diagram-container + .diagram + | fen:nbqnbrkr/pppppppp/8/8/8/8/PPPPPPPP/RNKBQNBR: + figcaption 1.Nxe7# and 1...Nxg2: "knightriders' anomalies". + h3 Source p diff --git a/client/src/translations/rules/Enpassant/es.pug b/client/src/translations/rules/Enpassant/es.pug index 1c501597..cb4dc791 100644 --- a/client/src/translations/rules/Enpassant/es.pug +++ b/client/src/translations/rules/Enpassant/es.pug @@ -31,6 +31,18 @@ figure.diagram-container Posibles jugadas de caballero después de 1.d3 g6 2.Nc3 Bg7. Si 3.Nxd7, 3...Bxe5 e.p. es posible. +p. + Los movimientos del caballo se incrementan, una captura, + un jaque o incluso un jaque mate sería posible en el primer movimiento. + En el diagrama a continuación 1.Nxe7 es mate, y las negras al turno + podría jugar 1...Nxg2 atrapando a la dama. + Es por eso que las capturas solo están autorizadas desde el segundo movimiento. + +figure.diagram-container + .diagram + | fen:nbqnbrkr/pppppppp/8/8/8/8/PPPPPPPP/RNKBQNBR: + figcaption 1.Nxe7# y 1...Nxg2: "anomalías de caballeros". + h3 Fuente p diff --git a/client/src/translations/rules/Enpassant/fr.pug b/client/src/translations/rules/Enpassant/fr.pug index 6dd1e14f..a4178bf4 100644 --- a/client/src/translations/rules/Enpassant/fr.pug +++ b/client/src/translations/rules/Enpassant/fr.pug @@ -31,6 +31,18 @@ figure.diagram-container Possibles coups de chevalier après 1.d3 g6 2.Nc3 Bg7. Si 3.Nxd7, 3...Bxe5 e.p. est possible. +p. + Les déplacements du cavalier étant augmentés, une capture, + un échec ou même un mat seraient possibles au premier coup. + Sur le diagramme ci-dessous 1.Nxe7 fait mat, et les noirsau trait + pourraient jouer 1...Nxg2 piégeant la dame. + C'est pourquoi les captures ne sont autorisées qu'à partir du second coup. + +figure.diagram-container + .diagram + | fen:nbqnbrkr/pppppppp/8/8/8/8/PPPPPPPP/RNKBQNBR: + figcaption 1.Nxe7# et 1...Nxg2 : "anomalies de chevaliers". + h3 Source p diff --git a/client/src/translations/rules/Grasshopper/en.pug b/client/src/translations/rules/Grasshopper/en.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Grasshopper/en.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Grasshopper/es.pug b/client/src/translations/rules/Grasshopper/es.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Grasshopper/es.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Grasshopper/fr.pug b/client/src/translations/rules/Grasshopper/fr.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Grasshopper/fr.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Hiddenqueen/en.pug b/client/src/translations/rules/Hiddenqueen/en.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Hiddenqueen/en.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Hiddenqueen/es.pug b/client/src/translations/rules/Hiddenqueen/es.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Hiddenqueen/es.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Hiddenqueen/fr.pug b/client/src/translations/rules/Hiddenqueen/fr.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Hiddenqueen/fr.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Knightmate/en.pug b/client/src/translations/rules/Knightmate/en.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Knightmate/en.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Knightmate/es.pug b/client/src/translations/rules/Knightmate/es.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Knightmate/es.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Knightmate/fr.pug b/client/src/translations/rules/Knightmate/fr.pug new file mode 100644 index 00000000..4f56997b --- /dev/null +++ b/client/src/translations/rules/Knightmate/fr.pug @@ -0,0 +1 @@ +p TODO diff --git a/client/src/translations/rules/Rifle/en.pug b/client/src/translations/rules/Rifle/en.pug index 1b0248de..f168ce9b 100644 --- a/client/src/translations/rules/Rifle/en.pug +++ b/client/src/translations/rules/Rifle/en.pug @@ -14,7 +14,7 @@ figure.diagram-container p. This "small" difference alters the strategy a lot: guarding pieces is useless, - for example, and the king cannot escape a check by capturing. + for example, and the king cannot escape a distant check by capturing. h3 Source diff --git a/client/src/translations/rules/Rifle/es.pug b/client/src/translations/rules/Rifle/es.pug index 7e714ed3..5314cfcd 100644 --- a/client/src/translations/rules/Rifle/es.pug +++ b/client/src/translations/rules/Rifle/es.pug @@ -14,7 +14,7 @@ figure.diagram-container p. Esta "pequeña" diferencia altera enormemente la estrategia: defender las piezas - es inútil, por ejemplo, y el rey no puede escapar un jaque capturando. + es inútil, por ejemplo, y el rey no puede escapar un jaque remota capturando. h3 Fuente diff --git a/client/src/translations/rules/Rifle/fr.pug b/client/src/translations/rules/Rifle/fr.pug index cc9bf481..0f0bb3a7 100644 --- a/client/src/translations/rules/Rifle/fr.pug +++ b/client/src/translations/rules/Rifle/fr.pug @@ -14,7 +14,7 @@ figure.diagram-container p. Cette "petite" différence altère beaucoup la stratégie : défendre les pièces - est inutile, par exemple, et le roi ne pas échapper à un échec en capturant. + est inutile, par exemple, et le roi ne pas échapper à un échec distant en capturant. h3 Source diff --git a/client/src/translations/rules/Wormhole/es.pug b/client/src/translations/rules/Wormhole/es.pug index 0dd6dd6f..1ba5ae02 100644 --- a/client/src/translations/rules/Wormhole/es.pug +++ b/client/src/translations/rules/Wormhole/es.pug @@ -19,9 +19,9 @@ p. El rey negro puede ir a c6: se mueve hacia la casilla no desaparecida más cercano (si lo hay). -figure.diagram de contenedores +figure.diagram-container .diagram - | fen: rbkxxxbn / ppxppppx / 2qxxB2 / 4x2p / 3P1x2 / 3n1x2 / PPPxPPPP / RBxxxNKR b2, f2, b4, c5, g5, f6: + | fen:rbkxxxbn/ppxppppx/2qxxB2/4x2p/3P1x2/3n1x2/PPPxPPPP/RBxxxNKR b2,f2,b4,c5,g5,f6: figcaption Posibles movimientos para el caballo en d3. p. diff --git a/client/src/variants/Alice.js b/client/src/variants/Alice.js index a92338f7..aa956928 100644 --- a/client/src/variants/Alice.js +++ b/client/src/variants/Alice.js @@ -282,14 +282,17 @@ export const VariantRules = class AliceRules extends ChessRules { } static get VALUES() { - return Object.assign(ChessRules.VALUES, { - s: 1, - u: 5, - o: 3, - c: 3, - t: 9, - l: 1000 - }); + return Object.assign( + { + s: 1, + u: 5, + o: 3, + c: 3, + t: 9, + l: 1000 + }, + ChessRules.VALUES + ); } getNotation(move) { diff --git a/client/src/variants/Antiking.js b/client/src/variants/Antiking.js index f07f6268..21e37f4b 100644 --- a/client/src/variants/Antiking.js +++ b/client/src/variants/Antiking.js @@ -145,7 +145,10 @@ export const VariantRules = class AntikingRules extends ChessRules { } static get VALUES() { - return Object.assign(ChessRules.VALUES, { a: 1000 }); + return Object.assign( + { a: 1000 }, + ChessRules.VALUES + ); } static GenRandInitFen() { diff --git a/client/src/variants/Enpassant.js b/client/src/variants/Enpassant.js index 374a620c..3f8f3b2e 100644 --- a/client/src/variants/Enpassant.js +++ b/client/src/variants/Enpassant.js @@ -67,6 +67,52 @@ export const VariantRules = class EnpassantRules extends ChessRules { return res.slice(0, -1); //remove last comma } + getPotentialMovesFrom([x, y]) { + let moves = super.getPotentialMovesFrom([x,y]); + // Add en-passant captures from this square: + const L = this.epSquares.length; + if (!this.epSquares[L - 1]) return moves; + const squares = this.epSquares[L - 1]; + const S = squares.length; + // Object describing the removed opponent's piece: + const pipoV = new PiPo({ + x: squares[S-1].x, + y: squares[S-1].y, + c: V.GetOppCol(this.turn), + p: this.getPiece(squares[S-1].x, squares[S-1].y) + }); + // Check if existing non-capturing moves could also capture en passant + moves.forEach(m => { + if ( + m.appear[0].p != V.PAWN && //special pawn case is handled elsewhere + m.vanish.length <= 1 && + [...Array(S-1).keys()].some(i => { + return m.end.x == squares[i].x && m.end.y == squares[i].y; + }) + ) { + m.vanish.push(pipoV); + } + }); + // Special case of the king knight's movement: + if (this.getPiece(x, y) == V.KING) { + V.steps[V.KNIGHT].forEach(step => { + const endX = x + step[0]; + const endY = y + step[1]; + if ( + V.OnBoard(endX, endY) && + [...Array(S-1).keys()].some(i => { + return endX == squares[i].x && endY == squares[i].y; + }) + ) { + let enpassantMove = this.getBasicMove([x, y], [endX, endY]); + enpassantMove.vanish.push(pipoV); + moves.push(enpassantMove); + } + }); + } + return moves; + } + // TODO: this getPotentialPawnMovesFrom() is mostly duplicated: // it could be split in "capture", "promotion", "enpassant"... getPotentialPawnMoves([x, y]) { @@ -144,11 +190,19 @@ export const VariantRules = class EnpassantRules extends ChessRules { } // Remove the "onestep" condition: knight promote to knightrider: - getPotentialKnightMoves(sq) { return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT]); } + 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 captures: + return filteredMoves.filter(m => m.vanish.length == 1); + } + isAttackedByKnight(sq, colors) { return this.isAttackedBySlideNJump( sq, @@ -158,52 +212,6 @@ export const VariantRules = class EnpassantRules extends ChessRules { ); } - getPotentialMovesFrom([x, y]) { - let moves = super.getPotentialMovesFrom([x,y]); - // Add en-passant captures from this square: - const L = this.epSquares.length; - if (!this.epSquares[L - 1]) return moves; - const squares = this.epSquares[L - 1]; - const S = squares.length; - // Object describing the removed opponent's piece: - const pipoV = new PiPo({ - x: squares[S-1].x, - y: squares[S-1].y, - c: V.GetOppCol(this.turn), - p: this.getPiece(squares[S-1].x, squares[S-1].y) - }); - // Check if existing non-capturing moves could also capture en passant - moves.forEach(m => { - if ( - m.appear[0].p != V.PAWN && //special pawn case is handled elsewhere - m.vanish.length <= 1 && - [...Array(S-1).keys()].some(i => { - return m.end.x == squares[i].x && m.end.y == squares[i].y; - }) - ) { - m.vanish.push(pipoV); - } - }); - // Special case of the king knight's movement: - if (this.getPiece(x, y) == V.KING) { - V.steps[V.KNIGHT].forEach(step => { - const endX = x + step[0]; - const endY = y + step[1]; - if ( - V.OnBoard(endX, endY) && - [...Array(S-1).keys()].some(i => { - return endX == squares[i].x && endY == squares[i].y; - }) - ) { - let enpassantMove = this.getBasicMove([x, y], [endX, endY]); - enpassantMove.vanish.push(pipoV); - moves.push(enpassantMove); - } - }); - } - return moves; - } - static get VALUES() { return { p: 1, diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js index d804f614..d3659af8 100644 --- a/client/src/variants/Grand.js +++ b/client/src/variants/Grand.js @@ -307,8 +307,8 @@ export const VariantRules = class GrandRules extends ChessRules { static get VALUES() { return Object.assign( - ChessRules.VALUES, - { c: 5, m: 7 } //experimental + { c: 5, m: 7 }, //experimental + ChessRules.VALUES ); } diff --git a/client/src/variants/Grasshopper.js b/client/src/variants/Grasshopper.js new file mode 100644 index 00000000..043c0fc4 --- /dev/null +++ b/client/src/variants/Grasshopper.js @@ -0,0 +1,133 @@ +import { ChessRules } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; +import { randInt } from "@/utils/alea"; + +export const VariantRules = class GrasshopperRules extends ChessRules { + static get GRASSHOPPER() { + return "g"; + } + + static get PIECES() { + return ChessRules.PIECES.concat([V.GRASSHOPPER]); + } + + getPpath(b) { + return (b[1] == V.GRASSHOPPER ? "Grasshopper/" : "") + b; + } + + getPotentialMovesFrom([x, y]) { + switch (this.getPiece(x, y)) { + case V.GRASSHOPPER: + return this.getPotentialGrasshopperMoves([x, y]); + default: + return super.getPotentialMovesFrom([x, y]); + } + } + + getPotentialGrasshopperMoves([x, y]) { + let moves = []; + // Look in every direction until an obstacle (to jump) is met + for (const step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + let i = x + step[0]; + let j = y + step[1]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += step[0]; + j += step[1]; + } + // Move is valid if the next square is empty or occupied by enemy + const nextSq = [i+step[0], j+step[1]]; + if (V.OnBoard(nextSq[0], nextSq[1]) && this.canTake([x, y], nextSq)) + moves.push(this.getBasicMove([x, y], nextSq)); + } + return moves; + } + + isAttacked(sq, colors) { + return ( + super.isAttacked(sq, colors) || + this.isAttackedByGrasshopper(sq, colors) + ); + } + + isAttackedByGrasshopper([x, y], colors) { + // Reversed process: is there an adjacent obstacle, + // and a grasshopper next in the same line? + for (const step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + const nextSq = [x+step[0], y+step[1]]; + if ( + V.OnBoard(nextSq[0], nextSq[1]) && + this.board[nextSq[0]][nextSq[1]] != V.EMPTY + ) { + let i = nextSq[0] + step[0]; + let j = nextSq[1] + step[1]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += step[0]; + j += step[1]; + } + if ( + V.OnBoard(i, j) && + this.getPiece(i, j) == V.GRASSHOPPER && + colors.includes(this.getColor(i, j)) + ) { + return true; + } + } + } + return false; + } + + static get VALUES() { + return Object.assign( + // TODO: grasshoppers power decline when less pieces on board... + { g: 3 }, + ChessRules.VALUES + ); + } + + static GenRandInitFen() { + let pieces = { w: new Array(10), b: new Array(10) }; + for (let c of ["w", "b"]) { + let positions = ArrayFun.range(8); + + // Get random squares for grasshoppers (unconstrained) + let randIndex = randInt(8); + const grasshopper1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = randInt(7); + const grasshopper2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Knights + randIndex = randInt(6); + let knight1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = randInt(5); + let knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Queen + randIndex = randInt(4); + let queenPos = positions[randIndex]; + positions.splice(randIndex, 1); + + let rook1Pos = positions[0]; + let kingPos = positions[1]; + let rook2Pos = positions[2]; + + pieces[c][rook1Pos] = "r"; + pieces[c][knight1Pos] = "n"; + pieces[c][grasshopper1Pos] = "g"; + pieces[c][queenPos] = "q"; + pieces[c][kingPos] = "k"; + pieces[c][grasshopper2Pos] = "g"; + pieces[c][knight2Pos] = "n"; + pieces[c][rook2Pos] = "r"; + } + return ( + pieces["b"].join("") + + "/pppppppp/8/8/8/8/PPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + " w 0 1111 -" + ); + } +}; diff --git a/client/src/variants/Hiddenqueen.js b/client/src/variants/Hiddenqueen.js new file mode 100644 index 00000000..fe34fb28 --- /dev/null +++ b/client/src/variants/Hiddenqueen.js @@ -0,0 +1,155 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; +import { randInt } from "@/utils/alea"; + +export const VariantRules = class HiddenqueenRules extends ChessRules { + // Analyse in Hiddenqueen mode makes no sense + static get CanAnalyze() { + return false; + } + + static get HIDDEN_QUEEN() { + return 't'; + } + + static get PIECES() { + return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE)); + } + + getPiece(i, j) { + const piece = this.board[i][j].charAt(1); + if ( + piece != V.HIDDEN_QUEEN || + // 'side' is used to determine what I see: a pawn or a (hidden)queen? + this.getColor(i, j) == this.side + ) { + return piece; + } + return V.PAWN; + } + + getPpath(b, color, score) { + if (b[1] == V.HIDDEN_QUEEN) { + // Supposed to be hidden. + if (score == "*" && (!color || color != b[0])) + return b[0] + "p"; + return "Hiddenqueen/" + b[0] + "t"; + } + return b; + } + + isValidPawnMove(move) { + const color = move.vanish[0].c; + const pawnShift = color == "w" ? -1 : 1; + const startRank = color == "w" ? V.size.x - 2 : 1; + const lastRank = color == "w" ? 0 : V.size.x - 1; + return ( + // The queen is discovered if she reaches the 8th rank, + // even if this would be a technically valid pawn move. + move.end.x != lastRank && + ( + ( + move.end.x - move.start.x == pawnShift && + ( + ( + // Normal move + move.end.y == move.start.y && + this.board[move.end.x][move.end.y] == V.EMPTY + ) + || + ( + // Capture + Math.abs(move.end.y - move.start.y) == 1 && + this.board[move.end.x][move.end.y] != V.EMPTY + ) + ) + ) + || + ( + // Two-spaces initial jump + move.start.x == startRank && + move.end.y == move.start.y && + move.end.x - move.start.x == 2 * pawnShift && + this.board[move.end.x][move.end.y] == V.EMPTY + ) + ) + ); + } + + getPotentialMovesFrom([x, y]) { + if (this.getPiece(x, y) == V.HIDDEN_QUEEN) { + const pawnMoves = super.getPotentialPawnMoves([x, y]); + let queenMoves = super.getPotentialQueenMoves([x, y]); + // Remove from queen moves those corresponding to a pawn move: + queenMoves = queenMoves + .filter(m => !this.isValidPawnMove(m)) + // Hidden queen is revealed if moving like a queen: + .map(m => { + m.appear[0].p = V.QUEEN; + return m; + }); + return pawnMoves.concat(queenMoves); + } + return super.getPotentialMovesFrom([x, y]); + } + + getPossibleMovesFrom(sq) { + this.side = this.turn; + return this.filterValid(this.getPotentialMovesFrom(sq)); + } + + static GenRandInitFen() { + let fen = ChessRules.GenRandInitFen(); + // Place hidden queens at random: + let hiddenQueenPos = randInt(8); + let pawnRank = "PPPPPPPP".split(""); + pawnRank[hiddenQueenPos] = "T"; + fen = fen.replace("PPPPPPPP", pawnRank.join("")); + hiddenQueenPos = randInt(8); + pawnRank = "pppppppp".split(""); + pawnRank[hiddenQueenPos] = "t"; + fen = fen.replace("pppppppp", pawnRank.join("")); + return fen; + } + + updateVariables(move) { + super.updateVariables(move); + if (move.vanish.length == 2 && move.vanish[1].p == V.KING) + // We took opponent king + this.kingPos[this.turn] = [-1, -1]; + } + + unupdateVariables(move) { + super.unupdateVariables(move); + const c = move.vanish[0].c; + const oppCol = V.GetOppCol(c); + if (this.kingPos[oppCol][0] < 0) + // Last move took opponent's king: + this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y]; + } + + getCurrentScore() { + const color = this.turn; + if (this.kingPos[color][0] < 0) + // King disappeared + return color == "w" ? "0-1" : "1-0"; + return super.getCurrentScore(); + } + + // Search is biased, so not really needed to explore deeply + static get SEARCH_DEPTH() { + return 2; + } + + static get VALUES() { + return Object.assign( + { t: 9 }, + ChessRules.VALUES + ); + } + + getComputerMove() { + this.side = this.turn; + return super.getComputerMove(); + } +}; diff --git a/client/src/variants/Knightmate.js b/client/src/variants/Knightmate.js new file mode 100644 index 00000000..c0e84c63 --- /dev/null +++ b/client/src/variants/Knightmate.js @@ -0,0 +1,132 @@ +import { ChessRules } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; +import { randInt } from "@/utils/alea"; + +export const VariantRules = class KnightmateRules extends ChessRules { + static get COMMONER() { + return "c"; + } + + static get PIECES() { + return ChessRules.PIECES.concat([V.COMMONER]); + } + + getPpath(b) { + return ([V.KING, V.COMMONER].includes(b[1]) ? "Knightmate/" : "") + b; + } + + static GenRandInitFen() { + let pieces = { w: new Array(8), b: new Array(8) }; + // Shuffle pieces on first and last rank + for (let c of ["w", "b"]) { + let positions = ArrayFun.range(8); + + // Get random squares for bishops + let randIndex = 2 * randInt(4); + const bishop1Pos = positions[randIndex]; + let randIndex_tmp = 2 * randInt(4) + 1; + const bishop2Pos = positions[randIndex_tmp]; + positions.splice(Math.max(randIndex, randIndex_tmp), 1); + positions.splice(Math.min(randIndex, randIndex_tmp), 1); + + // Get random squares for commoners + randIndex = randInt(6); + const commoner1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = randInt(5); + const commoner2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Get random square for queen + randIndex = randInt(4); + const queenPos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Rooks and king positions are now fixed, + // because of the ordering rook-king-rook + const rook1Pos = positions[0]; + const kingPos = positions[1]; + const rook2Pos = positions[2]; + + // Finally put the shuffled pieces in the board array + pieces[c][rook1Pos] = "r"; + pieces[c][commoner1Pos] = "c"; + pieces[c][bishop1Pos] = "b"; + pieces[c][queenPos] = "q"; + pieces[c][kingPos] = "k"; + pieces[c][bishop2Pos] = "b"; + pieces[c][commoner2Pos] = "c"; + pieces[c][rook2Pos] = "r"; + } + // Add turn + flags + enpassant + return ( + pieces["b"].join("") + + "/pppppppp/8/8/8/8/PPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + " w 0 1111 -" + ); + } + + getPotentialMovesFrom([x, y]) { + switch (this.getPiece(x, y)) { + case V.COMMONER: + return this.getPotentialCommonerMoves([x, y]); + default: + return super.getPotentialMovesFrom([x, y]); + } + } + + getPotentialCommonerMoves(sq) { + return this.getSlideNJumpMoves( + sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + "oneStep" + ); + } + + getPotentialKingMoves(sq) { + return super.getPotentialKnightMoves(sq).concat(super.getCastleMoves(sq)); + } + + isAttacked(sq, colors) { + return ( + this.isAttackedByCommoner(sq, colors) || + this.isAttackedByPawn(sq, colors) || + this.isAttackedByRook(sq, colors) || + this.isAttackedByBishop(sq, colors) || + this.isAttackedByQueen(sq, colors) || + this.isAttackedByKing(sq, colors) + ); + } + + isAttackedByKing(sq, colors) { + return this.isAttackedBySlideNJump( + sq, + colors, + V.KING, + V.steps[V.KNIGHT], + "oneStep" + ); + } + + isAttackedByCommoner(sq, colors) { + return this.isAttackedBySlideNJump( + sq, + colors, + V.COMMONER, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + "oneStep" + ); + } + + static get VALUES() { + return { + p: 1, + r: 5, + c: 5, //the commoner is valuable + b: 3, + q: 9, + k: 1000 + }; + } +}; diff --git a/client/src/variants/Wildebeest.js b/client/src/variants/Wildebeest.js index 1634f494..fa89cb21 100644 --- a/client/src/variants/Wildebeest.js +++ b/client/src/variants/Wildebeest.js @@ -235,8 +235,8 @@ export const VariantRules = class WildebeestRules extends ChessRules { static get VALUES() { return Object.assign( - ChessRules.VALUES, - { c: 3, w: 7 } //experimental + { c: 3, w: 7 }, //experimental + ChessRules.VALUES ); } diff --git a/client/src/variants/Wormhole.js b/client/src/variants/Wormhole.js index b04efa94..76f6d127 100644 --- a/client/src/variants/Wormhole.js +++ b/client/src/variants/Wormhole.js @@ -264,6 +264,8 @@ export const VariantRules = class WormholeRules extends ChessRules { return this.isAttackedByJump(sq, colors, V.KING, V.steps[V.KING]); } + // NOTE: altering move in getBasicMove doesn't work and wouldn't be logical. + // This is a side-effect on board generated by the move. static PlayOnBoard(board, move) { board[move.vanish[0].x][move.vanish[0].y] = V.HOLE; for (let psq of move.appear) board[psq.x][psq.y] = psq.c + psq.p; diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue index 4307b9d0..0a66ffc1 100644 --- a/client/src/views/Analyse.vue +++ b/client/src/views/Analyse.vue @@ -41,15 +41,34 @@ export default { // then it doesn't trigger BaseGame.re_init() and the result is weird. created: function() { this.gameRef.vname = this.$route.params["vname"]; - this.gameRef.fen = this.$route.query["fen"].replace(/_/g, " "); - this.initialize(); + const routeFen = this.$route.query["fen"]; + if (!routeFen) this.alertAndQuit("Missing FEN"); + else { + this.gameRef.fen = routeFen.replace(/_/g, " "); + this.initialize(); + } }, methods: { + alertAndQuit: function(text, wrongVname) { + // Soon after component creation, st.tr might be uninitialized. + // Set a timeout to let a chance for the message to show translated. + const newUrl = "/variants" + (wrongVname ? "" : "/" + this.gameRef.vname); + setTimeout(() => { + alert(this.st.tr[text] || text); + this.$router.replace(newUrl); + }, 500); + }, initialize: async function() { // Obtain VariantRules object - const vModule = await import("@/variants/" + this.gameRef.vname + ".js"); - window.V = vModule.VariantRules; - this.loadGame(); + await import("@/variants/" + this.gameRef.vname + ".js") + .then((vModule) => { + window.V = vModule.VariantRules; + if (!V.CanAnalyze) + // Late check, in case the user tried to enter URL by hand + this.alertAndQuit("Analysis disabled for this variant"); + else this.loadGame(); + }) + .catch((err) => { this.alertAndQuit("Mispelled variant name", true); }); }, loadGame: function() { // NOTE: no need to set score (~unused) diff --git a/client/src/views/Rules.vue b/client/src/views/Rules.vue index c05e1843..556ea863 100644 --- a/client/src/views/Rules.vue +++ b/client/src/views/Rules.vue @@ -112,9 +112,20 @@ export default { return getDiagram(args); }, re_setVariant: async function(vname) { - const vModule = await import("@/variants/" + vname + ".js"); - this.V = window.V = vModule.VariantRules; - this.gameInfo.vname = vname; + await import("@/variants/" + vname + ".js") + .then((vModule) => { + this.V = window.V = vModule.VariantRules; + this.gameInfo.vname = vname; + }) + .catch((err) => { + // Soon after component creation, st.tr might be uninitialized. + // Set a timeout to let a chance for the message to show translated. + const text = "Mispelled variant name"; + setTimeout(() => { + alert(this.st.tr[text] || text); + this.$router.replace("/variants"); + }, 500); + }); }, startGame: function(mode) { if (this.gameInProgress) return; diff --git a/server/db/populate.sql b/server/db/populate.sql index 2accc888..6ddedf69 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -20,7 +20,10 @@ insert or ignore into Variants (name,description) values ('Enpassant', 'Capture en passant'), ('Extinction', 'Capture all of a kind'), ('Grand', 'Big board'), + ('Grasshopper', 'Long jumps over pieces'), ('Hidden', 'Unidentified pieces'), + ('Hiddenqueen', 'Queen disguised as a pawn'), + ('Knightmate', 'Mate the knight'), ('Knightrelay', 'Move like a knight'), ('Losers', 'Lose all pieces'), ('Magnetic', 'Laws of attraction'), diff --git a/server/models/User.js b/server/models/User.js index f3adb31d..5117c175 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -141,13 +141,20 @@ const UserModel = const day = 86400000; db.serialize(function() { const query = - "SELECT id, sessionToken, created " + + "SELECT id, sessionToken, created, name, email " + "FROM Users"; db.all(query, (err, users) => { users.forEach(u => { - // Remove unlogged users for >1 day + // Remove unlogged users for > 24h if (!u.sessionToken && tsNow - u.created > day) + { + notify( + u, + "Your account has been deleted because " + + "you didn't log in for 24h after registration" + ); db.run("DELETE FROM Users WHERE id = " + u.id); + } }); }); });