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/variants/%24%7Bvname%7D/current/img/pieces/%7B%7B%20asset%28%27mixstore/images/doc/html/left.jpg?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;
+ }
+
};