From: Benjamin Auder Date: Fri, 20 Mar 2020 20:05:12 +0000 (+0100) Subject: Add Coregal variant + simplify time management in Game.vue X-Git-Url: https://git.auder.net/%7B%7B%20asset%28%27mixstore/css/img/pieces/current/git-logo.png?a=commitdiff_plain;h=3f22c2c3939dfd6bd66da26e6d6d9848c6da86d2;p=vchess.git Add Coregal variant + simplify time management in Game.vue --- diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 71fa13cf..b83a409b 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -829,7 +829,7 @@ export const ChessRules = class ChessRules { castleSide++ //large, then small ) { if (this.castleFlags[c][castleSide] >= V.size.y) continue; - // If this code is reached, rooks and king are on initial position + // If this code is reached, rook and king are on initial position // NOTE: in some variants this is not a rook, but let's keep variable name const rookPos = this.castleFlags[c][castleSide]; diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 65e96cf6..e86c551e 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -203,10 +203,12 @@ export default { this.positionCursorTo(this.moves.length - 1); this.incheck = this.vr.getCheckSquares(this.vr.turn); const score = this.vr.getCurrentScore(); - if (["1-0","0-1"].includes(score)) - this.moves[L - 1].notation += "#"; - else if (this.vr.getCheckSquares(this.vr.turn).length > 0) - this.moves[L - 1].notation += "+"; + if (L > 0 && this.moves[L - 1].notation != "...") { + if (["1-0","0-1"].includes(score)) + this.moves[L - 1].notation += "#"; + else if (this.vr.getCheckSquares(this.vr.turn).length > 0) + this.moves[L - 1].notation += "+"; + } }, positionCursorTo: function(index) { this.cursor = index; diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 5d093742..ded2b8a9 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -197,5 +197,6 @@ export const translations = { "Squares disappear": "Squares disappear", "Standard rules": "Standard rules", "Transform an essay": "Transform an essay", + "Two royal pieces": "Two royal pieces", "Unidentified pieces": "Unidentified pieces" }; diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 877f8587..c065cf0e 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -197,5 +197,6 @@ export const translations = { "Squares disappear": "Las casillas desaparecen", "Standard rules": "Reglas estandar", "Transform an essay": "Transformar un ensayo", + "Two royal pieces": "Dos piezas reales", "Unidentified pieces": "Piezas no identificadas" }; diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index cf32f6e4..120571b7 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -197,5 +197,6 @@ export const translations = { "Squares disappear": "Les cases disparaissent", "Standard rules": "Règles usuelles", "Transform an essay": "Transformer un essai", + "Two royal pieces": "Deux pièces royales", "Unidentified pieces": "Pièces non identifiées" }; diff --git a/client/src/translations/rules/Coregal/en.pug b/client/src/translations/rules/Coregal/en.pug index 3a33838b..e9eb1ed8 100644 --- a/client/src/translations/rules/Coregal/en.pug +++ b/client/src/translations/rules/Coregal/en.pug @@ -1,2 +1,51 @@ p.boxed - | TODO + | Checkmating the queen wins too. A queen cannot go or stay under check. + +p Just as the king, the queen can be checked and mated. This means that +ul + li It is not allowed to make a move such that the queen can be captured. + li. + When your queen is attacked, you must play a move such that the queen + is no longer attacked. + li If it's impossible, then you lose. + +p. + Since the king remains royal, this allows a new way to win: check both + royal pieces at the same time, like on the following diagram. + +figure.diagram-container + .diagram + | fen:4Q3/4K3/8/8/3N4/5k2/2q5/8: + figcaption Both black king and queen are in check: white wins. + +h3 Special moves + +p. + If a pawn promotes into a queen, the latter is royal as well. + So under-promotions might be wiser. + +p. + You can castle with the queen or the king and any of the two rooks, + under the same conditions as orthodox castling. + Here is the resulting position after two white small castles and + one black large castle with the queen: + +figure.diagram-container + .diagram + | fen:r4rq1/ppppppkp/6p1/8/8/8/PPPPPPPP/1QR2RK1: + figcaption After two white small castles and one black large castle. + +p. + Note: to castle in a game you need to select + the king or queen first, and then move it to a rook. + +h3 Source + +p + a(href="https://www.chessvariants.com/winning.dir/coregal.html") Coregal Chess + |  on chessvariants.com. + | This variant can be played too + a(href="https://greenchess.net/rules.php?v=coregal") on greenchess.net + | . + +p Inventor: Vernon R. Parton (1970) diff --git a/client/src/translations/rules/Coregal/es.pug b/client/src/translations/rules/Coregal/es.pug index 3a33838b..489775b9 100644 --- a/client/src/translations/rules/Coregal/es.pug +++ b/client/src/translations/rules/Coregal/es.pug @@ -1,2 +1,53 @@ p.boxed - | TODO + | Es posible ganar dando jaque mate a la dama. + | Una dama no puede ir o permanecer en jaque. + +p Al igual que el rey, la dama puede ser en jaque (mate). Es decir que +ul + li Se prohíbe un movimiento que permita al oponente capturar a la dama. + li. + Cuando tu dama es atacada, debes hacer un movimiento + para que ya no sea atacado. + li Si es imposible, entonces has perdido. + +p. + Como el rey sigue siendo real, esto agrega una nueva forma de ganar: + jaque las dos piezas reales al mismo tiempo, como en el siguiente diagrama. + +figure.diagram-container + .diagrama + | fen:4Q3/4K3/8/8/3N4/5k2/2q5/8: + figcaption Las blancas ganan porque el rey y la dama negra están en jaque. + +h3 Movimientos especiales + +p. + Si un peón es promovido a reina, este último también es real. + Entonces, las sub-promociones pueden ser más sabias. + +p. + Puedes hacer el enroque con la dama o el rey y cualquiera de los + dos torres, en las mismas condiciones que para el ajedrez ortodoxo. + Aquí hay una posible posición después de dos pequeñas rocas blancas y + un gran enroque negro con la dama: + +figure.diagram-container + .diagrama + | fen:r4rq1/ppppppkp/6p1/8/8/8/PPPPPPPP/1QR2RK1: + figcaption Después de dos pequeñas rocas blancas y un gran enroque negro. + +p. + Nota: para enrocarse en una partida debes seleccionar el rey o la dama + primero, luego mueva la pieza a una torre. + +h3 Fuente + +p + | La + a(href="https://www.chessvariants.com/winning.dir/coregal.html") cariante Coregal + |  en chessvariants.com. + | Esta variante también es jugable + a(href="https://greenchess.net/rules.php?v=coregal") en greenchess.net + | . + +p Inventor: Vernon R. Parton (1970) diff --git a/client/src/translations/rules/Coregal/fr.pug b/client/src/translations/rules/Coregal/fr.pug index 3a33838b..47247f15 100644 --- a/client/src/translations/rules/Coregal/fr.pug +++ b/client/src/translations/rules/Coregal/fr.pug @@ -1,2 +1,53 @@ p.boxed - | TODO + | On peut gagner en matant la dame. Une dame ne peut aller ou rester en échec. + +p Tout comme le roi, la dame peut être mise en échec et matée. C'est-à-dire que +ul + li Un coup qui laisserait l'adversaire capturer la dame est interdit. + li. + Quand votre dame est attaquée, vous devez jouer un coup faisant + en sorte que celle-ci ne soit plus attaquée. + li Si c'est impossible, alors vous avez perdu. + +p. + Puisque le roi reste royal, ceci ajoute une nouvelle manière de gagner : + mettre en échc les deux pièces royales en même temps, + comme sur le diagramme suivant. + +figure.diagram-container + .diagram + | fen:4Q3/4K3/8/8/3N4/5k2/2q5/8: + figcaption Les blancs gagnent car le roi et la dame noir sont en échec. + +h3 Coups spéciaux + +p. + Si un pion est promu en dame, cette dernière est royale également. + Ainsi, les sous promotions peuvent être plus sages. + +p. + Vous pouvez roquer avec la dame ou le roi et n'importe laquelle des + deux tours, sous les mêmes conditions qu'aux échecs orthodoxes. + Voici une position possible après deux petits roques blancs et + un grand roque noir avec la dame : + +figure.diagram-container + .diagram + | fen:r4rq1/ppppppkp/6p1/8/8/8/PPPPPPPP/1QR2RK1: + figcaption Après deux petits roques blancs et un grand roque noir. + +p. + Note : pour roquer dans une partie il faut sélectionner le roi ou la dame + d'abord, puis déplacer la pièce sur une tour. + +h3 Source + +p + | La + a(href="https://www.chessvariants.com/winning.dir/coregal.html") variante Coregal + |  sur chessvariants.com. + | Cette variante est jouable également + a(href="https://greenchess.net/rules.php?v=coregal") sur greenchess.net + | . + +p Inventeur : Vernon R. Parton (1970) diff --git a/client/src/variants/Coregal.js b/client/src/variants/Coregal.js index ff4f1dfa..1a4a4038 100644 --- a/client/src/variants/Coregal.js +++ b/client/src/variants/Coregal.js @@ -1,10 +1,11 @@ -import { ChessRules } from "@/base_rules"; +import { ChessRules, Move, PiPo } from "@/base_rules"; import { ArrayFun } from "@/utils/array"; import { randInt, sample } from "@/utils/alea"; export class CoregalRules extends ChessRules { static IsGoodPosition(position) { if (!super.IsGoodPosition(position)) return false; + const rows = position.split("/"); // Check that at least one queen of each color is there: let queens = {}; for (let row of rows) { @@ -19,11 +20,37 @@ export class CoregalRules extends ChessRules { return !!flags.match(/^[a-z]{8,8}$/); } + // Scanning king position for faster updates is still interesting, + // but no need for INIT_COL_KING because it's given in castle flags. + scanKings(fen) { + this.kingPos = { w: [-1, -1], b: [-1, -1] }; + const fenRows = V.ParseFen(fen).position.split("/"); + const startRow = { 'w': V.size.x - 1, 'b': 0 }; + for (let i = 0; i < fenRows.length; i++) { + let k = 0; + for (let j = 0; j < fenRows[i].length; j++) { + switch (fenRows[i].charAt(j)) { + case "k": + this.kingPos["b"] = [i, k]; + break; + case "K": + this.kingPos["w"] = [i, k]; + break; + default: { + const num = parseInt(fenRows[i].charAt(j)); + if (!isNaN(num)) k += num - 1; + } + } + k++; + } + } + } + getCheckSquares(color) { let squares = []; const oppCol = V.GetOppCol(color); if (this.isAttacked(this.kingPos[color], oppCol)) - squares.push(this.kingPos[color]); + squares.push(JSON.parse(JSON.stringify(this.kingPos[color]))); for (let i=0; i= V.size.y) continue; -// // If this code is reached, rooks and king are on initial position -// -// // NOTE: in some variants this is not a rook, but let's keep variable name -// const rookPos = this.castleFlags[c][castleSide]; -// const castlingPiece = this.getPiece(x, rookPos); -// if (this.getColor(x, rookPos) != c) -// // Rook is here but changed color (see Benedict) -// continue; -// -// // Nothing on the path of the king ? (and no checks) -// const finDist = finalSquares[castleSide][0] - y; -// let step = finDist / Math.max(1, Math.abs(finDist)); -// i = y; -// do { -// if ( -// (!castleInCheck && this.isAttacked([x, i], oppCol)) || -// (this.board[x][i] != V.EMPTY && -// // NOTE: next check is enough, because of chessboard constraints -// (this.getColor(x, i) != c || -// ![V.KING, castlingPiece].includes(this.getPiece(x, i)))) -// ) { -// continue castlingCheck; -// } -// i += step; -// } while (i != finalSquares[castleSide][0]); -// -// // Nothing on the path to the rook? -// step = castleSide == 0 ? -1 : 1; -// for (i = y + step; i != rookPos; i += step) { -// if (this.board[x][i] != V.EMPTY) continue castlingCheck; -// } -// -// // Nothing on final squares, except maybe king and castling rook? -// for (i = 0; i < 2; i++) { -// if ( -// this.board[x][finalSquares[castleSide][i]] != V.EMPTY && -// this.getPiece(x, finalSquares[castleSide][i]) != V.KING && -// finalSquares[castleSide][i] != rookPos -// ) { -// continue castlingCheck; -// } -// } -// -// // If this code is reached, castle is valid -// moves.push( -// new Move({ -// appear: [ -// new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }), -// new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c }) -// ], -// vanish: [ -// new PiPo({ x: x, y: y, p: V.KING, c: c }), -// new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c }) -// ], -// end: -// Math.abs(y - rookPos) <= 2 -// ? { x: x, y: rookPos } -// : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) } -// }) -// ); -// } -// -// return moves; + getCastleMoves([x, y]) { + const c = this.getColor(x, y); + if ( + x != (c == "w" ? V.size.x - 1 : 0) || + !this.castleFlags[c].slice(1, 3).includes(y) + ) { + // x isn't first rank, or piece moved + return []; + } + const castlingPiece = this.getPiece(x, y); + + // Relative position of the selected piece: left or right ? + // If left: small castle left, large castle right. + // If right: usual situation. + const relPos = (this.castleFlags[c][1] == y ? "left" : "right"); + + // Castling ? + const oppCol = V.GetOppCol(c); + let moves = []; + let i = 0; + // Castling piece, then rook: + const finalSquares = { + 0: (relPos == "left" ? [1, 2] : [2, 3]), + 3: (relPos == "right" ? [6, 5] : [5, 4]) + }; + + // Left, then right castle: + castlingCheck: for (let castleSide of [0, 3]) { + if (this.castleFlags[c][castleSide] >= 8) continue; + + // Rook and castling piece are on initial position + const rookPos = this.castleFlags[c][castleSide]; + + // Nothing on the path of the king ? (and no checks) + const finDist = finalSquares[castleSide][0] - y; + let step = finDist / Math.max(1, Math.abs(finDist)); + i = y; + do { + if ( + this.isAttacked([x, i], oppCol) || + (this.board[x][i] != V.EMPTY && + // NOTE: next check is enough, because of chessboard constraints + (this.getColor(x, i) != c || + ![castlingPiece, V.ROOK].includes(this.getPiece(x, i)))) + ) { + continue castlingCheck; + } + i += step; + } while (i != finalSquares[castleSide][0]); + + // Nothing on the path to the rook? + step = castleSide == 0 ? -1 : 1; + for (i = y + step; i != rookPos; i += step) { + if (this.board[x][i] != V.EMPTY) continue castlingCheck; + } + + // Nothing on final squares, except maybe castling piece and rook? + for (i = 0; i < 2; i++) { + if ( + this.board[x][finalSquares[castleSide][i]] != V.EMPTY && + ![y, rookPos].includes(finalSquares[castleSide][i]) + ) { + continue castlingCheck; + } + } + + // If this code is reached, castle is valid + moves.push( + new Move({ + appear: [ + new PiPo({ x: x, y: finalSquares[castleSide][0], p: castlingPiece, c: c }), + new PiPo({ x: x, y: finalSquares[castleSide][1], p: V.ROOK, c: c }) + ], + vanish: [ + new PiPo({ x: x, y: y, p: castlingPiece, c: c }), + new PiPo({ x: x, y: rookPos, p: V.ROOK, c: c }) + ], + // In this variant, always castle by playing onto the rook + end: { x: x, y: rookPos } + }) + ); + } + + return moves; } underCheck(color) { @@ -242,27 +269,55 @@ export class CoregalRules extends ChessRules { } updateCastleFlags(move, piece) { -// const c = V.GetOppCol(this.turn); -// const firstRank = (c == "w" ? V.size.x - 1 : 0); -// // Update castling flags if rooks are moved -// const oppCol = V.GetOppCol(c); -// const oppFirstRank = V.size.x - 1 - firstRank; -// if (piece == V.KING && move.appear.length > 0) -// this.castleFlags[c] = [V.size.y, V.size.y]; -// else if ( -// move.start.x == firstRank && //our rook moves? -// this.castleFlags[c].includes(move.start.y) -// ) { -// const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1); -// this.castleFlags[c][flagIdx] = V.size.y; -// } else if ( -// move.end.x == oppFirstRank && //we took opponent rook? -// this.castleFlags[oppCol].includes(move.end.y) -// ) { -// const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1); -// this.castleFlags[oppCol][flagIdx] = V.size.y; -// } + const c = V.GetOppCol(this.turn); + const firstRank = (c == "w" ? V.size.x - 1 : 0); + // Update castling flags if castling pieces moved or were captured + const oppCol = V.GetOppCol(c); + const oppFirstRank = V.size.x - 1 - firstRank; + if (move.start.x == firstRank && [V.KING, V.QUEEN].includes(piece)) { + if (this.castleFlags[c][1] == move.start.y) + this.castleFlags[c][1] = 8; + else if (this.castleFlags[c][2] == move.start.y) + this.castleFlags[c][2] = 8; + // Else: the flag is already turned off + } + else if ( + move.start.x == firstRank && //our rook moves? + [this.castleFlags[c][0], this.castleFlags[c][3]].includes(move.start.y) + ) { + const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 3); + this.castleFlags[c][flagIdx] = 8; + } else if ( + move.end.x == oppFirstRank && //we took opponent rook? + [this.castleFlags[oppCol][0], this.castleFlags[oppCol][3]].includes(move.end.y) + ) { + const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 3); + this.castleFlags[oppCol][flagIdx] = 8; + } } // NOTE: do not set queen value to 1000 or so, because there may be several. + + getNotation(move) { + if (move.appear.length == 2) { + // Castle: determine the right notation + const color = move.appear[0].c; + let symbol = (move.appear[0].p == V.QUEEN ? "Q" : "") + "0-0"; + if ( + ( + this.castleFlags[color][1] == move.vanish[0].y && + move.end.y > move.start.y + ) + || + ( + this.castleFlags[color][2] == move.vanish[0].y && + move.end.y < move.start.y + ) + ) { + symbol += "-0"; + } + return symbol; + } + return super.getNotation(move); + } }; diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index ac6ed4d5..6339a3ce 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -288,11 +288,11 @@ export default { // Discard potential "/?next=[...]" for page indication: encodeURIComponent(this.$route.path.match(/\/game\/[a-zA-Z0-9]+/)[0]); this.conn = new WebSocket(this.connexionString); - this.conn.onmessage = this.socketMessageListener; - this.conn.onclose = this.socketCloseListener; + this.conn.addEventListener("message", this.socketMessageListener); + this.conn.addEventListener("close", this.socketCloseListener); // Socket init required before loading remote game: const socketInit = callback => { - if (!!this.conn && this.conn.readyState == 1) + if (this.conn.readyState == 1) // 1 == OPEN state callback(); else @@ -480,6 +480,8 @@ export default { } case "killed": // I logged in elsewhere: + this.conn.removeEventListener("message", this.socketMessageListener); + this.conn.removeEventListener("close", this.socketCloseListener); this.conn = null; alert(this.st.tr["New connexion detected: tab now offline"]); break; @@ -567,7 +569,7 @@ export default { .filter(k => [ "id","fen","players","vid","cadence","fenStart","vname", - "moves","clocks","initime","score","drawOffer","rematchOffer" + "moves","clocks","score","drawOffer","rematchOffer" ].includes(k)) .reduce( (obj, k) => { @@ -591,13 +593,11 @@ export default { case "lastate": { // Got opponent infos about last move this.gotLastate = true; - if (!data.data.nothing) { - this.lastate = data.data; - if (this.game.rendered) - // Game is rendered (Board component) - this.processLastate(); - // Else: will be processed when game is ready - } + this.lastate = data.data; + if (this.game.rendered) + // Game is rendered (Board component) + this.processLastate(); + // Else: will be processed when game is ready break; } case "newmove": { @@ -636,12 +636,11 @@ export default { } } this.$refs["basegame"].play(movePlus.move, "received", null, true); + const moveColIdx = ["w", "b"].indexOf(movePlus.color); + this.game.clocks[moveColIdx] = movePlus.clock; this.processMove( movePlus.move, - { - clock: movePlus.clock, - receiveMyMove: receiveMyMove - } + { receiveMyMove: receiveMyMove } ); } } @@ -649,9 +648,8 @@ export default { } case "gotmove": { this.opponentGotMove = true; - // Now his clock starts running: + // Now his clock starts running on my side: const oppIdx = ['w','b'].indexOf(this.vr.turn); - this.game.initime[oppIdx] = Date.now(); this.re_setClocks(); break; } @@ -734,45 +732,43 @@ export default { ); }, sendLastate: function(target) { - if ( - (this.game.moves.length > 0 && this.vr.turn != this.game.mycolor) || - this.game.score != "*" || - this.drawOffer == "sent" || - this.rematchOffer == "sent" - ) { - // Send our "last state" informations to opponent - const L = this.game.moves.length; - const myIdx = ["w", "b"].indexOf(this.game.mycolor); - const myLastate = { - lastMove: L > 0 ? this.game.moves[L - 1] : undefined, - clock: this.game.clocks[myIdx], - // Since we played a move (or abort or resign), - // only drawOffer=="sent" is possible - drawSent: this.drawOffer == "sent", - rematchSent: this.rematchOffer == "sent", - score: this.game.score, - scoreMsg: this.game.scoreMsg, - movesCount: L, - initime: this.game.initime[1 - myIdx] //relevant only if I played - }; - this.send("lastate", { data: myLastate, target: target }); - } else { - this.send("lastate", { data: {nothing: true}, target: target }); - } + // Send our "last state" informations to opponent + const L = this.game.moves.length; + const myIdx = ["w", "b"].indexOf(this.game.mycolor); + const myLastate = { + lastMove: + (L > 0 && this.vr.turn != this.game.mycolor) + ? this.game.moves[L - 1] + : undefined, + clock: this.game.clocks[myIdx], + // Since we played a move (or abort or resign), + // only drawOffer=="sent" is possible + drawSent: this.drawOffer == "sent", + rematchSent: this.rematchOffer == "sent", + score: this.game.score != "*" ? this.game.score : undefined, + scoreMsg: this.game.score != "*" ? this.game.scoreMsg : undefined, + movesCount: L + }; + this.send("lastate", { data: myLastate, target: target }); }, // lastate was received, but maybe game wasn't ready yet: processLastate: function() { const data = this.lastate; this.lastate = undefined; //security... const L = this.game.moves.length; + const oppIdx = 1 - ["w", "b"].indexOf(this.game.mycolor); + this.game.clocks[oppIdx] = data.clock; if (data.movesCount > L) { // Just got last move from him this.$refs["basegame"].play(data.lastMove, "received", null, true); - this.processMove(data.lastMove, { clock: data.clock }); + this.processMove(data.lastMove); + } else { + clearInterval(this.clockUpdate); + this.re_setClocks(); } if (data.drawSent) this.drawOffer = "received"; if (data.rematchSent) this.rematchOffer = "received"; - if (data.score != "*") { + if (!!data.score) { this.drawOffer = ""; if (this.game.score == "*") this.gameOver(data.score, data.scoreMsg); @@ -817,7 +813,6 @@ export default { // Game state (including FEN): will be updated moves: [], clocks: [-1, -1], //-1 = unstarted - initime: [0, 0], //initialized later score: "*" } ); @@ -913,16 +908,16 @@ export default { const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers if (!game.chats) game.chats = []; //live games don't have chat history if (gtype == "corr") { - // NOTE: clocks in seconds, initime in milliseconds + // NOTE: clocks in seconds game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of game.clocks = [tc.mainTime, tc.mainTime]; const L = game.moves.length; if (game.score == "*") { - // Set clocks + initime - game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; - if (L >= 1) game.initime[L % 2] = game.moves[L-1].played; - // NOTE: game.clocks shouldn't be computed right now: - // job will be done in re_setClocks() called soon below. + // Adjust clocks + if (L >= 2) { + game.clocks[L % 2] -= + (Date.now() - game.moves[L-1].played) / 1000; + } } // Sort chat messages from newest to oldest game.chats.sort((c1, c2) => { @@ -948,16 +943,20 @@ export default { // Now that we used idx and played, re-format moves as for live games game.moves = game.moves.map(m => m.squares); } - if (gtype == "live" && game.clocks[0] < 0) { - // Game is unstarted. clocks and initime are ignored until move 2 - game.clocks = [tc.mainTime, tc.mainTime]; - game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; - if (myIdx >= 0) { - // I play in this live game - GameStorage.update(game.id, { - clocks: game.clocks, - initime: game.initime - }); + if (gtype == "live") { + if (game.clocks[0] < 0) { + // Game is unstarted. clock is ignored until move 2 + game.clocks = [tc.mainTime, tc.mainTime]; + if (myIdx >= 0) { + // I play in this live game + GameStorage.update(game.id, { + clocks: game.clocks + }); + } + } else { + if (!!game.initime) + // It's my turn: clocks not updated yet + game.clocks[myIdx] -= (Date.now() - game.initime) / 1000; } } // TODO: merge next 2 "if" conditions @@ -1075,41 +1074,34 @@ export default { GameStorage.get(this.gameRef, callback); }, re_setClocks: function() { + this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':')); if (this.game.moves.length < 2 || this.game.score != "*") { // 1st move not completed yet, or game over: freeze time - this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':')); return; } const currentTurn = this.vr.turn; const currentMovesCount = this.game.moves.length; const colorIdx = ["w", "b"].indexOf(currentTurn); - let countdown = - this.game.clocks[colorIdx] - - (Date.now() - this.game.initime[colorIdx]) / 1000; - this.virtualClocks = [0, 1].map(i => { - const removeTime = - i == colorIdx ? (Date.now() - this.game.initime[colorIdx]) / 1000 : 0; - return ppt(this.game.clocks[i] - removeTime).split(':'); - }); this.clockUpdate = setInterval( () => { if ( - countdown < 0 || + this.game.clocks[colorIdx] < 0 || this.game.moves.length > currentMovesCount || this.game.score != "*" ) { clearInterval(this.clockUpdate); - if (countdown < 0) + if (this.game.clocks[colorIdx] < 0) this.gameOver( currentTurn == "w" ? "0-1" : "1-0", "Time" ); - } else + } else { this.$set( this.virtualClocks, colorIdx, - ppt(Math.max(0, --countdown)).split(':') + ppt(Math.max(0, --this.game.clocks[colorIdx])).split(':') ); + } }, 1000 ); @@ -1122,21 +1114,28 @@ export default { const nextIdx = 1 - colorIdx; const doProcessMove = () => { const origMovescount = this.game.moves.length; - let addTime = 0; //for live games + // The move is (about to be) played: stop clock + clearInterval(this.clockUpdate); if (moveCol == this.game.mycolor && !data.receiveMyMove) { if (this.drawOffer == "received") // I refuse draw this.drawOffer = ""; if (this.game.type == "live" && origMovescount >= 2) { - const elapsed = Date.now() - this.game.initime[colorIdx]; - // elapsed time is measured in milliseconds - addTime = this.game.increment - elapsed / 1000; + this.game.clocks[colorIdx] += this.game.increment; + // For a correct display in casqe of disconnected opponent: + this.$set( + this.virtualClocks, + colorIdx, + ppt(this.game.clocks[colorIdx]).split(':') + ); + GameStorage.update(this.gameRef, { + // It's not my turn anymore: + initime: null + }); } } // Update current game object: playMove(move, this.vr); - // The move is played: stop clock - clearInterval(this.clockUpdate); if (!data.score) // Received move, score is computed in BaseGame, but maybe not yet. // ==> Compute it here, although this is redundant (TODO) @@ -1144,21 +1143,10 @@ export default { if (data.score != "*") this.gameOver(data.score); this.game.moves.push(move); this.game.fen = this.vr.getFen(); - if (this.game.type == "live") { - if (!!data.clock) this.game.clocks[colorIdx] = data.clock; - else this.game.clocks[colorIdx] += addTime; - } else { + if (this.game.type == "corr") { // In corr games, just reset clock to mainTime: this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime; } - // NOTE: opponent's initime is reset after "gotmove" is received - if ( - !this.game.mycolor || - moveCol != this.game.mycolor || - !!data.receiveMyMove - ) { - this.game.initime[nextIdx] = Date.now(); - } // If repetition detected, consider that a draw offer was received: const fenObj = this.vr.getFenForRepeat(); this.repeat[fenObj] = @@ -1183,6 +1171,19 @@ export default { } // Since corr games are stored at only one location, update should be // done only by one player for each move: + if ( + this.game.type == "live" && + !!this.game.mycolor && + moveCol != this.game.mycolor && + this.game.moves.length >= 2 + ) { + // Receive a move: update initime + this.game.initime = Date.now(); + GameStorage.update(this.gameRef, { + // It's my turn now! + initime: this.game.initime + }); + } if ( !!this.game.mycolor && !data.receiveMyMove && @@ -1219,7 +1220,6 @@ export default { move: filtered_move, moveIdx: origMovescount, clocks: this.game.clocks, - initime: this.game.initime, drawOffer: drawCode }); }; @@ -1288,10 +1288,7 @@ export default { // The board might have been hidden: if (boardDiv.style.visibility == "hidden") boardDiv.style.visibility = "visible"; - if (data.score == "*") { - this.game.initime[nextIdx] = Date.now(); - this.re_setClocks(); - } + if (data.score == "*") this.re_setClocks(); } }; let el = document.querySelector("#buttonsConfirm > .acceptBtn"); diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 4d830bbf..d0cabb46 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -296,8 +296,8 @@ export default { encodeURIComponent(this.$route.path); this.conn = new WebSocket(this.connexionString); this.conn.onopen = connectAndPoll; - this.conn.onmessage = this.socketMessageListener; - this.conn.onclose = this.socketCloseListener; + this.conn.addEventListener("message", this.socketMessageListener); + this.conn.addEventListener("close", this.socketCloseListener); }, mounted: function() { document.addEventListener('visibilitychange', this.visibilityChange); @@ -653,6 +653,8 @@ export default { break; case "killed": // I logged in elsewhere: + this.conn.removeEventListener("message", this.socketMessageListener); + this.conn.removeEventListener("close", this.socketCloseListener); this.conn = null; alert(this.st.tr["New connexion detected: tab now offline"]); break; @@ -1190,7 +1192,6 @@ export default { // Game state (including FEN): will be updated moves: [], clocks: [-1, -1], //-1 = unstarted - initime: [0, 0], //initialized later score: "*" } ); diff --git a/server/db/populate.sql b/server/db/populate.sql index 2107afb9..d6566535 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -17,6 +17,7 @@ insert or ignore into Variants (name,description) values ('Checkered', 'Shared pieces'), ('Chess960', 'Standard rules'), ('Circular', 'Run forward'), + ('Coregal', 'Two royal pieces'), ('Crazyhouse', 'Captures reborn'), ('Cylinder', 'Neverending rows'), ('Dark', 'In the shadow'),