From 6ec2feb288c621ffd9a6bf9d3a461f9937b8c9d8 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Fri, 27 Mar 2020 01:56:00 +0100 Subject: [PATCH] Apocalypse chess is better but still slightly buggy --- .../public/images/pieces/Apocalypse/empty.svg | 1 + client/src/components/Board.vue | 46 ++--- .../src/translations/rules/Apocalypse/en.pug | 23 ++- client/src/variants/Apocalypse.js | 165 +++++++++++++----- 4 files changed, 159 insertions(+), 76 deletions(-) create mode 100644 client/public/images/pieces/Apocalypse/empty.svg diff --git a/client/public/images/pieces/Apocalypse/empty.svg b/client/public/images/pieces/Apocalypse/empty.svg new file mode 100644 index 00000000..08ec9068 --- /dev/null +++ b/client/public/images/pieces/Apocalypse/empty.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/> \ No newline at end of file diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index 0856096a..5f888805 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -87,7 +87,7 @@ export default { const gameDiv = h( "div", { - class: { + "class": { game: true, clearer: true } @@ -97,7 +97,7 @@ export default { return h( "div", { - class: { + "class": { row: true }, style: { opacity: this.choices.length > 0 ? "0.5" : "1" } @@ -108,7 +108,7 @@ export default { if (showPiece(ci, cj)) { elems.push( h("img", { - class: { + "class": { piece: true, ghost: !!this.selectedPiece && @@ -131,7 +131,7 @@ export default { if (this.settings.hints && hintSquares[ci][cj]) { elems.push( h("img", { - class: { + "class": { "mark-square": true }, attrs: { @@ -144,7 +144,7 @@ export default { return h( "div", { - class: { + "class": { board: true, ["board" + sizeY]: true, "light-square": lightSquare, @@ -176,13 +176,13 @@ export default { h( "div", { - class: { board: true, ["board" + sizeY]: true }, + "class": { board: true, ["board" + sizeY]: true }, attrs: { id: getSquareId({ x: sizeX + shiftIdx, y: i }) }, style: { opacity: qty > 0 ? 1 : 0.35 } }, [ h("img", { - class: { piece: true, reserve: true }, + "class": { piece: true, reserve: true }, attrs: { src: "/images/pieces/" + @@ -190,7 +190,7 @@ export default { ".svg" } }), - h("sup", { class: { "reserve-count": true } }, [ qty ]) + h("sup", { "class": { "reserve-count": true } }, [ qty ]) ] ) ); @@ -203,13 +203,13 @@ export default { h( "div", { - class: { board: true, ["board" + sizeY]: true }, + "class": { board: true, ["board" + sizeY]: true }, attrs: { id: getSquareId({ x: sizeX + (1 - shiftIdx), y: i }) }, style: { opacity: qty > 0 ? 1 : 0.35 } }, [ h("img", { - class: { piece: true, reserve: true }, + "class": { piece: true, reserve: true }, attrs: { src: "/images/pieces/" + @@ -217,7 +217,7 @@ export default { ".svg" } }), - h("sup", { class: { "reserve-count": true } }, [ qty ]) + h("sup", { "class": { "reserve-count": true } }, [ qty ]) ] ) ); @@ -233,7 +233,7 @@ export default { h( "div", { - class: { + "class": { game: true, "reserve-div": true }, @@ -245,7 +245,7 @@ export default { h( "div", { - class: { + "class": { row: true, "reserve-row": true } @@ -258,7 +258,7 @@ export default { h( "div", { - class: { + "class": { game: true, "reserve-div": true }, @@ -270,7 +270,7 @@ export default { h( "div", { - class: { + "class": { row: true, "reserve-row": true } @@ -300,7 +300,7 @@ export default { "div", { attrs: { id: "choices" }, - class: { row: true }, + "class": { row: true }, style: { top: topOffset + "px", left: @@ -313,7 +313,9 @@ export default { }, [ h( "div", - { }, + { + "class": { "full-width": true } + }, this.choices.map(m => { // A "choice" is a move const applyMove = (e) => { @@ -331,7 +333,7 @@ export default { return h( "div", { - class: { + "class": { board: true, ["board" + sizeY]: true }, @@ -349,7 +351,7 @@ export default { this.vr.getPPpath(m, this.orientation) + V.IMAGE_EXTENSION }, - class: { "choice-piece": true }, + "class": { "choice-piece": true }, on: onClick }) ] @@ -488,16 +490,16 @@ export default { </script> <style lang="sass" scoped> +// NOTE: no variants with reserve of size != 8 .game.reserve-div margin-bottom: 18px - .reserve-count padding-left: 40% - .reserve-row margin-bottom: 15px -// NOTE: no variants with reserve of size != 8 +.full-width + width: 100% .game user-select: none diff --git a/client/src/translations/rules/Apocalypse/en.pug b/client/src/translations/rules/Apocalypse/en.pug index 3a0ef740..eaeab80f 100644 --- a/client/src/translations/rules/Apocalypse/en.pug +++ b/client/src/translations/rules/Apocalypse/en.pug @@ -2,11 +2,16 @@ p.boxed | Both players play a move "at the same time". | The goal is to eliminate all pawns. +figure.diagram-container + .diagram + | fen:npppn/p3p/5/P3P/NPPPN: + figcaption Initial position. + 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. + | 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. @@ -24,13 +29,14 @@ 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. + li. + If both moves arrive on the same square: the illegal move prevails, + if the other was legal (higher risk => reward). + If both moves are legal or illegal, then a horseman wins over a footman. + Finally, at same risk level and same piece type, both disappear. figure.diagram-container .diagram @@ -43,6 +49,8 @@ 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. + Even in this last case, pawn promotions may appear possible by + anticipation of a knight capture. This is risky but playable. h3 End of the game @@ -50,7 +58,8 @@ 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. + It can happen if the two last pawns decide to advance to the same square + for example. h3 Source diff --git a/client/src/variants/Apocalypse.js b/client/src/variants/Apocalypse.js index 96431c35..b84fb5ba 100644 --- a/client/src/variants/Apocalypse.js +++ b/client/src/variants/Apocalypse.js @@ -22,13 +22,19 @@ export class ApocalypseRules extends ChessRules { } static get CanAnalyze() { - return false; + return true; //false; } static get ShowMoves() { return "byrow"; } + getPPpath(m) { + // Show the piece taken, if any, and not multiple pawns: + if (m.vanish.length == 1) return "Apocalypse/empty"; + return m.vanish[1].c + m.vanish[1].p; + } + static get PIECES() { return [V.PAWN, V.KNIGHT]; } @@ -112,7 +118,9 @@ export class ApocalypseRules extends ChessRules { } getFlagsFen() { - return this.penaltyFlags.join(""); + return ( + this.penaltyFlags['w'].toString() + this.penaltyFlags['b'].toString() + ); } setOtherVariables(fen) { @@ -126,7 +134,10 @@ export class ApocalypseRules extends ChessRules { } setFlags(fenflags) { - this.penaltyFlags = [0, 1].map(i => parseInt(fenflags[i])); + this.penaltyFlags = { + 'w': parseInt(fenflags[0]), + 'b': parseInt(fenflags[1]) + }; } getWhitemoveFen() { @@ -149,13 +160,17 @@ export class ApocalypseRules extends ChessRules { this.turn = V.GetOppCol(color); const oppMoves = super.getAllValidMoves(); this.turn = color; - // For each opponent's move, generate valid moves [from sq] + // For each opponent's move, generate valid moves [from sq if same color] let speculations = []; oppMoves.forEach(m => { V.PlayOnBoard(this.board, m); const newValidMoves = !!sq - ? super.getPotentialMovesFrom(sq) + ? ( + this.getColor(sq[0], sq[1]) == color + ? super.getPotentialMovesFrom(sq) + : [] + ) : super.getAllValidMoves(); newValidMoves.forEach(vm => { const mHash = "m" + vm.start.x + vm.start.y + vm.end.x + vm.end.y; @@ -190,8 +205,8 @@ export class ApocalypseRules extends ChessRules { // If 0 or 1 horsemen, promote in knight let knightCounter = 0; let emptySquares = []; - for (let i=0; i<V.size.x; i++) { - for (let j=0; j<V.size.y; j++) { + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { if (this.board[i][j] == V.EMPTY) emptySquares.push([i, j]); else if ( this.getColor(i, j) == color && @@ -203,10 +218,22 @@ export class ApocalypseRules extends ChessRules { } if (knightCounter <= 1) finalPieces = [V.KNIGHT]; else { - // Generate all possible landings + // Generate all possible landings, maybe capturing something on the way + let capture = undefined; + if (this.board[x2][y2] != V.EMPTY) { + capture = JSON.parse(JSON.stringify({ + x: x2, + y: y2, + c: this.getColor(x2, y2), + p: this.getPiece(x2, y2) + })); + } emptySquares.forEach(sq => { - if (sq[0] != lastRank) - moves.push(this.getBasicMove([x1, y1], [sq[0], sq[1]])); + if (sq[0] != lastRank) { + let newMove = this.getBasicMove([x1, y1], [sq[0], sq[1]]); + if (!!capture) newMove.vanish.push(capture); + moves.push(newMove); + } }); return; } @@ -233,41 +260,66 @@ export class ApocalypseRules extends ChessRules { // 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; - } - } + let m1 = this.whiteMove; + let m2 = move; + const movingLikeCapture = (m) => { + const shift = (m.vanish[0].c == 'w' ? -1 : 1); + return ( + m.start.x + shift == m.end.x && + Math.abs(m.end.y - m.start.y) == 1 + ); + }; + const isPossible = (m, other) => { + return ( + ( + m.vanish[0].p == V.KNIGHT && + (m.vanish.length == 1 || m.vanish[1].c != m.vanish[0].c) + ) + || + ( + // Promotion attempt + m.end.x == (m.vanish[0].c == "w" ? 0 : V.size.x - 1) && + other.vanish.length == 2 && + other.vanish[1].p == V.KNIGHT && + other.vanish[1].c == m.vanish[0].c + ) + || + ( + // Moving attempt + !movingLikeCapture(m) && + other.start.x == m.end.x && + other.start.y == m.end.y + ) + || + ( + // Capture attempt + movingLikeCapture(m) && + other.end.x == m.end.x && + other.end.y == m.end.y + ) + ); + }; + if (!!m1.illegal && !isPossible(m1, m2)) { + // Either an anticipated capture of something which didn't move + // (or not to the right square), or a push through blocus. + // ==> Just discard the move, and add a penalty point + this.penaltyFlags[m1.vanish[0].c]++; + m1.isNull = true; } - + if (!!m2.illegal && !isPossible(m2, m1)) { + this.penaltyFlags[m2.vanish[0].c]++; + m2.isNull = true; + } + if (!!m1.isNull) m1 = null; + if (!!m2.isNull) m2 = null; + // If one move is illegal, just execute the other + if (!m1 && !!m2) return m2; + if (!m2 && !!m1) return m1; // 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]); @@ -296,15 +348,27 @@ export class ApocalypseRules extends ChessRules { smove.vanish.push(m2.vanish[1]); } } else { - // Collision: both disappear except if different kinds (knight remains) + // Collision: priority to the anticipated capture, if any. + // If ex-aequo: knight wins (higher risk), or both disappears. + // Then, priority to the knight vs pawn: remains. + // Finally: both disappears. + let remain = null; const p1 = m1.vanish[0].p; const p2 = m2.vanish[0].p; - if ([p1, p2].includes(V.KNIGHT) && [p1, p2].includes(V.PAWN)) { + if (!!m1.illegal && !m2.illegal) remain = { c: 'w', p: p1 }; + else if (!!m2.illegal && !m1.illegal) remain = { c: 'b', p: p2 }; + if (!remain) { + // Either both are illegal or both are legal + if (p1 == V.KNIGHT && p2 == V.PAWN) remain = { c: 'w', p: p1 }; + else if (p2 == V.KNIGHT && p1 == V.PAWN) remain = { c: 'b', p: p2 }; + // If remain is still null: same type same risk, both disappear + } + if (!!remain) { smove.appear.push({ x: m1.end.x, y: m1.end.y, - p: V.KNIGHT, - c: (p1 == V.KNIGHT ? 'w' : 'b') + p: remain.p, + c: remain.c }); } } @@ -312,6 +376,10 @@ export class ApocalypseRules extends ChessRules { } play(move) { + if (!this.states) this.states = []; + const stateFen = this.getFen(); + this.states.push(stateFen); + // Do not play on board (would reveal the move...) move.flags = JSON.stringify(this.aggregateFlags()); this.turn = V.GetOppCol(this.turn); @@ -325,7 +393,6 @@ export class ApocalypseRules extends ChessRules { this.whiteMove = move; return; } - // A full turn just ended: const smove = this.resolveSynchroneMove(move); V.PlayOnBoard(this.board, smove); @@ -342,6 +409,10 @@ export class ApocalypseRules extends ChessRules { this.turn = V.GetOppCol(this.turn); this.movesCount--; this.postUndo(move); + + const stateFen = this.getFen(); + if (stateFen != this.states[this.states.length-1]) debugger; + this.states.pop(); } postUndo(move) { @@ -373,9 +444,9 @@ export class ApocalypseRules extends ChessRules { 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 (Object.values(this.penaltyFlags).every(v => v == 2)) return "1/2"; + if (this.penaltyFlags['w'] == 2) return "0-1"; + if (this.penaltyFlags['b'] == 2) return "1-0"; if (!this.atLeastOneMove('w') || !this.atLeastOneMove('b')) // Stalemate (should be very rare) return "1/2"; -- 2.44.0