From 1a3cfdc05b40c8ecc79397be02529b35411f073f Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 26 Mar 2020 22:47:15 +0100 Subject: [PATCH] More efficient Synchrone chess + fix a bug. FIrst draft of Apocalypse --- client/src/App.vue | 4 + .../src/translations/rules/Apocalypse/en.pug | 63 +++ .../src/translations/rules/Apocalypse/es.pug | 2 + .../src/translations/rules/Apocalypse/fr.pug | 2 + client/src/variants/Apocalypse.js | 421 ++++++++++++++++++ client/src/variants/Synchrone.js | 30 +- client/src/views/Game.vue | 2 +- 7 files changed, 510 insertions(+), 14 deletions(-) create mode 100644 client/src/translations/rules/Apocalypse/en.pug create mode 100644 client/src/translations/rules/Apocalypse/es.pug create mode 100644 client/src/translations/rules/Apocalypse/fr.pug create mode 100644 client/src/variants/Apocalypse.js diff --git a/client/src/App.vue b/client/src/App.vue index 77059bba..caf91a96 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -301,6 +301,10 @@ div.board display: inline-block position: relative +div.board5 + width: 20% + padding-bottom: 20% + div.board8 width: 12.5% padding-bottom: 12.5% diff --git a/client/src/translations/rules/Apocalypse/en.pug b/client/src/translations/rules/Apocalypse/en.pug new file mode 100644 index 00000000..3a0ef740 --- /dev/null +++ b/client/src/translations/rules/Apocalypse/en.pug @@ -0,0 +1,63 @@ +p.boxed + | Both players play a move "at the same time". + | The goal is to eliminate all pawns. + +p + | This variant is inspired by the + a(href="https://en.wikipedia.org/wiki/Four_Horsemen_of_the_Apocalypse") + |  Four Horsemen of the Apocalypse + | mythology. Knights are horsemen, and pawns are footmen. + | The goal is to eliminate all enemy footmen, + | most likely with the help of your horsemen. + | If all footmen die, the other side wins. + +p. + At each turn you can decide either to play safely an apparently valid move, + or speculate on your opponent's move and choose a move valid only + conditionally on his choice. In this last case the move may end up not + being playable: you would get a penalty point. Two penalty points loses + the game. For example in the initial position, 1.(c1)c2 is safe while 1.axb3 + will be valid only if black plays 1...Nb3. + +p Resolving rules: +ul + li. + If both moves are illegal none are played. + If one is illegal, the other is played. + li. + If both moves arrive on the same square, both pieces disappear except + if one is a horseman and the other a footman. + In this case only the horseman remains. + li. + If a capture was intended but the target moved, the move is still played + but doesn't capture anything. + +figure.diagram-container + .diagram + | fen:npppn/p4/4P/P2pP/NPP1N: + figcaption After 1.d1d2 e4e3 2.dxe3 exd2, pawns placements are inversed. + +h3 Pawn promotions + +p. + Pawns automatically promote in a knight, except if the player already + have two horsemen on the board. In this case the footman is relocated on + any free square which is not on last rank. + +h3 End of the game + +p. + As stated previously, losing all pawns lose the game, so promoting your + last pawn loses. It may be the only legal move. + If however both footmen armies vanish at the same time, it's a draw. + It can happen if the two last pawns decide to advance to the same square. + +h3 Source + +p + a(href="https://www.chessvariants.com/rules/apocalypse") Apocalypse chess + |  on chessvariants.com. This variant is playable at + a(href="http://apocalypsechess.online/") apocalypsechess.online + |  but without the promotion restriction. + +p Inventor: C.S. Elliott (1976) diff --git a/client/src/translations/rules/Apocalypse/es.pug b/client/src/translations/rules/Apocalypse/es.pug new file mode 100644 index 00000000..3a33838b --- /dev/null +++ b/client/src/translations/rules/Apocalypse/es.pug @@ -0,0 +1,2 @@ +p.boxed + | TODO diff --git a/client/src/translations/rules/Apocalypse/fr.pug b/client/src/translations/rules/Apocalypse/fr.pug new file mode 100644 index 00000000..3a33838b --- /dev/null +++ b/client/src/translations/rules/Apocalypse/fr.pug @@ -0,0 +1,2 @@ +p.boxed + | TODO diff --git a/client/src/variants/Apocalypse.js b/client/src/variants/Apocalypse.js new file mode 100644 index 00000000..96431c35 --- /dev/null +++ b/client/src/variants/Apocalypse.js @@ -0,0 +1,421 @@ +import { ChessRules } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class ApocalypseRules extends ChessRules { + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { + twoSquares: false, + promotions: [V.KNIGHT] + } + ); + } + + static get HasCastle() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get CanAnalyze() { + return false; + } + + static get ShowMoves() { + return "byrow"; + } + + static get PIECES() { + return [V.PAWN, V.KNIGHT]; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + // At least one pawn per color + let pawns = { "p": 0, "P": 0 }; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (['P','p'].includes(row[i])) pawns[row[i]]++; + if (V.PIECES.includes(row[i].toLowerCase())) sumElts++; + else { + const num = parseInt(row[i]); + if (isNaN(num)) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + if (Object.values(pawns).some(v => v == 0)) + return false; + return true; + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 4) Check whiteMove + if ( + ( + fenParsed.turn == "w" && + // NOTE: do not check really JSON stringified move... + (!fenParsed.whiteMove || fenParsed.whiteMove == "-") + ) + || + (fenParsed.turn == "b" && fenParsed.whiteMove != "-") + ) { + return false; + } + return true; + } + + static IsGoodFlags(flags) { + return !!flags.match(/^[0-2]{2,2}$/); + } + + aggregateFlags() { + return this.penaltyFlags; + } + + disaggregateFlags(flags) { + this.penaltyFlags = flags; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { whiteMove: fenParts[4] } + ); + } + + static get size() { + return { x: 5, y: 5 }; + } + + static GenRandInitFen() { + return "npppn/p3p/5/P3P/NPPPN w 0 00 -"; + } + + getFen() { + return super.getFen() + " " + this.getWhitemoveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getWhitemoveFen(); + } + + getFlagsFen() { + return this.penaltyFlags.join(""); + } + + setOtherVariables(fen) { + const parsedFen = V.ParseFen(fen); + this.setFlags(parsedFen.flags); + // Also init whiteMove + this.whiteMove = + parsedFen.whiteMove != "-" + ? JSON.parse(parsedFen.whiteMove) + : null; + } + + setFlags(fenflags) { + this.penaltyFlags = [0, 1].map(i => parseInt(fenflags[i])); + } + + getWhitemoveFen() { + if (!this.whiteMove) return "-"; + return JSON.stringify({ + start: this.whiteMove.start, + end: this.whiteMove.end, + appear: this.whiteMove.appear, + vanish: this.whiteMove.vanish + }); + } + + getSpeculations(moves, sq) { + let moveSet = {}; + moves.forEach(m => { + const mHash = "m" + m.start.x + m.start.y + m.end.x + m.end.y; + moveSet[mHash] = true; + }); + const color = this.turn; + this.turn = V.GetOppCol(color); + const oppMoves = super.getAllValidMoves(); + this.turn = color; + // For each opponent's move, generate valid moves [from sq] + let speculations = []; + oppMoves.forEach(m => { + V.PlayOnBoard(this.board, m); + const newValidMoves = + !!sq + ? super.getPotentialMovesFrom(sq) + : super.getAllValidMoves(); + newValidMoves.forEach(vm => { + const mHash = "m" + vm.start.x + vm.start.y + vm.end.x + vm.end.y; + if (!moveSet[mHash]) { + moveSet[mHash] = true; + vm.illegal = true; //potentially illegal! + speculations.push(vm); + } + }); + V.UndoOnBoard(this.board, m); + }); + return speculations; + } + + getPossibleMovesFrom([x, y]) { + const possibleMoves = super.getPotentialMovesFrom([x, y]) + // Augment potential moves with opponent's moves speculation: + return possibleMoves.concat(this.getSpeculations(possibleMoves, [x, y])); + } + + getAllValidMoves() { + // Return possible moves + potentially valid moves + const validMoves = super.getAllValidMoves(); + return validMoves.concat(this.getSpeculations(validMoves)); + } + + addPawnMoves([x1, y1], [x2, y2], moves) { + let finalPieces = [V.PAWN]; + const color = this.turn; + const lastRank = (color == "w" ? 0 : V.size.x - 1); + if (x2 == lastRank) { + // If 0 or 1 horsemen, promote in knight + let knightCounter = 0; + let emptySquares = []; + for (let i=0; i { + if (sq[0] != lastRank) + moves.push(this.getBasicMove([x1, y1], [sq[0], sq[1]])); + }); + return; + } + } + let tr = null; + for (let piece of finalPieces) { + tr = (piece != V.PAWN ? { c: color, p: piece } : null); + moves.push(this.getBasicMove([x1, y1], [x2, y2], tr)); + } + } + + filterValid(moves) { + // No checks: + return moves; + } + + atLeastOneMove(color) { + const curTurn = this.turn; + this.turn = color; + const res = super.atLeastOneMove(); + this.turn = curTurn; + return res; + } + + // White and black (partial) moves were played: merge + resolveSynchroneMove(move) { + let m = [this.whiteMove, move]; + for (let i of [0, 1]) { + if (!!m[i].illegal) { + // Either an anticipated capture of something which didn't move + // (or not to the right square), or a push through blocus. + if ( + ( + // Push attempt + m[i].start.y == m[i].end.y && + (m[1-i].start.x != m[i].end.x || m[1-i].start.y != m[i].end.y) + ) + || + ( + // Capture attempt + Math.abs(m[i].start.y - m[i].end.y) == 1 && + (m[1-i].end.x != m[i].end.x || m[1-i].end.y != m[i].end.y) + ) + ) { + // Just discard the move, and add a penalty point + this.penaltyFlags[m[i].vanish[0].c]++; + m[i] = null; + } + } + } + + // For PlayOnBoard (no need for start / end, irrelevant) + let smove = { + appear: [], + vanish: [] + }; + const m1 = m[0], + m2 = m[1]; + // If one move is illegal, just execute the other + if (!m1 && !!m2) return m2; + if (!m2 && !!m1) return m1; + if (!m1 && !m2) return smove; + // Both move are now legal: + smove.vanish.push(m1.vanish[0]); + smove.vanish.push(m2.vanish[0]); + if ((m1.end.x != m2.end.x) || (m1.end.y != m2.end.y)) { + // Easy case: two independant moves + smove.appear.push(m1.appear[0]); + smove.appear.push(m2.appear[0]); + // "Captured" pieces may have moved: + if ( + m1.vanish.length == 2 && + ( + m1.vanish[1].x != m2.start.x || + m1.vanish[1].y != m2.start.y + ) + ) { + smove.vanish.push(m1.vanish[1]); + } + if ( + m2.vanish.length == 2 && + ( + m2.vanish[1].x != m1.start.x || + m2.vanish[1].y != m1.start.y + ) + ) { + smove.vanish.push(m2.vanish[1]); + } + } else { + // Collision: both disappear except if different kinds (knight remains) + const p1 = m1.vanish[0].p; + const p2 = m2.vanish[0].p; + if ([p1, p2].includes(V.KNIGHT) && [p1, p2].includes(V.PAWN)) { + smove.appear.push({ + x: m1.end.x, + y: m1.end.y, + p: V.KNIGHT, + c: (p1 == V.KNIGHT ? 'w' : 'b') + }); + } + } + return smove; + } + + play(move) { + // Do not play on board (would reveal the move...) + move.flags = JSON.stringify(this.aggregateFlags()); + this.turn = V.GetOppCol(this.turn); + this.movesCount++; + this.postPlay(move); + } + + postPlay(move) { + if (this.turn == 'b') { + // NOTE: whiteMove is used read-only, so no need to copy + this.whiteMove = move; + return; + } + + // A full turn just ended: + const smove = this.resolveSynchroneMove(move); + V.PlayOnBoard(this.board, smove); + move.whiteMove = this.whiteMove; //for undo + this.whiteMove = null; + move.smove = smove; + } + + undo(move) { + this.disaggregateFlags(JSON.parse(move.flags)); + if (this.turn == 'w') + // Back to the middle of the move + V.UndoOnBoard(this.board, move.smove); + this.turn = V.GetOppCol(this.turn); + this.movesCount--; + this.postUndo(move); + } + + postUndo(move) { + if (this.turn == 'w') this.whiteMove = null; + else this.whiteMove = move.whiteMove; + } + + getCheckSquares(color) { + return []; + } + + getCurrentScore() { + if (this.turn == 'b') + // Turn (white + black) not over yet + return "*"; + // Count footmen: if a side has none, it loses + let fmCount = { 'w': 0, 'b': 0 }; + for (let i=0; i<5; i++) { + for (let j=0; j<5; j++) { + if (this.board[i][j] != V.EMPTY && this.getPiece(i, j) == V.PAWN) + fmCount[this.getColor(i, j)]++; + } + } + if (Object.values(fmCount).some(v => v == 0)) { + if (fmCount['w'] == 0 && fmCount['b'] == 0) + // Everyone died + return "1/2"; + if (fmCount['w'] == 0) return "0-1"; + return "1-0"; //fmCount['b'] == 0 + } + // Check penaltyFlags: if a side has 2 or more, it loses + if (this.penaltyFlags.every(f => f == 2)) return "1/2"; + if (this.penaltyFlags[0] == 2) return "0-1"; + if (this.penaltyFlags[1] == 2) return "1-0"; + if (!this.atLeastOneMove('w') || !this.atLeastOneMove('b')) + // Stalemate (should be very rare) + return "1/2"; + return "*"; + } + + getComputerMove() { + const maxeval = V.INFINITY; + const color = this.turn; + let moves = this.getAllValidMoves(); + if (moves.length == 0) + // TODO: this situation should not happen + return null; + + if (Math.random() < 0.5) + // Return a random move + return moves[randInt(moves.length)]; + + // Rank moves at depth 1: + // try to capture something (not re-capturing) + moves.forEach(m => { + V.PlayOnBoard(this.board, m); + m.eval = this.evalPosition(); + V.UndoOnBoard(this.board, m); + }); + moves.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++) + candidates.push(i); + return moves[candidates[randInt(candidates.length)]]; + } + + getNotation(move) { + // Basic system: piece + init + dest square + return ( + move.vanish[0].p.toUpperCase() + + V.CoordsToSquare(move.start) + + V.CoordsToSquare(move.end) + ); + } +}; diff --git a/client/src/variants/Synchrone.js b/client/src/variants/Synchrone.js index cee27c75..55d49dae 100644 --- a/client/src/variants/Synchrone.js +++ b/client/src/variants/Synchrone.js @@ -106,12 +106,12 @@ export class SynchroneRules extends ChessRules { }); } - // NOTE: lazy unefficient implementation (for now. TODO?) getPossibleMovesFrom([x, y]) { - const moves = this.getAllValidMoves(); - return moves.filter(m => { - return m.start.x == x && m.start.y == y; - }); + return ( + this.filterValid(super.getPotentialMovesFrom([x, y])) + // Augment with potential recaptures: + .concat(this.getRecaptures()) + ); } // Aux function used to find opponent and self captures @@ -191,12 +191,10 @@ export class SynchroneRules extends ChessRules { return this.filterValid(moves); } - getAllValidMoves() { - const color = this.turn; - // 0) Generate our possible moves - let myMoves = super.getAllValidMoves(); + getRecaptures() { // 1) Generate all opponent's capturing moves let oppCaptureMoves = []; + const color = this.turn; const oppCol = V.GetOppCol(color); for (let i=0; i<8; i++) { for (let j=0; j<8; j++) { @@ -215,6 +213,7 @@ export class SynchroneRules extends ChessRules { // 2) Play each opponent's capture, and see if back-captures are possible: // Lookup table to quickly decide if a move is already in list: let moveSet = {}; + let moves = []; oppCaptureMoves.forEach(m => { // If another opponent capture with same endpoint already processed, skip: const mHash = "m" + m.end.x + m.end.y; @@ -227,11 +226,16 @@ export class SynchroneRules extends ChessRules { }; V.PlayOnBoard(this.board, justDisappear); // Can I take on [m.end.x, m.end.y] ? If yes, add to list: - this.getCaptures(m.end.x, m.end.y, color).forEach(cm => myMoves.push(cm)); + this.getCaptures(m.end.x, m.end.y, color).forEach(cm => moves.push(cm)); V.UndoOnBoard(this.board, justDisappear); } }); - return myMoves; + return moves; + } + + getAllValidMoves() { + // Return possible moves + potential recaptures + return super.getAllValidMoves().concat(this.getRecaptures()); } filterValid(moves) { @@ -426,9 +430,9 @@ export class SynchroneRules extends ChessRules { this.undo(lastMove); //will erase whiteMove, thus saved above } let res = []; - if (this.underCheck('w')) + if (this.kingPos['w'][0] >= 0 && this.underCheck('w')) res.push(JSON.parse(JSON.stringify(this.kingPos['w']))); - if (this.underCheck('b')) + if (this.kingPos['b'][0] >= 0 && this.underCheck('b')) res.push(JSON.parse(JSON.stringify(this.kingPos['b']))); if (color == 'b') this.play(lastMove); return res; diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 6d37d1ed..ec4f2c98 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -155,7 +155,7 @@ export default { gameRef: "", nextIds: [], game: {}, //passed to BaseGame - focus: false, + focus: !document.hidden, //will not always work... TODO // virtualClocks will be initialized from true game.clocks virtualClocks: [], vr: null, //"variant rules" object initialized from FEN -- 2.44.0