From 98d144512e5505f5ef8b702b139ca6ceff92c823 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Sun, 3 Jul 2022 20:41:51 +0200 Subject: [PATCH] First working draft of Apocalypse --- base_rules.js | 71 +++++--- variants.js | 2 +- variants/Apocalypse/class.js | 289 +++++++++++++++++++++++++++------ variants/Apocalypse/rules.html | 1 + variants/Apocalypse/style.css | 12 ++ variants/Chakart/class.js | 7 +- 6 files changed, 305 insertions(+), 77 deletions(-) create mode 100644 variants/Apocalypse/rules.html create mode 100644 variants/Apocalypse/style.css diff --git a/base_rules.js b/base_rules.js index d9adc0d..08e9d1b 100644 --- a/base_rules.js +++ b/base_rules.js @@ -116,6 +116,11 @@ export default class ChessRules { return false; } + // Some variants reveal moves only after both players played + hideMoves() { + return false; + } + // Some variants use click infos: doClick(coords) { if (typeof coords.x != "number") @@ -1074,6 +1079,21 @@ export default class ChessRules { } } + displayMessage(elt, msg, classe_s, timeout) { + if (elt) + // Fixed element, e.g. for Dice Chess + elt.innerHTML = msg; + else { + // Temporary div (Chakart, Apocalypse...) + let divMsg = document.createElement("div"); + C.AddClass_es(divMsg, classe_s); + divMsg.innerHTML = msg; + let container = document.getElementById(this.containerId); + container.appendChild(divMsg); + setTimeout(() => container.removeChild(divMsg), timeout); + } + } + //////////////// // DARK METHODS @@ -1527,18 +1547,18 @@ export default class ChessRules { moves = this.capturePostProcess(moves, oppCol); if (this.options["atomic"]) - this.atomicPostProcess(moves, color, oppCol); + moves = this.atomicPostProcess(moves, color, oppCol); if ( moves.length > 0 && this.getPieceType(moves[0].start.x, moves[0].start.y) == "p" ) { - this.pawnPostProcess(moves, color, oppCol); + moves = this.pawnPostProcess(moves, color, oppCol); } if (this.options["cannibal"] && this.options["rifle"]) // In this case a rifle-capture from last rank may promote a pawn - this.riflePromotePostProcess(moves, color); + moves = this.riflePromotePostProcess(moves, color); return moves; } @@ -1610,6 +1630,7 @@ export default class ChessRules { m.next = mNext; } }); + return moves; } pawnPostProcess(moves, color, oppCol) { @@ -1649,7 +1670,7 @@ export default class ChessRules { moreMoves.push(newMove); } }); - Array.prototype.push.apply(moves, moreMoves); + return moves.concat(moreMoves); } riflePromotePostProcess(moves, color) { @@ -1671,7 +1692,7 @@ export default class ChessRules { } } }); - Array.prototype.push.apply(moves, newMoves); + return moves.concat(newMoves); } // Generic method to find possible moves of "sliding or jumping" pieces @@ -2458,7 +2479,7 @@ export default class ChessRules { this.computeNextMove(move); this.play(move); const newTurn = this.turn; - if (this.moveStack.length == 1) + if (this.moveStack.length == 1 && !this.hideMoves) this.playVisual(move, r); if (move.next) { this.gameState = { @@ -2610,31 +2631,37 @@ export default class ChessRules { return 0; //nb of targets } - playReceivedMove(moves, callback) { - const launchAnimation = () => { - const r = container.querySelector(".chessboard").getBoundingClientRect(); - const animateRec = i => { - this.animate(moves[i], () => { - this.play(moves[i]); - this.playVisual(moves[i], r); - if (i < moves.length - 1) - setTimeout(() => animateRec(i+1), 300); - else - callback(); - }); - }; - animateRec(0); + launchAnimation(moves, callback) { + if (this.hideMoves) { + moves.forEach(m => this.play(m)); + callback(); + return; + } + const r = container.querySelector(".chessboard").getBoundingClientRect(); + const animateRec = i => { + this.animate(moves[i], () => { + this.play(moves[i]); + this.playVisual(moves[i], r); + if (i < moves.length - 1) + setTimeout(() => animateRec(i+1), 300); + else + callback(); + }); }; + animateRec(0); + } + + playReceivedMove(moves, callback) { // Delay if user wasn't focused: const checkDisplayThenAnimate = (delay) => { if (container.style.display == "none") { alert("New move! Let's go back to game..."); document.getElementById("gameInfos").style.display = "none"; container.style.display = "block"; - setTimeout(launchAnimation, 700); + setTimeout(() => this.launchAnimation(moves, callback), 700); } else - setTimeout(launchAnimation, delay || 0); + setTimeout(() => this.launchAnimation(moves, callback), delay || 0); }; let container = document.getElementById(this.containerId); if (document.hidden) { diff --git a/variants.js b/variants.js index 4108165..d3831c6 100644 --- a/variants.js +++ b/variants.js @@ -8,7 +8,7 @@ const variants = [ {name: 'Antiking1', desc: 'Keep antiking in check', disp: 'Anti-King I'}, {name: 'Antiking2', desc: 'Keep antiking in check', disp: 'Anti-King II'}, {name: 'Antimatter', desc: 'Dangerous collisions'}, -// {name: 'Apocalypse', desc: 'The end of the world'}, + {name: 'Apocalypse', desc: 'The end of the world'}, // {name: 'Arena', desc: 'Middle battle'}, // {name: 'Atarigo', desc: 'First capture wins', disp: 'Atari-Go'}, {name: 'Atomic', desc: 'Explosive captures'}, diff --git a/variants/Apocalypse/class.js b/variants/Apocalypse/class.js index c0cd8ee..0600991 100644 --- a/variants/Apocalypse/class.js +++ b/variants/Apocalypse/class.js @@ -1,12 +1,22 @@ import ChessRules from "/base_rules.js"; import {ArrayFun} from "/utils/array.js"; -export class ApocalypseRules extends ChessRules { +export default class ApocalypseRules extends ChessRules { static get Options() { return {}; } + get hasFlags() { + return false; + } + get hasEnpassant() { + return false; + } + get hideMoves() { + return true; + } + get pawnPromotions() { return ['n', 'p']; } @@ -17,35 +27,37 @@ export class ApocalypseRules extends ChessRules { setOtherVariables(fenParsed) { super.setOtherVariables(fenParsed); + // Often a simple move, but sometimes an array (pawn relocation) this.whiteMove = fenParsed.whiteMove != "-" ? JSON.parse(fenParsed.whiteMove) - : null; + : []; + this.firstMove = null; //used if black turn pawn relocation + this.penalties = ArrayFun.toObject( + ['w', 'b'], + [0, 1].map(i => parseInt(fenParsed.penalties.charAt(i), 10)) + ); } genRandInitBaseFen() { return { fen: "npppn/p3p/5/P3P/NPPPN w 0", - o: {"flags":"00"} + o: {} }; } getPartFen(o) { - let parts = super.getPartFen(o); - parts["whiteMove"] = this.whiteMove || "-"; - return parts; - } - - getFlagsFen() { - return Object.values(this.penaltyFlags).join(""); - } - - setFlags(fenflags) { - this.penaltyFlags = ArrayFun.toObject( - ['w', 'b'], [0, 1].map(i => parseInt(fenflags.charAt(i), 10))); + return { + whiteMove: (o.init || !this.whiteMove) ? "-" : this.whiteMove, + penalties: o.init ? "00" : Object.values(this.penalties).join("") + }; } getWhitemoveFen() { - return !this.whiteMove ? "-" : JSON.stringify(this.whiteMove); + if (this.whiteMove.length == 0) + return "-"; + if (this.whiteMove.length == 1) + return JSON.stringify(this.whiteMove[0]); + return JSON.stringify(this.whiteMove); //pawn relocation } // Allow pawns to move diagonally and capture vertically, @@ -76,7 +88,7 @@ export class ApocalypseRules extends ChessRules { getPotentialMovesFrom([x, y]) { let moves = []; if (this.subTurn == 2) { - const start = this.moveStack[0].end; + const start = this.firstMove.end; if (x == start.x && y == start.y) { // Move the pawn to any empty square not on last rank (== x) for (let i=0; i { + // Remove pawn push toward own color (absurd) + return ( + m.vanish[0].p != 'p' || + m.end.y != m.start.y || + m.vanish.length == 1 || + m.vanish[1].c == oppCol + ); + }); // Flag a priori illegal moves moves.forEach(m => { if ( @@ -112,46 +133,218 @@ export class ApocalypseRules extends ChessRules { return moves; } + pawnPostProcess(moves, color, oppCol) { + let knightCount = 0; + for (let i=0; i { + if ( + m.vanish[0].p == 'p' && + ( + (color == 'w' && m.end.x == 0) || + (color == 'b' && m.end.x == this.size.x - 1) + ) + ) { + // Pawn promotion + if (knightCount <= 1 && m.appear[0].p == 'p') + return false; //knight promotion mandatory + if (knightCount == 2 && m.appear[0].p == 'n') + m.illegal = true; //will be legal only if one knight is captured + } + return true; + }); + } + filterValid(moves) { // No checks: return moves; } - -//TODO: from here - // White and black (partial) moves were played: merge - // + animate both at the same time ! resolveSynchroneMove(move) { - // TODO + const condensate = (mArr) => { + const illegal = (mArr.length == 1 && mArr[0].illegal) || + (!mArr[0] && mArr[1].illegal); + if (mArr.length == 1) + return Object.assign({illegal: illegal}, mArr[0]); + if (!mArr[0]) + return Object.assign({illegal: illegal}, mArr[1]); + // Pawn relocation + return { + start: mArr[0].start, + end: mArr[1].end, + vanish: mArr[0].vanish, + appear: mArr[1].appear, + segments: [ + [[mArr[0].start.x, mArr[0].start.y], [mArr[0].end.x, mArr[0].end.y]], + [[mArr[1].start.x, mArr[1].start.y], [mArr[1].end.x, mArr[1].end.y]] + ] + }; + }; + const compatible = (m1, m2) => { + if (m2.illegal) + return false; + // Knight promotion? + if (m1.appear[0].p != m1.vanish[0].p) + return m2.vanish.length == 2 && m2.vanish[1].p == 'n'; + if ( + // Self-capture attempt? + (m1.vanish.length == 2 && m1.vanish[1].c == m1.vanish[0].c) || + // Pawn captures something by anticipation? + ( + m1.vanish[0].p == 'p' && + m1.vanish.length == 1 && + m1.start.y != m1.end.y + ) + ) { + return m2.end.x == m1.end.x && m2.end.y == m1.end.y; + } + // Pawn push toward an enemy piece? + if ( + m1.vanish[0].p == 'p' && + m1.vanish.length == 2 && + m1.start.y == m1.end.y + ) { + return m2.start.x == m1.end.x && m2.start.y == m1.end.y; + } + return true; + }; + const adjust = (res) => { + if (!res.wm || !res.bm) + return; + for (let c of ['w', 'b']) { + const myMove = res[c + 'm'], oppMove = res[C.GetOppCol(c) + 'm']; + if ( + myMove.end.x == oppMove.start.x && + myMove.end.y == oppMove.start.y + ) { + // Whatever was supposed to vanish, finally doesn't vanish + myMove.vanish.pop(); + } + } + if (res.wm.end.y == res.bm.end.y && res.wm.end.x == res.bm.end.x) { + // Collision (necessarily on empty square) + if (!res.wm.illegal && !res.bm.illegal) { + if (res.wm.vanish[0].p != res.bm.vanish[0].p) { + const c = (res.wm.vanish[0].p == 'n' ? 'w' : 'b'); + res[c + 'm'].vanish.push(res[C.GetOppCol(c) + 'm'].appear.shift()); + } + else { + // Collision of two pieces of same nature: both disappear + res.wm.appear.shift(); + res.bm.appear.shift(); + } + } + else { + const c = (!res.wm.illegal ? 'w' : 'b'); + // Illegal move wins: + res[c + 'm'].appear.shift(); + } + } + }; + // Clone moves to avoid altering them: + let whiteMove = JSON.parse(JSON.stringify(this.whiteMove)), + blackMove = JSON.parse(JSON.stringify([this.firstMove, move])); + [whiteMove, blackMove] = [condensate(whiteMove), condensate(blackMove)]; + let res = { + wm: ( + (!whiteMove.illegal || compatible(whiteMove, blackMove)) + ? whiteMove + : null + ), + bm: ( + (!blackMove.illegal || compatible(blackMove, whiteMove)) + ? blackMove + : null + ) + }; + adjust(res); + return res; } play(move) { - if (this.subTurn...) //TODO: detect (mark?) if pawn move arriving on last rank (=> subTurn++) - this.turn = V.GetOppCol(this.turn); - this.movesCount++; - this.postPlay(move); - } - - postPlay(move) { - if (pawn promotion into pawn) { - this.curMove move; //TODO: animate both move at same time + effects AFTER ! - this.subTurn = 2 + const color = this.turn; + if (color == 'w') + this.whiteMove.push(move); + if ( + move.vanish[0].p == 'p' && move.appear[0].p == 'p' && + ( + (color == 'w' && move.end.x == 0) || + (color == 'b' && move.end.x == this.size.x - 1) + ) + ) { + // Pawn on last rank : will relocate + this.subTurn = 2; + this.firstMove = move; + if (color == this.playerColor) { + this.playOnBoard(move); + this.playVisual(move); + } + return; } - else if (this.turn == 'b') - // NOTE: whiteMove is used read-only, so no need to copy - this.whiteMove = move; + if (color == this.playerColor && this.firstMove) { + // The move was played on board: undo it + this.undoOnBoard(this.firstMove); + const revFirstMove = { + start: this.firstMove.end, + end: this.firstMove.start, + appear: this.firstMove.vanish, + vanish: this.firstMove.appear + }; + this.playVisual(revFirstMove); } - else { - // A full turn just ended: - const [wMove, bMove] = this.resolveSynchroneMove(move); - V.PlayOnBoard(this.board, smove); //----> ici : animate both ! - this.whiteMove = null; + this.turn = C.GetOppCol(color); + this.movesCount++; + this.subTurn = 1; + this.firstMove = null; + if (color == 'b') { + // A full turn just ended + const res = this.resolveSynchroneMove(move); + const callback = () => { + // start + end don't matter for playOnBoard() and playVisual(). + // Merging is necessary because moves may overlap. + let toPlay = {appear: [], vanish: []}; + for (let c of ['w', 'b']) { + if (res[c + 'm']) { + Array.prototype.push.apply(toPlay.vanish, res[c + 'm'].vanish); + Array.prototype.push.apply(toPlay.appear, res[c + 'm'].appear); + } + } + this.playOnBoard(toPlay); + this.playVisual(toPlay); + }; + if (res.wm) + this.animate(res.wm, () => {if (!res.bm) callback();}); + if (res.bm) + this.animate(res.bm, callback); + if (!res.wm && !res.bm) { + this.displayIllegalInfo("both illegal"); + ['w', 'b'].forEach(c => this.penalties[c]++); + } + else if (!res.wm) { + this.displayIllegalInfo("white illegal"); + this.penalties['w']++; + } + else if (!res.bm) { + this.displayIllegalInfo("black illegal"); + this.penalties['b']++; + } + this.whiteMove = []; } } -//until here - + displayIllegalInfo(msg) { + super.displayMessage(null, msg, "illegal-text", 2000); + } atLeastOneLegalMove(color) { for (let i=0; i !m.illegal) + this.getPotentialMovesFrom([i, j]).some(m => !m.illegal) ) { return true; } @@ -180,7 +373,7 @@ export class ApocalypseRules extends ChessRules { 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) + if (this.board[i][j] != "" && this.getPiece(i, j) == 'p') fmCount[this.getColor(i, j)]++; } } @@ -192,9 +385,9 @@ export class ApocalypseRules extends ChessRules { return "1-0"; //fmCount['b'] == 0 } // Check penaltyFlags: if a side has 2 or more, it loses - 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 (Object.values(this.penalties).every(v => v == 2)) return "1/2"; + if (this.penalties['w'] == 2) return "0-1"; + if (this.penalties['b'] == 2) return "1-0"; if (!this.atLeastOneLegalMove('w') || !this.atLeastOneLegalMove('b')) // Stalemate (should be very rare) return "1/2"; diff --git a/variants/Apocalypse/rules.html b/variants/Apocalypse/rules.html new file mode 100644 index 0000000..c65158e --- /dev/null +++ b/variants/Apocalypse/rules.html @@ -0,0 +1 @@ +

TODO

diff --git a/variants/Apocalypse/style.css b/variants/Apocalypse/style.css new file mode 100644 index 0000000..07cebbb --- /dev/null +++ b/variants/Apocalypse/style.css @@ -0,0 +1,12 @@ +@import url("/base_pieces.css"); + +div.illegal-text { + position: relative; + margin-top: 5%; + width: 100%; + text-align: center; + background-color: transparent; + color: darkred; + font-weight: bold; + font-size: 2em; +} diff --git a/variants/Chakart/class.js b/variants/Chakart/class.js index 916156e..d804548 100644 --- a/variants/Chakart/class.js +++ b/variants/Chakart/class.js @@ -699,12 +699,7 @@ export default class ChakartRules extends ChessRules { } displayBonus(move) { - let divBonus = document.createElement("div"); - divBonus.classList.add("bonus-text"); - divBonus.innerHTML = move.egg; - let container = document.getElementById(this.containerId); - container.appendChild(divBonus); - setTimeout(() => container.removeChild(divBonus), 2000); + super.displayMessage(null, move.egg, "bonus-text", 2000); } atLeastOneMove() { -- 2.44.0