From: Benjamin Auder Date: Sat, 25 Jun 2022 10:22:15 +0000 (+0200) Subject: Experimental: improve animation, reduce lags in stack moves sending. Add Allmate X-Git-Url: https://git.auder.net/doc/html/css/img/%24%7BgetWhatsApp%28link%29%7D?a=commitdiff_plain;h=5f08c59b29c2173cc8b2df1a3799ee971a14e691;p=xogo.git Experimental: improve animation, reduce lags in stack moves sending. Add Allmate --- diff --git a/app.js b/app.js index 4c0b5e4..6f17c64 100644 --- a/app.js +++ b/app.js @@ -456,9 +456,28 @@ function notifyMe(code) { let curMoves = [], lastFen; -const afterPlay = (move_s) => { - const callbackAfterSend = () => { - curMoves = []; +const afterPlay = (move_s, newTurn, ops) => { + if (ops.send) { + // Pack into one moves array, then send (if turn changed) + if (Array.isArray(move_s)) + // Array of simple moves (e.g. Chakart) + Array.prototype.push.apply(curMoves, move_s); + else + // Usual case + curMoves.push(move_s); + if (newTurn != playerColor) { + send("newmove", + {gid: gid, moves: curMoves, fen: vr.getFen()}, + { + retry: true, + success: () => curMoves = [], + error: () => alert("Move not sent: reload page") + } + ); + } + } + if (ops.res && newTurn != playerColor) { + toggleTurnIndicator(false); //now all moves are sent and animated const result = vr.getCurrentScore(move_s); if (result != "*") { setTimeout(() => { @@ -466,23 +485,6 @@ const afterPlay = (move_s) => { send("gameover", {gid: gid}); }, 2000); } - }; - // Pack into one moves array, then send - if (Array.isArray(move_s)) - // Array of simple moves (e.g. Chakart) - Array.prototype.push.apply(curMoves, move_s); - else - // Usual case - curMoves.push(move_s); - if (vr.turn != playerColor) { - toggleTurnIndicator(false); - send("newmove", - {gid: gid, moves: curMoves, fen: vr.getFen()}, - { - retry: true, - success: callbackAfterSend, - error: () => alert("Move not sent: reload page") - }); } }; @@ -532,8 +534,9 @@ function initializeGame(obj) { afterPlay: afterPlay, options: options }); - if (!obj.fen) { - // Game creation: both players set FEN, in case of one is offline + const gameCreation = !obj.fen; + if (gameCreation) { + // Both players set FEN, in case of one is offline send("setfen", {gid: obj.gid, fen: vr.getFen()}); localStorage.setItem("gid", obj.gid); } @@ -547,7 +550,7 @@ function initializeGame(obj) { } const playerIndex = (playerColor == "w" ? 0 : 1); fillGameInfos(obj, 1 - playerIndex); - if (obj.players[playerIndex].randvar) + if (obj.players[playerIndex].randvar && gameCreation) toggleVisible("gameInfos"); else toggleVisible("boardContainer"); diff --git a/base_rules.js b/base_rules.js index 3fac51b..6562673 100644 --- a/base_rules.js +++ b/base_rules.js @@ -3,6 +3,21 @@ import { ArrayFun } from "/utils/array.js"; import PiPo from "/utils/PiPo.js"; import Move from "/utils/Move.js"; +// Helper class for move animation +class TargetObj { + + constructor(callOnComplete) { + this.value = 0; + this.target = 0; + this.callOnComplete = callOnComplete; + } + increment() { + if (++this.value == this.target) + this.callOnComplete(); + } + +}; + // NOTE: x coords: top to bottom (white perspective); y: left to right // NOTE: ChessRules is aliased as window.C, and variants as window.V export default class ChessRules { @@ -96,6 +111,10 @@ export default class ChessRules { return !!this.options["dark"]; } + get hasMoveStack() { + return false; + } + // Some variants use click infos: doClick(coords) { if (typeof coords.x != "number") @@ -391,17 +410,21 @@ export default class ChessRules { // Fen string fully describes the game state if (!o.fen) o.fen = this.genRandInitFen(o.seed); - const fenParsed = this.parseFen(o.fen); - this.board = this.getBoard(fenParsed.position); - this.turn = fenParsed.turn; - this.movesCount = parseInt(fenParsed.movesCount, 10); - this.setOtherVariables(fenParsed); + this.re_initFromFen(o.fen); // Graphical (can use variables defined above) this.containerId = o.element; this.graphicalInit(); } + re_initFromFen(fen, oldBoard) { + const fenParsed = this.parseFen(fen); + this.board = oldBoard || this.getBoard(fenParsed.position); + this.turn = fenParsed.turn; + this.movesCount = parseInt(fenParsed.movesCount, 10); + this.setOtherVariables(fenParsed); + } + // Turn position fen into double array ["wb","wp","bk",...] getBoard(position) { const rows = position.split("/"); @@ -433,7 +456,6 @@ export default class ChessRules { this.initReserves(fenParsed.reserve); if (this.options["crazyhouse"]) this.initIspawn(fenParsed.ispawn); - this.subTurn = 1; //may be unused if (this.options["teleport"]) { this.subTurnTeleport = 1; this.captured = null; @@ -443,6 +465,9 @@ export default class ChessRules { this.enlightened = ArrayFun.init(this.size.x, this.size.y, false); this.updateEnlightened(); } + this.subTurn = 1; //may be unused + if (!this.moveStack) //avoid resetting (unwanted) + this.moveStack = []; } updateEnlightened() { @@ -2128,35 +2153,38 @@ export default class ChessRules { this.subTurnTeleport = 1; this.captured = null; } - if (this.options["balance"]) { - if (![1, 3].includes(this.movesCount)) - this.turn = oppCol; - } - else { + if ( + ( + this.options["doublemove"] && + this.movesCount >= 1 && + this.subTurn == 1 + ) || + (this.options["progressive"] && this.subTurn <= this.movesCount) + ) { + const oppKingPos = this.searchKingPos(oppCol); if ( + oppKingPos[0] >= 0 && ( - this.options["doublemove"] && - this.movesCount >= 1 && - this.subTurn == 1 - ) || - (this.options["progressive"] && this.subTurn <= this.movesCount) + this.options["taking"] || + !this.underCheck(oppKingPos, color) + ) ) { - const oppKingPos = this.searchKingPos(oppCol); - if ( - oppKingPos[0] >= 0 && - ( - this.options["taking"] || - !this.underCheck(oppKingPos, color) - ) - ) { - this.subTurn++; - return; - } + this.subTurn++; + return; } + } + if (this.isLastMove(move)) { this.turn = oppCol; + this.movesCount++; + this.subTurn = 1; } - this.movesCount++; - this.subTurn = 1; + } + + isLastMove(move) { + return ( + (this.options["balance"] && ![1, 3].includes(this.movesCount)) || + !move.next + ); } // "Stop at the first move found" @@ -2233,11 +2261,50 @@ export default class ChessRules { } playPlusVisual(move, r) { + if (this.hasMoveStack) + this.buildMoveStack(move); + else { + this.play(move); + this.playVisual(move, r); + this.afterPlay(move, this.turn, {send: true, res: true}); //user method + } + } + + // TODO: send stack receive stack, or allow incremental? (good/bad points) + buildMoveStack(move) { + this.moveStack.push(move); + this.computeNextMove(move); this.play(move); - this.playVisual(move, r); - this.afterPlay(move); //user method + const newTurn = this.turn; + if (this.moveStack.length == 1) { + this.playVisual(move); + this.gameState = { + fen: this.getFen(), + board: JSON.parse(JSON.stringify(this.board)) //easier + }; + } + if (move.next) + this.buildMoveStack(move.next); + else { + // Send, animate + play until here + if (this.moveStack.length == 1) { + this.afterPlay(this.moveStack, newTurn, {send: true, res: true}); + this.moveStack = [] + } + else { + this.afterPlay(this.moveStack, newTurn, {send: true, res: false}); + this.re_initFromFen(this.gameState.fen, this.gameState.board); + this.playReceivedMove(this.moveStack.slice(1), () => { + this.afterPlay(this.moveStack, newTurn, {send: false, res: true}); + this.moveStack = [] + }); + } + } } + // Implemented in variants using (automatic) moveStack + computeNextMove(move) {} + getMaxDistance(r) { // Works for all rectangular boards: return Math.sqrt(r.width ** 2 + r.height ** 2); @@ -2247,41 +2314,37 @@ export default class ChessRules { return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y]; } - animate(move, callback) { - if (this.noAnimate || move.noAnimate) { - callback(); - return; - } - let initPiece = this.getDomPiece(move.start.x, move.start.y); - // NOTE: cloning generally not required, but light enough, and simpler + animateMoving(start, end, drag, segments, cb) { + let initPiece = this.getDomPiece(start.x, start.y); + // NOTE: cloning often not required, but light enough, and simpler let movingPiece = initPiece.cloneNode(); initPiece.style.opacity = "0"; let container = document.getElementById(this.containerId) const r = container.querySelector(".chessboard").getBoundingClientRect(); - if (typeof move.start.x == "string") { + if (typeof start.x == "string") { // Need to bound width/height (was 100% for reserve pieces) const pieceWidth = this.getPieceWidth(r.width); movingPiece.style.width = pieceWidth + "px"; movingPiece.style.height = pieceWidth + "px"; } const maxDist = this.getMaxDistance(r); - const apparentColor = this.getColor(move.start.x, move.start.y); - const pieces = this.pieces(apparentColor, move.start.x, move.start.y); - if (move.drag) { - const startCode = this.getPiece(move.start.x, move.start.y); + const apparentColor = this.getColor(start.x, start.y); + const pieces = this.pieces(apparentColor, start.x, start.y); + if (drag) { + const startCode = this.getPiece(start.x, start.y); C.RemoveClass_es(movingPiece, pieces[startCode]["class"]); - C.AddClass_es(movingPiece, pieces[move.drag.p]["class"]); - if (apparentColor != move.drag.c) { + C.AddClass_es(movingPiece, pieces[drag.p]["class"]); + if (apparentColor != drag.c) { movingPiece.classList.remove(C.GetColorClass(apparentColor)); - movingPiece.classList.add(C.GetColorClass(move.drag.c)); + movingPiece.classList.add(C.GetColorClass(drag.c)); } } container.appendChild(movingPiece); const animateSegment = (index, cb) => { // NOTE: move.drag could be generalized per-segment (usage?) - const [i1, j1] = move.segments[index][0]; - const [i2, j2] = move.segments[index][1]; + const [i1, j1] = segments[index][0]; + const [i2, j2] = segments[index][1]; const dep = this.getPixelPosition(i1, j1, r); const arr = this.getPixelPosition(i2, j2, r); movingPiece.style.transitionDuration = "0s"; @@ -2297,24 +2360,63 @@ export default class ChessRules { setTimeout(cb, duration * 1000); }, 50); }; - if (!move.segments) { - move.segments = [ - [[move.start.x, move.start.y], [move.end.x, move.end.y]] - ]; - } let index = 0; const animateSegmentCallback = () => { - if (index < move.segments.length) + if (index < segments.length) animateSegment(index++, animateSegmentCallback); else { movingPiece.remove(); initPiece.style.opacity = "1"; - callback(); + cb(); } }; animateSegmentCallback(); } + // Input array of objects with at least fields x,y (e.g. PiPo) + animateFading(arr, cb) { + const animLength = 350; //TODO: 350ms? More? Less? + arr.forEach(v => { + let fadingPiece = this.getDomPiece(v.x, v.y); + fadingPiece.style.transitionDuration = (animLength / 1000) + "s"; + fadingPiece.style.opacity = "0"; + }); + setTimeout(cb, animLength); + } + + animate(move, callback) { + if (this.noAnimate || move.noAnimate) { + callback(); + return; + } + let segments = move.segments; + if (!segments) + segments = [ [[move.start.x, move.start.y], [move.end.x, move.end.y]] ]; + let targetObj = new TargetObj(callback); + if (move.start.x != move.end.x || move.start.y != move.end.y) { + targetObj.target++; + this.animateMoving(move.start, move.end, move.drag, segments, + () => targetObj.increment()); + } + if (move.vanish.length > move.appear.length) { + const arr = move.vanish.slice(move.appear.length) + .filter(v => v.x != move.end.x || v.y != move.end.y); + if (arr.length > 0) { + targetObj.target++; + this.animateFading(arr, () => targetObj.increment()); + } + } + targetObj.target += + this.customAnimate(move, segments, () => targetObj.increment()); + if (targetObj.target == 0) + callback(); + } + + // Potential other animations (e.g. for Suction variant) + customAnimate(move, segments, cb) { + return 0; //nb of targets + } + playReceivedMove(moves, callback) { const launchAnimation = () => { const r = container.querySelector(".chessboard").getBoundingClientRect(); diff --git a/variants.js b/variants.js index 5d04f45..18d5891 100644 --- a/variants.js +++ b/variants.js @@ -3,7 +3,7 @@ const variants = [ {name: 'Alapo', desc: 'Geometric Chess'}, {name: 'Alice', desc: 'Both sides of the mirror'}, {name: 'Align4', desc: 'Align four pawns'}, -// {name: 'Allmate', desc: 'Mate any piece'}, + {name: 'Allmate', desc: 'Mate any piece'}, {name: 'Ambiguous', desc: "Play opponent's pieces"}, // {name: 'Antiking1', desc: 'Keep antiking in check', disp: 'Anti-King'}, // {name: 'Antimatter', desc: 'Dangerous collisions'}, diff --git a/variants/Allmate/class.js b/variants/Allmate/class.js new file mode 100644 index 0000000..8c5a92c --- /dev/null +++ b/variants/Allmate/class.js @@ -0,0 +1,97 @@ +import ChessRules from "/base_rules.js"; +import PiPo from "/utils/PiPo.js"; +import Move from "/utils/Move.js"; + +export default class AllmateRules extends ChessRules { + + static get Options() { + return { + select: C.Options.select, + styles: [ + "cylinder", + "madrasi", + "zen" + ] + }; + } + + get hasEnpassant() { + return false; + } + get hasMoveStack() { + return true; + } + + setOtherVariables(fenParsed) { + super.setOtherVariables(fenParsed); + this.curMove = null; + } + + getPotentialMovesFrom(sq) { + // Remove direct captures: + return super.getPotentialMovesFrom(sq) + .filter(m => m.vanish.length == m.appear.length); + } + + // Called "recursively" before a move is played, until no effect + computeNextMove(move) { + if (move.appear.length > 0) + this.curMove = move; + const color = this.turn; + const oppCol = C.GetOppCol(this.turn); + let mv = new Move({ + start: this.curMove.end, + end: this.curMove.end, + appear: [], + vanish: [] + }); + this.playOnBoard(move); + for (let i=0; i 0 ? mv : null); + } + + // is piece on square x,y mated by color? + isMated(x, y, color) { + const myColor = C.GetOppCol(color); + if (!this.underCheck([x, y], color)) + return false; + for (let i=0; iTODO

+ +

Win by mate-capturing the enemy king.

+ + + chessvariants page. + + +

Dr. Chris Taylor (1979).

diff --git a/variants/Allmate/style.css b/variants/Allmate/style.css new file mode 100644 index 0000000..a3550bc --- /dev/null +++ b/variants/Allmate/style.css @@ -0,0 +1 @@ +@import url("/base_pieces.css"); diff --git a/variants/Chakart/class.js b/variants/Chakart/class.js index 24bbd3e..abd695b 100644 --- a/variants/Chakart/class.js +++ b/variants/Chakart/class.js @@ -41,6 +41,9 @@ export default class ChakartRules extends ChessRules { get hasReserveFen() { return false; } + get hasMoveStack() { + return true; + } static get IMMOBILIZE_CODE() { return { @@ -178,15 +181,17 @@ export default class ChakartRules extends ChessRules { } setOtherVariables(fenParsed) { - this.setFlags(fenParsed.flags); - this.reserve = {}; //to be filled later + super.setOtherVariables(fenParsed); this.egg = null; - this.moveStack = []; // Change seed (after FEN generation!!) // so that further calls differ between players: Random.setSeed(Math.floor(19840 * Math.random())); } + initReserves() { + this.reserve = {}; //to be filled later + } + // For Toadette bonus getDropMovesFrom([c, p]) { if (typeof c != "string" || this.reserve[c][p] == 0) @@ -415,57 +420,11 @@ export default class ChakartRules extends ChessRules { super.showChoices(moves); return false; } - if (!move.nextComputed) { - // Set potential random effects, so that play() is deterministic - // from opponent viewpoint: - const endPiece = this.getPiece(move.end.x, move.end.y); - switch (endPiece) { - case V.EGG: - move.egg = Random.sample(V.EGG_SURPRISE); - move.next = this.getEggEffect(move); - break; - case V.MUSHROOM: - move.next = this.getMushroomEffect(move); - break; - case V.BANANA: - case V.BOMB: - move.next = this.getBombBananaEffect(move, endPiece); - break; - } - if (!move.next && move.appear.length > 0 && !move.kingboo) { - const movingPiece = move.appear[0].p; - if (['b', 'r'].includes(movingPiece)) { - // Drop a banana or bomb: - const bs = - this.getRandomSquare([move.end.x, move.end.y], - movingPiece == 'r' - ? [[1, 1], [1, -1], [-1, 1], [-1, -1]] - : [[1, 0], [-1, 0], [0, 1], [0, -1]], - "freeSquare"); - if (bs) { - move.appear.push( - new PiPo({ - x: bs[0], - y: bs[1], - c: 'a', - p: movingPiece == 'r' ? 'd' : 'w' - }) - ); - if (this.board[bs[0]][bs[1]] != "") { - move.vanish.push( - new PiPo({ - x: bs[0], - y: bs[1], - c: this.getColor(bs[0], bs[1]), - p: this.getPiece(bs[0], bs[1]) - }) - ); - } - } - } - } - move.nextComputed = true; - } + this.postPlay(move, color, oppCol); + return true; + } + + postPlay(move, color, oppCol) { this.egg = move.egg; if (move.egg == "toadette") { this.reserve = { w: {}, b: {} }; @@ -526,15 +485,73 @@ export default class ChakartRules extends ChessRules { } } } - if (!move.next && !["daisy", "toadette", "kingboo"].includes(move.egg)) { - this.turn = oppCol; - this.movesCount++; - } + this.playOnBoard(move); + super.postPlay(move); + } + + playVisual(move, r) { + super.playVisual(move, r); if (move.egg) this.displayBonus(move); - this.playOnBoard(move); - this.nextMove = move.next; - return true; + } + + computeNextMove(move) { + // Set potential random effects, so that play() is deterministic + // from opponent viewpoint: + const endPiece = this.getPiece(move.end.x, move.end.y); + switch (endPiece) { + case V.EGG: + move.egg = Random.sample(V.EGG_SURPRISE); + move.next = this.getEggEffect(move); + break; + case V.MUSHROOM: + move.next = this.getMushroomEffect(move); + break; + case V.BANANA: + case V.BOMB: + move.next = this.getBombBananaEffect(move, endPiece); + break; + } + // NOTE: Chakart has also some side-effects: + if ( + !move.next && move.appear.length > 0 && + !move.kingboo && !move.luigiEffect + ) { + const movingPiece = move.appear[0].p; + if (['b', 'r'].includes(movingPiece)) { + // Drop a banana or bomb: + const bs = + this.getRandomSquare([move.end.x, move.end.y], + movingPiece == 'r' + ? [[1, 1], [1, -1], [-1, 1], [-1, -1]] + : [[1, 0], [-1, 0], [0, 1], [0, -1]], + "freeSquare"); + if (bs) { + move.appear.push( + new PiPo({ + x: bs[0], + y: bs[1], + c: 'a', + p: movingPiece == 'r' ? 'd' : 'w' + }) + ); + if (this.board[bs[0]][bs[1]] != "") { + move.vanish.push( + new PiPo({ + x: bs[0], + y: bs[1], + c: this.getColor(bs[0], bs[1]), + p: this.getPiece(bs[0], bs[1]) + }) + ); + } + } + } + } + } + + isLastMove(move) { + return !move.next && !["daisy", "toadette", "kingboo"].includes(move.egg); } // Helper to set and apply banana/bomb effect @@ -584,6 +601,7 @@ export default class ChakartRules extends ChessRules { new PiPo({x: coords[0], y: coords[1], c: oldColor, p: piece}) ] }); + em.luigiEffect = true; //avoid dropping bomb/banana by mistake } break; case "bowser": @@ -703,23 +721,13 @@ export default class ChakartRules extends ChessRules { return moves; } - playPlusVisual(move, r) { - const nextLines = () => { - if (!this.play(move)) - return; - this.moveStack.push(move); - this.playVisual(move, r); - if (this.nextMove) - this.playPlusVisual(this.nextMove, r); - else { - this.afterPlay(this.moveStack); - this.moveStack = []; - } - }; - if (this.moveStack.length == 0) - nextLines(); - else - this.animate(move, nextLines); + // Kingboo bonus can be animated better: + customAnimate(move, segments, cb) { + if (!move.kingboo) + return 0; + super.animateMoving(move.end, move.start, null, + segments.reverse().map(s => s.reverse()), cb); + return 1; } }; diff --git a/variants/Suction/class.js b/variants/Suction/class.js index e2e36c8..dbaefa0 100644 --- a/variants/Suction/class.js +++ b/variants/Suction/class.js @@ -110,4 +110,13 @@ export default class SuctionRules extends ChessRules { return "*"; } + // Better animation for swaps + customAnimate(move, segments, cb) { + if (move.vanish.length < 2) + return 0; + super.animateMoving(move.end, move.start, null, + segments.reverse().map(s => s.reverse()), cb); + return 1; + } + };