From: Benjamin Auder Date: Wed, 29 Apr 2020 19:55:45 +0000 (+0200) Subject: Simplify Monster + Doublemove2. Smoother example games X-Git-Url: https://git.auder.net/?p=vchess.git;a=commitdiff_plain;h=bc0b9205e41c5db0552e4ccf060b945342e36ed0 Simplify Monster + Doublemove2. Smoother example games --- diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue index ad56543e..f611167d 100644 --- a/client/src/components/ComputerGame.vue +++ b/client/src/components/ComputerGame.vue @@ -44,7 +44,11 @@ export default { setTimeout(() => { if (this.currentUrl != document.location.href) return; //page change self.$refs["basegame"].play(compMove, "received"); - self.processMove(compMove); + const animationLength = + // 250 = length of animation, 500 = delay between sub-moves + // TODO: a callback would be cleaner. + 250 + (Array.isArray(compMove) ? (compMove.length - 1) * 750 : 0); + setTimeout(() => self.processMove(compMove), animationLength); self.compThink = false; if (self.game.score != "*") // User action diff --git a/client/src/translations/rules/Doublemove2/en.pug b/client/src/translations/rules/Doublemove2/en.pug index c1bda2ee..ca24491f 100644 --- a/client/src/translations/rules/Doublemove2/en.pug +++ b/client/src/translations/rules/Doublemove2/en.pug @@ -1,5 +1,5 @@ p.boxed - | Move twice at every turn. + | Move twice at every turn. The goal is to capture the king. p. The only difference with orthodox chess is the double-move rule, @@ -8,7 +8,7 @@ p. p. At the very first move of the game, white make only one move - as usual. However, after that and for all the game each side must play twice at - every turn. The goal is to checkmate. + every turn (even if the first move captures the enemy king). figure.diagram-container .diagram.diag12 @@ -17,14 +17,12 @@ figure.diagram-container | fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR: figcaption. Left: after the moves 1.e4 e5, the 'd' pawn is pinned. - Right: the black king is under check. + Right: the black king is "under check". p. In the diagram position on the left, after the first black move ...e5, - the 'd' pawn is pinned because moving it would allow 2.Bb5,Bxe8 capturing - the king. - On the right, after 2.Qf3,Bc4 black king is under check because of - the options 3.Bxf7,Bxe8 or 3.Qxf7,Qxe8. + moving the 'd' pawn would allow 2.Bb5,Bxe8 capturing the king. + On the right, after 2.Qf3,Bc4 the threats are 3.Bxf7,Bxe8 and 3.Qxf7,Qxe8. h3 En-passant capture @@ -50,3 +48,5 @@ p | . It is also playable a(href="https://greenchess.net/rules.php?v=double-move") on greenchess | . + +p Inventor: Fred Galvin (1957) diff --git a/client/src/translations/rules/Doublemove2/es.pug b/client/src/translations/rules/Doublemove2/es.pug index aa4751d6..179025b3 100644 --- a/client/src/translations/rules/Doublemove2/es.pug +++ b/client/src/translations/rules/Doublemove2/es.pug @@ -1,15 +1,15 @@ p.boxed - | Juega dos jugadas a cada turno. + | Juega dos jugadas a cada turno. El objetivo es capturar al rey. p. La única diferencia con el juego ortodoxo es la regla del doble movimiento, pero eso afecta mucho el juego p. - Al comienzo del juego, los blancos solo juegan un movimiento, como + Al comienzo del juego, las blancas solo juegan un movimiento, como en general. Sin embargo, después de eso y por el resto del juego, - cada lado debe jugar dos jugadas cada turno. - El objetivo es de dar jaque mate. + cada lado debe jugar dos jugadas cada turno + (incluso si la primera captura al rey contrario). figure.diagram-container .diagram.diag12 @@ -18,14 +18,13 @@ figure.diagram-container | fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR: figcaption. Izquierda: después de las jugadas 1.e4 e5, el peón 'd' se clava. - Derecha: el rey negro está en jaque. + Derecha: el rey negro está "en jaque". p. En la posición del diagrama de la izquierda, después del primer movimiento - negro ...e5, el peón 'd' está clavado porque su movimiento autorizaría - 2.Bb5,Bxe8 captura el rey. - A la derecha, después de 2.Qf3,Bc4 el rey negro está en jaque debido a las - posibilidades 3.Bxf7,Bxe8 o 3.Qxf7,Qxe8. + negro ...e5, un movimiento del peón 'd' autorizaría 2.Bb5,Bxe8 que + captura el rey. A la derecha, después de 2.Qf3,Bc4 las amenazas son + 3.Bxf7,Bxe8 y 3.Qxf7,Qxe8. h3 Captura en passant @@ -52,3 +51,5 @@ p | . También es jugable a(href="https://greenchess.net/rules.php?v=double-move") en greenchess | . + +p Inventor: Fred Galvin (1957) diff --git a/client/src/translations/rules/Doublemove2/fr.pug b/client/src/translations/rules/Doublemove2/fr.pug index 66021864..0dff04f9 100644 --- a/client/src/translations/rules/Doublemove2/fr.pug +++ b/client/src/translations/rules/Doublemove2/fr.pug @@ -1,5 +1,5 @@ p.boxed - | Jouez deux coups à chaque tour. + | Jouez deux coups à chaque tour. Le but est de capturer le roi. p. La seule différence avec le jeu orthodoxe est la règle du double-coup, mais @@ -8,7 +8,8 @@ p. p. Au tout début de la partie les blancs ne jouent qu'un seul coup, comme d'habitude. Cependant, après cela et ce pour tout le reste de la partie - chaque camp doit jouer deux coups à chaque tour. L'objectif est de mater. + chaque camp doit jouer deux coups à chaque tour + (même si le premier capture le roi adverse). figure.diagram-container .diagram.diag12 @@ -17,14 +18,12 @@ figure.diagram-container | fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR: figcaption. Gauche : après les coups 1.e4 e5, le pion 'd' est cloué. - Droite : le roi noir est en échec. + Droite : le roi noir est "en échec". p. Dans la position du diagramme à gauche, après le premier coup noir ...e5, - le pion 'd' est cloué car son déplacement autoriserait 2.Bb5,Bxe8 capturant - le roi. - À droite, après 2.Qf3,Bc4 le roi noir est en échec à cause des possibilités - 3.Bxf7,Bxe8 ou 3.Qxf7,Qxe8. + un déplacement du pion 'd' autoriserait 2.Bb5,Bxe8 capturant le roi. + À droite, après 2.Qf3,Bc4 les menaces sont 3.Bxf7,Bxe8 et 3.Qxf7,Qxe8. h3 Prise en passant @@ -50,3 +49,5 @@ p | . Elle est jouable également a(href="https://greenchess.net/rules.php?v=double-move") sur greenchess | . + +p Inventeur : Fred Galvin (1957) diff --git a/client/src/translations/rules/Monster/en.pug b/client/src/translations/rules/Monster/en.pug index 8e7061de..d0551d10 100644 --- a/client/src/translations/rules/Monster/en.pug +++ b/client/src/translations/rules/Monster/en.pug @@ -8,10 +8,8 @@ figure.diagram-container 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. + row shouldn't be underestimated. At each turn white plays two moves + without any constraint. The goal is to capture the king. figure.diagram-container .diagram.diag12 diff --git a/client/src/translations/rules/Monster/es.pug b/client/src/translations/rules/Monster/es.pug index edf1bdf2..f9149f6e 100644 --- a/client/src/translations/rules/Monster/es.pug +++ b/client/src/translations/rules/Monster/es.pug @@ -10,10 +10,7 @@ figure.diagram-container 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. + dos jugadas sin ninguna restricción. El objetivo es capturar al rey. figure.diagram-container .diagram.diag12 diff --git a/client/src/translations/rules/Monster/fr.pug b/client/src/translations/rules/Monster/fr.pug index 757ae3e6..c0b99100 100644 --- a/client/src/translations/rules/Monster/fr.pug +++ b/client/src/translations/rules/Monster/fr.pug @@ -10,9 +10,8 @@ figure.diagram-container 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. + blancs jouent deux coups sans aucune contrainte. + L'objectif est de capturer le roi. figure.diagram-container .diagram.diag12 diff --git a/client/src/variants/Doublemove1.js b/client/src/variants/Doublemove1.js index 7d1ee4dc..c30ad0cf 100644 --- a/client/src/variants/Doublemove1.js +++ b/client/src/variants/Doublemove1.js @@ -201,7 +201,7 @@ export class Doublemove1Rules extends ChessRules { return res; }; - let moves11 = this.getAllValidMoves(); + const moves11 = this.getAllValidMoves(); let doubleMoves = []; // Rank moves using a min-max at depth 2 for (let i = 0; i < moves11.length; i++) { @@ -209,13 +209,14 @@ export class Doublemove1Rules extends ChessRules { if (this.turn != color) { // We gave check with last move: search the best opponent move doubleMoves.push({ moves: [moves11[i]], eval: getBestMoveEval() }); - } else { + } + else { let moves12 = this.getAllValidMoves(); for (let j = 0; j < moves12.length; j++) { this.play(moves12[j]); doubleMoves.push({ moves: [moves11[i], moves12[j]], - eval: getBestMoveEval() + eval: getBestMoveEval() + 0.05 - Math.random() / 10 }); this.undo(moves12[j]); } @@ -223,6 +224,8 @@ export class Doublemove1Rules extends ChessRules { this.undo(moves11[i]); } + // TODO: array + sort + candidates logic not required when adding small + // fluctuations to the eval function (could also be generalized). doubleMoves.sort((a, b) => { return (color == "w" ? 1 : -1) * (b.eval - a.eval); }); @@ -234,7 +237,6 @@ export class Doublemove1Rules extends ChessRules { ) { candidates.push(i); } - const selected = doubleMoves[randInt(candidates.length)].moves; if (selected.length == 1) return selected[0]; return selected; diff --git a/client/src/variants/Doublemove2.js b/client/src/variants/Doublemove2.js index d1afff83..080d13b6 100644 --- a/client/src/variants/Doublemove2.js +++ b/client/src/variants/Doublemove2.js @@ -79,40 +79,23 @@ export class Doublemove2Rules extends ChessRules { return moves; } - isAttacked(sq, color, castling) { - const singleMoveAttack = super.isAttacked(sq, color); - if (singleMoveAttack) return true; - if (!!castling) { - if (this.subTurn == 1) - // Castling at move 1 could be done into check - return false; - return singleMoveAttack; - } - // Double-move allowed: - const curTurn = this.turn; - this.turn = color; - const moves1 = super.getAllPotentialMoves(); - this.turn = curTurn; - for (let move of moves1) { - this.play(move); - const res = super.isAttacked(sq, color); - this.undo(move); - if (res) return res; - } + isAttacked(sq, color) { + // Goal is king capture => no checks return false; } filterValid(moves) { - if (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); + return moves; + } + + getCheckSquares() { + return []; + } + + getCurrentScore() { + const color = this.turn; + if (this.kingPos[color][0] < 0) return (color == 'w' ? "0-1" : "1-0"); + return "*"; } play(move) { @@ -139,12 +122,14 @@ export class Doublemove2Rules extends ChessRules { const firstRank = c == "w" ? V.size.x - 1 : 0; if (piece == V.KING && move.appear.length > 0) { - this.kingPos[c][0] = move.appear[0].x; - this.kingPos[c][1] = move.appear[0].y; + this.kingPos[c] = [move.appear[0].x, move.appear[0].y]; this.castleFlags[c] = [V.size.y, V.size.y]; return; } const oppCol = V.GetOppCol(c); + if (move.vanish.length == 2 && move.vanish[1].p == V.KING) + // Opponent's king is captured, game over + this.kingPos[oppCol] = [-1, -1]; const oppFirstRank = V.size.x - 1 - firstRank; if ( move.start.x == firstRank && //our rook moves? @@ -152,7 +137,8 @@ export class Doublemove2Rules extends ChessRules { ) { const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1); this.castleFlags[c][flagIdx] = V.size.y; - } else if ( + } + else if ( move.end.x == oppFirstRank && //we took opponent rook? this.castleFlags[oppCol].includes(move.end.y) ) { @@ -175,28 +161,47 @@ export class Doublemove2Rules extends ChessRules { this.turn = V.GetOppCol(this.turn); } if (this.movesCount > 0) this.subTurn = 3 - this.subTurn; - super.postUndo(move); + this.postUndo(move); } - static get VALUES() { - return { - p: 1, - r: 5, - n: 3, - b: 3, - q: 7, //slightly less than in orthodox game - k: 1000 - }; + postUndo(move) { + if (move.vanish.length == 2 && move.vanish[1].p == V.KING) + // Opponent's king was captured + this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y]; + super.postUndo(move); } - // No alpha-beta here, just adapted min-max at depth 1(+1) + // No alpha-beta here, just adapted min-max at depth 2(+1) getComputerMove() { + const maxeval = V.INFINITY; const color = this.turn; + const oppCol = V.GetOppCol(this.turn); + + // Search best (half) move for opponent turn + const getBestMoveEval = () => { + let score = this.getCurrentScore(); + if (score != "*") return maxeval * (score == "1-0" ? 1 : -1); + let moves = this.getAllValidMoves(); + let res = oppCol == "w" ? -maxeval : maxeval; + for (let m of moves) { + this.play(m); + score = this.getCurrentScore(); + if (score != "*") { + // King captured + this.undo(m); + return maxeval * (score == "1-0" ? 1 : -1); + } + const evalPos = this.evalPosition(); + res = oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos); + this.undo(m); + } + return res; + }; + const moves11 = this.getAllValidMoves(); if (this.movesCount == 0) // First white move at random: return moves11[randInt(moves11.length)]; - let doubleMoves = []; // Rank moves using a min-max at depth 2 for (let i = 0; i < moves11.length; i++) { @@ -206,7 +211,8 @@ export class Doublemove2Rules extends ChessRules { this.play(moves12[j]); doubleMoves.push({ moves: [moves11[i], moves12[j]], - eval: this.evalPosition() + // Small fluctuations to uniformize play a little + eval: getBestMoveEval() + 0.05 - Math.random() / 10 }); this.undo(moves12[j]); } diff --git a/client/src/variants/Monster.js b/client/src/variants/Monster.js index 1fb6a773..071c8db0 100644 --- a/client/src/variants/Monster.js +++ b/client/src/variants/Monster.js @@ -46,23 +46,24 @@ export class MonsterRules extends ChessRules { } 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; - } + // Goal is king capture => no checks return false; } + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + getCurrentScore() { + const color = this.turn; + if (this.kingPos[color][0] < 0) return (color == 'w' ? "0-1" : "1-0"); + return "*"; + } + play(move) { move.flags = JSON.stringify(this.aggregateFlags()); if (this.turn == 'b' || this.subTurn == 2) @@ -105,10 +106,11 @@ export class MonsterRules extends ChessRules { // 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) { - this.kingPos[c][0] = move.appear[0].x; - this.kingPos[c][1] = move.appear[0].y; - } + if (piece == V.KING) + this.kingPos[c] = [move.appear[0].x, move.appear[0].y]; + if (move.vanish.length == 2 && move.vanish[1].p == V.KING) + // Opponent's king is captured, game over + this.kingPos[move.vanish[1].c] = [-1, -1]; this.updateCastleFlags(move, piece); } @@ -120,52 +122,49 @@ export class MonsterRules extends ChessRules { if (this.subTurn == 2) this.subTurn = 1; else this.turn = 'b'; this.movesCount--; - } else { + } + 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; + postUndo(move) { + if (move.vanish.length == 2 && move.vanish[1].p == V.KING) + // Opponent's king was captured + this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y]; + super.postUndo(move); } + // Custom search at depth 1(+1) getComputerMove() { - const color = this.turn; - if (color == 'w') { + const getBestWhiteMove = (terminal) => { // Generate all sequences of 2-moves - const moves1 = this.getAllValidMoves(); + let 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; - } - }); + if (!!terminal) m1.eval = this.evalPosition(); + else { + const moves2 = this.getAllValidMoves(); + moves2.forEach(m2 => { + this.play(m2); + const eval2 = this.evalPosition() + 0.05 - Math.random() / 10; + this.undo(m2); + if (eval2 > m1.eval) { + m1.eval = eval2; + m1.move2 = m2; + } + }); + } this.undo(m1); }); moves1.sort((a, b) => b.eval - a.eval); + if (!!terminal) + // The move itself doesn't matter, only its eval: + return moves1[0]; let candidates = [0]; for ( let i = 1; @@ -178,8 +177,31 @@ export class MonsterRules extends ChessRules { 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(); + }; + + const getBestBlackMove = () => { + let moves = this.getAllValidMoves(); + moves.forEach(m => { + m.eval = V.INFINITY; + this.play(m); + const evalM = getBestWhiteMove("terminal").eval + this.undo(m); + if (evalM < m.eval) m.eval = evalM; + }); + moves.sort((a, b) => a.eval - b.eval); + let candidates = [0]; + for ( + let i = 1; + i < moves.length && moves[i].eval == moves[0].eval; + i++ + ) { + candidates.push(i); + } + const idx = candidates[randInt(candidates.length)]; + return moves[idx]; + }; + + const color = this.turn; + return (color == 'w' ? getBestWhiteMove() : getBestBlackMove()); } }; diff --git a/client/src/variants/Teleport.js b/client/src/variants/Teleport.js index 7704184f..6cf14a80 100644 --- a/client/src/variants/Teleport.js +++ b/client/src/variants/Teleport.js @@ -311,8 +311,6 @@ export class TeleportRules extends ChessRules { (color == 'w' && mvEval > m.eval) || (color == 'b' && mvEval < m.eval) ) { - // TODO: if many second moves have the same eval, only the - // first is kept. Could be randomized. m.eval = mvEval; m.next = m2; }