From: Benjamin Auder Date: Sun, 29 Mar 2020 02:33:46 +0000 (+0200) Subject: A few small fixes + add Monster variant X-Git-Url: https://git.auder.net/variants/current/doc/css/img/assets/%3C?a=commitdiff_plain;h=5e1bc6519d4c81aeac40aec7390c64c913cbf566;p=vchess.git A few small fixes + add Monster variant --- diff --git a/TODO b/TODO index 67912987..ecc877a4 100644 --- a/TODO +++ b/TODO @@ -1,12 +1,9 @@ # New variants -Finish first https://www.chessvariants.com/mvopponent.dir/dynamo.html +Finish https://www.chessvariants.com/mvopponent.dir/dynamo.html https://echekk.fr/spip.php?page=article&id_article=599 -Monster, Horde, Colorbound +Colorbound https://www.chessvariants.com/d.betza/chessvar/dan/colclob.html -https://lichess.org/analysis/horde#0 -https://greenchess.net/rules.php?v=monster -https://en.wikipedia.org/wiki/Monster_chess https://en.wikipedia.org/wiki/Grotesque_(chess) (fun) Maxima, Interweave, Roccoco diff --git a/client/src/base_rules.js b/client/src/base_rules.js index d35a9186..22f57d5b 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -862,7 +862,9 @@ export const ChessRules = class ChessRules { i = y; do { if ( - (!castleInCheck && this.isAttacked([x, i], oppCol)) || + // NOTE: "castling" arg is used by some variants (Monster), + // where "isAttacked" is overloaded in an infinite-recursive way. + (!castleInCheck && this.isAttacked([x, i], oppCol, "castling")) || (this.board[x][i] != V.EMPTY && // NOTE: next check is enough, because of chessboard constraints (this.getColor(x, i) != c || @@ -882,9 +884,12 @@ export const ChessRules = class ChessRules { // Nothing on final squares, except maybe king and castling rook? for (i = 0; i < 2; i++) { if ( + finalSquares[castleSide][i] != rookPos && this.board[x][finalSquares[castleSide][i]] != V.EMPTY && - this.getPiece(x, finalSquares[castleSide][i]) != V.KING && - finalSquares[castleSide][i] != rookPos + ( + this.getPiece(x, finalSquares[castleSide][i]) != V.KING || + this.getColor(x, finalSquares[castleSide][i]) != c + ) ) { continue castlingCheck; } @@ -942,9 +947,7 @@ export const ChessRules = class ChessRules { }); } - // Search for all valid moves considering current turn - // (for engine and game end) - getAllValidMoves() { + getAllPotentialMoves() { const color = this.turn; let potentialMoves = []; for (let i = 0; i < V.size.x; i++) { @@ -957,7 +960,13 @@ export const ChessRules = class ChessRules { } } } - return this.filterValid(potentialMoves); + return potentialMoves; + } + + // Search for all valid moves considering current turn + // (for engine and game end) + getAllValidMoves() { + return this.filterValid(this.getAllPotentialMoves()); } // Stop at the first move found diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 51d087a3..1ec412e1 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -228,10 +228,8 @@ export default { this.incheck = this.vr.getCheckSquares(this.vr.turn); const score = this.vr.getCurrentScore(); if (L > 0 && this.moves[L - 1].notation != "...") { - if (["1-0","0-1"].includes(score)) - this.moves[L - 1].notation += "#"; - else if (this.vr.getCheckSquares(this.vr.turn).length > 0) - this.moves[L - 1].notation += "+"; + if (["1-0","0-1"].includes(score)) this.moves[L - 1].notation += "#"; + else if (this.incheck.length > 0) this.moves[L - 1].notation += "+"; } }, positionCursorTo: function(index) { @@ -434,10 +432,8 @@ export default { const computeScore = () => { const score = this.vr.getCurrentScore(); if (!navigate) { - if (["1-0","0-1"].includes(score)) - this.lastMove.notation += "#"; - else if (this.vr.getCheckSquares(this.vr.turn).length > 0) - this.lastMove.notation += "+"; + if (["1-0","0-1"].includes(score)) this.lastMove.notation += "#"; + else if (this.incheck.length > 0) this.lastMove.notation += "+"; } if (score != "*" && this.game.mode == "analyze") { const message = getScoreMessage(score); diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 04900207..86b75dde 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -169,6 +169,7 @@ export const translations = { "Captures reborn": "Captures reborn", "Change colors": "Change colors", "Dangerous collisions": "Dangerous collisions", + "Double moves": "Double moves", "Each piece is unique": "Each piece is unique", "Exotic captures": "Exotic captures", "Explosive captures": "Explosive captures", @@ -191,7 +192,6 @@ export const translations = { "Mongolian Horde": "Mongolian Horde", "Move like a knight (v1)": "Move like a knight (v1)", "Move like a knight (v2)": "Move like a knight (v2)", - "Move twice": "Move twice", "Neverending rows": "Neverending rows", "No-check mode": "No-check mode", "Pawns move diagonally": "Pawns move diagonally", @@ -212,5 +212,6 @@ export const translations = { "Transform an essay": "Transform an essay", "Two kings": "Two kings", "Two royal pieces": "Two royal pieces", - "Unidentified pieces": "Unidentified pieces" + "Unidentified pieces": "Unidentified pieces", + "White move twice": "White move twice" }; diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 32282e25..1cd0b527 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -169,6 +169,7 @@ export const translations = { "Captures reborn": "Las capturas renacen", "Change colors": "Cambiar colores", "Dangerous collisions": "Colisiones peligrosas", + "Double moves": "Jugadas doble", "Each piece is unique": "Cada pieza es única", "Exotic captures": "Capturas exóticas", "Explosive captures": "Capturas explosivas", @@ -191,7 +192,6 @@ export const translations = { "Mongolian Horde": "Horda mongol", "Move like a knight (v1)": "Moverse como un caballo (v1)", "Move like a knight (v2)": "Moverse como un caballo (v2)", - "Move twice": "Mover dos veces", "Neverending rows": "Filas interminables", "No-check mode": "Modo sin jaque", "Pawns move diagonally": "Peones se mueven en diagonal", @@ -212,5 +212,6 @@ export const translations = { "Transform an essay": "Transformar un ensayo", "Two kings": "Dos reyes", "Two royal pieces": "Dos piezas reales", - "Unidentified pieces": "Piezas no identificadas" + "Unidentified pieces": "Piezas no identificadas", + "White move twice": "Las blancas juegan dos veces" }; diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 0e516844..f4c956fa 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -169,6 +169,7 @@ export const translations = { "Captures reborn": "Les captures renaissent", "Change colors": "Changer les couleurs", "Dangerous collisions": "Collisions dangeureuses", + "Double moves": "Coups doubles", "Each piece is unique": "Chaque pièce est unique", "Exotic captures": "Captures exotiques", "Explosive captures": "Captures explosives", @@ -191,7 +192,6 @@ export const translations = { "Mongolian Horde": "Horde mongole", "Move like a knight (v1)": "Bouger comme un cavalier (v1)", "Move like a knight (v2)": "Bouger comme un cavalier (v2)", - "Move twice": "Jouer deux coups", "Neverending rows": "Rangées sans fin", "No-check mode": "Mode sans échec", "Pawns move diagonally": "Les pions vont en diagonale", @@ -212,5 +212,6 @@ export const translations = { "Transform an essay": "Transformer un essai", "Two kings": "Deux rois", "Two royal pieces": "Deux pièces royales", - "Unidentified pieces": "Pièces non identifiées" + "Unidentified pieces": "Pièces non identifiées", + "White move twice": "Les blancs jouent deux fois" }; diff --git a/client/src/translations/rules/Monster/en.pug b/client/src/translations/rules/Monster/en.pug new file mode 100644 index 00000000..1401cda0 --- /dev/null +++ b/client/src/translations/rules/Monster/en.pug @@ -0,0 +1,49 @@ +p.boxed. + White has only four pawns and the king, but move twice at each turn. + +figure.diagram-container + .diagram + | fen:rnbqkbnr/pppppppp/8/8/8/8/2PPPP2/4K3: + figcaption Standard deterministic position + +p. + The white army can appear much too small, but the power to move twice in a + row shouldn't be underestimated. At each turn white plays two moves with + only one constraint: do not be under check in the end. + So if the white king attacks a defended piece, he can take it anyway by + coming back on its initial square on (sub)move 2. + +figure.diagram-container + .diagram.diag12 + | fen:rbbknn1r/1p2pp1p/2p3qK/p2p2p1/2PPP3/8/5P2/8: + .diagram.diag22 + | fen:3rq3/1p4p1/1k1pKp2/3P1P1n/p7/5n2/8/8: + figcaption Left: not a checkmate! Right: a "Monster-checkmate". + +p + | The diagram position on the left looks pretty much like a checkmate, + | but white can take the queen and come back to the h6 square. Finally, + | white can mate in an unusual way, like the following diagram found + a(href="https://en.wikipedia.org/wiki/Monster_chess") on Wikipedia + | . There is no way for the black king to avoid being captured since white + | plays twice (the threat is 2.d7,dxe8). + +figure.diagram-container + .diagram.diag12 + | fen:4k3/8/4P3/8/3P4/8/2q5/7K: + .diagram.diag22 + | fen:4k3/8/3PP3/8/8/8/2q5/7K: + figcaption Left: before 1.d5,d6. Right: after this move:, it's checkmate. + +h3 More information + +p + | Ralph Betza analyses this variant and the double move on + a(href="https://www.chessvariants.com/d.betza/chessvar/muenster.html") + | this page + | . There seems to be a common belief that black should win with accurate + | play, but it's clearly hard to demonstrate. And if someone can show a + | winning strategy, we'll add some white material to balance this game. + | Meanwhile, the variant is also playable + a(href="https://greenchess.net/rules.php?v=monster") on greenchess.net + | . diff --git a/client/src/translations/rules/Monster/es.pug b/client/src/translations/rules/Monster/es.pug new file mode 100644 index 00000000..87776c40 --- /dev/null +++ b/client/src/translations/rules/Monster/es.pug @@ -0,0 +1,54 @@ +p.boxed. + Las blancas solo tienen cuatro peones y un rey, pero hacen dos movimientos + a cada turno. + +figure.diagram-container + .diagram + | fen:rnbqkbnr/pppppppp/8/8/8/8/2PPPP2/4K3: + figcaption Posición inicial estándar + +p. + El ejército blanco puede parecer demasiado pequeño, pero el poder de jugar + dos veces seguidas y no debe subestimarse. En cada turno las blancas juegan + dos jugadas con la única restricción de no estar en jaque hasta el final. + Entonces, si tu rey blanco ataca una habitación protegida, él puede + tómalo de todos modos y luego regresa a tu caso inicial en el segundo + (sub)movimiento. + +figure.diagram-container + .diagram.diag12 + | fen:rbbknn1r/1p2pp1p/2p3qK/p2p2p1/2PPP3/8/5P2/8: + .diagram.diag22 + | fen:3rq3/1p4p1/1k1pKp2/3P1P1n/p7/5n2/8/8: + figcaption Izquierda: no es una jaque mate! Derecha: un "Monster-mate". + +p + | La posición del diagrama de la izquierda se ve bien como un jaque mate, + | pero las blancas pueden tomar la dama y volver llevar en h6. Finalmente, + | las blancas a veces pueden dar jaque mate de una manera inusual como se + | muestra en el siguiente diagrama encontrado + a(href="https://en.wikipedia.org/wiki/Monster_chess") en Wikipedia + | . El rey negro no tiene forma de escapar de la captura porque las blancas + | hacen dos movimientos (la amenaza es 2.d7, dxe8). + +figure.diagram-container + .diagram.diag12 + | fen:4k3/8/4P3/8/3P4/8/2q5/7K: + .diagram.diag22 + | fen:4k3/8/3PP3/8/8/8/2q5/7K: + figcaption. + Izquierda: antes de 1.d5,d6. + Derecha: después de esta jugada, es jaque mate. + +h3 Más información + +p + | Ralph Betza analiza esta variante y el doble movimiento en + a(href="https://www.chessvariants.com/d.betza/chessvar/muenster.html") + | esta página + | . La opinión general parece indicar que las negras deben ganar con un + | juego precisa, pero es claramente difícil de demostrar. Y si es necesario, + | se podría agregar material a las blancas para reequilibrar el juego. + | Dicho esto, la variante también es jugable + a(href="https://greenchess.net/rules.php?v=monster") en greenchess.net + | . diff --git a/client/src/translations/rules/Monster/fr.pug b/client/src/translations/rules/Monster/fr.pug new file mode 100644 index 00000000..b97b61ca --- /dev/null +++ b/client/src/translations/rules/Monster/fr.pug @@ -0,0 +1,51 @@ +p.boxed. + Les blancs n'ont que quatre pions et un roi, mais jouent deux coups à chaque + tour. + +figure.diagram-container + .diagram + | fen:rnbqkbnr/pppppppp/8/8/8/8/2PPPP2/4K3: + figcaption Position initiale standard + +p. + L'armée blanche peut paraître bien trop réduite, mais le pouvoir de jouer + deux fois d'affilée ne doit pas être sous-estimé. À chaque tour les + blancs jouent deux coups avec pour seule contrainte de ne pas être en échec + à la fin. Ainsi, si le roi blanc attaque une pièce protégée, il peut la + prendre quand-même puis revenir sur sa case initiale au second (sous)coup. + +figure.diagram-container + .diagram.diag12 + | fen:rbbknn1r/1p2pp1p/2p3qK/p2p2p1/2PPP3/8/5P2/8: + .diagram.diag22 + | fen:3rq3/1p4p1/1k1pKp2/3P1P1n/p7/5n2/8/8: + figcaption Gauche : pas un mat ! Droite : un "Monster-mat". + +p + | La position du diagramme à gauche ressemble bien à un mat, mais les blancs + | peuvent prendre la dame et revenir en h6. Enfin, les blancs peuvent parfois + | mater d'une manière inhabituelle comme le montre le diagramme suivant + | trouvé + a(href="https://en.wikipedia.org/wiki/Monster_chess") sur Wikipedia + | . Le roi noir n'a aucun moyen d'échapper à la capture puisque les blancs + | jouent deux coups (la menace est 2.d7,dxe8). + +figure.diagram-container + .diagram.diag12 + | fen:4k3/8/4P3/8/3P4/8/2q5/7K: + .diagram.diag22 + | fen:4k3/8/3PP3/8/8/8/2q5/7K: + figcaption Gauche : avant 1.d5,d6. Droite : après ce coup, c'est mat. + +h3 Plus d'information + +p + | Ralph Betza analyse cette variante et le double coup sur + a(href="https://www.chessvariants.com/d.betza/chessvar/muenster.html") + | cette page + | . L'avis général semble indiquer que les noirs doivent gagner avec un jeu + | précis, mais c'est clairement difficile à démontrer. Et le cas échéant, on + | pourrait ajouter du matériel aux blancs pour rééquilibrer le jeu. + | Ceci dit, la variante est jouable également + a(href="https://greenchess.net/rules.php?v=monster") sur greenchess.net + | . diff --git a/client/src/variants/Chess960.js b/client/src/variants/Chess960.js index 3f6af502..15eb7ecc 100644 --- a/client/src/variants/Chess960.js +++ b/client/src/variants/Chess960.js @@ -1,4 +1,5 @@ import { ChessRules } from "@/base_rules"; + export class Chess960Rules extends ChessRules { // Standard rules }; diff --git a/client/src/variants/Dynamo.js b/client/src/variants/Dynamo.js index 9f4cf3ff..9d47fd8f 100644 --- a/client/src/variants/Dynamo.js +++ b/client/src/variants/Dynamo.js @@ -351,8 +351,7 @@ export class DynamoRules extends ChessRules { if (this.subTurn == 2) { this.subTurn = 1; this.movesCount--; - } - else { + } else { // subTurn == 1 (after a move played) this.turn = V.GetOppCol(this.turn); this.subTurn = 2; diff --git a/client/src/variants/Monster.js b/client/src/variants/Monster.js new file mode 100644 index 00000000..177f71d1 --- /dev/null +++ b/client/src/variants/Monster.js @@ -0,0 +1,186 @@ +import { ChessRules } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class MonsterRules extends ChessRules { + static IsGoodFlags(flags) { + // Only black can castle + return !!flags.match(/^[a-z]{2,2}$/); + } + + static GenRandInitFen(randomness) { + if (randomness == 2) randomness--; + const fen = ChessRules.GenRandInitFen(randomness); + return ( + // 26 first chars are 6 rows + 6 slashes + fen.substr(0, 26) + // En passant available, and "half-castle" + .concat("2PPPP2/4K3 w 0 ") + .concat(fen.substr(-6, 2)) + .concat(" -") + ); + } + + getFlagsFen() { + return this.castleFlags['b'].map(V.CoordToColumn).join(""); + } + + setFlags(fenflags) { + this.castleFlags = { 'b': [-1, -1] }; + for (let i = 0; i < 2; i++) + this.castleFlags['b'][i] = V.ColumnToCoord(fenflags.charAt(i)); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + this.subTurn = 1; + } + + getPotentialKingMoves([x, y]) { + if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]); + // White doesn't castle: + return this.getSlideNJumpMoves( + [x, y], + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + "oneStep" + ); + } + + isAttacked(sq, color, castling) { + const singleMoveAttack = super.isAttacked(sq, color); + if (singleMoveAttack) return true; + if (color == 'b' || !!castling) return singleMoveAttack; + // Attacks by white: double-move allowed + const curTurn = this.turn; + this.turn = 'w'; + const w1Moves = super.getAllPotentialMoves(); + this.turn = curTurn; + for (let move of w1Moves) { + this.play(move); + const res = super.isAttacked(sq, 'w'); + this.undo(move); + if (res) return res; + } + return false; + } + + play(move) { + move.flags = JSON.stringify(this.aggregateFlags()); + if (this.turn == 'b' || this.subTurn == 2) + this.epSquares.push(this.getEpSquare(move)); + else this.epSquares.push(null); + V.PlayOnBoard(this.board, move); + if (this.turn == 'w') { + if (this.subTurn == 1) this.movesCount++; + else this.turn = 'b'; + this.subTurn = 3 - this.subTurn; + } else { + this.turn = 'w'; + this.movesCount++; + } + this.postPlay(move); + } + + updateCastleFlags(move, piece) { + // Only black can castle: + const firstRank = 0; + if (piece == V.KING && move.appear[0].c == 'b') + this.castleFlags['b'] = [8, 8]; + else if ( + move.start.x == firstRank && + this.castleFlags['b'].includes(move.start.y) + ) { + const flagIdx = (move.start.y == this.castleFlags['b'][0] ? 0 : 1); + this.castleFlags['b'][flagIdx] = 8; + } + else if ( + move.end.x == firstRank && + this.castleFlags['b'].includes(move.end.y) + ) { + const flagIdx = (move.end.y == this.castleFlags['b'][0] ? 0 : 1); + this.castleFlags['b'][flagIdx] = 8; + } + } + + postPlay(move) { + // Definition of 'c' in base class doesn't work: + const c = move.vanish[0].c; + const piece = move.vanish[0].p; + if (piece == V.KING && move.appear.length > 0) { + this.kingPos[c][0] = move.appear[0].x; + this.kingPos[c][1] = move.appear[0].y; + return; + } + this.updateCastleFlags(move, piece); + } + + undo(move) { + this.epSquares.pop(); + this.disaggregateFlags(JSON.parse(move.flags)); + V.UndoOnBoard(this.board, move); + if (this.turn == 'w') { + if (this.subTurn == 2) this.subTurn = 1; + else this.turn = 'b'; + this.movesCount--; + } else { + this.turn = 'w'; + this.subTurn = 2; + } + this.postUndo(move); + } + + filterValid(moves) { + if (this.turn == 'w' && this.subTurn == 1) { + return moves.filter(m1 => { + this.play(m1); + // NOTE: no recursion because next call will see subTurn == 2 + const res = super.atLeastOneMove(); + this.undo(m1); + return res; + }); + } + return super.filterValid(moves); + } + + static get SEARCH_DEPTH() { + return 1; + } + + getComputerMove() { + const color = this.turn; + if (color == 'w') { + // Generate all sequences of 2-moves + const moves1 = this.getAllValidMoves(); + moves1.forEach(m1 => { + m1.eval = -V.INFINITY; + m1.move2 = null; + this.play(m1); + const moves2 = this.getAllValidMoves(); + moves2.forEach(m2 => { + this.play(m2); + const eval2 = this.evalPosition(); + this.undo(m2); + if (eval2 > m1.eval) { + m1.eval = eval2; + m1.move2 = m2; + } + }); + this.undo(m1); + }); + moves1.sort((a, b) => b.eval - a.eval); + let candidates = [0]; + for ( + let i = 1; + i < moves1.length && moves1[i].eval == moves1[0].eval; + i++ + ) { + candidates.push(i); + } + const idx = candidates[randInt(candidates.length)]; + const move2 = moves1[idx].move2; + delete moves1[idx]["move2"]; + return [moves1[idx], move2]; + } + // For black at depth 1, super method is fine: + return super.getComputerMove(); + } +}; diff --git a/server/db/populate.sql b/server/db/populate.sql index 147868d9..83ba4cc0 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -42,7 +42,8 @@ insert or ignore into Variants (name, description) values ('Knightrelay2', 'Move like a knight (v2)'), ('Losers', 'Get strong at self-mate'), ('Magnetic', 'Laws of attraction'), - ('Marseille', 'Move twice'), + ('Marseille', 'Double moves'), + ('Monster', 'White move twice'), ('Orda', 'Mongolian Horde'), ('Parachute', 'Landing on the board'), ('Perfect', 'Powerful pieces'),