From: Benjamin Auder <benjamin.auder@somewhere> Date: Thu, 20 Feb 2020 01:17:04 +0000 (+0100) Subject: Fixes X-Git-Url: https://git.auder.net/doc/html/css/scripts/css/index.css?a=commitdiff_plain;h=8477e53d8e78606e4c4e4bf91c77b1011aab583c;p=vchess.git Fixes --- diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 5ebe4997..295b4cd3 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -43,7 +43,7 @@ export const ChessRules = class ChessRules { } // Some variants cannot have analyse mode - static get CanAnalyse() { + static get CanAnalyze() { return true; } diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 07d62cf7..c52f94c8 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -112,14 +112,6 @@ export default { "game.fenStart": function() { this.re_setVariables(); }, - // Received a new move to play: - "game.moveToPlay": function(move) { - if (move) this.play(move, "receive"); - }, - // ...Or to undo (corr game, move not validated) - "game.moveToUndo": function(move) { - if (move) this.undo(move); - } }, computed: { showMoves: function() { @@ -225,26 +217,21 @@ export default { }, re_setVariables: function() { this.endgameMessage = ""; - this.orientation = this.game.mycolor || "w"; //default orientation for observed games + // "w": default orientation for observed games + this.orientation = this.game.mycolor || "w"; this.moves = JSON.parse(JSON.stringify(this.game.moves || [])); - // Post-processing: decorate each move with color + current FEN: - // (to be able to jump to any position quickly) - let vr_tmp = new V(this.game.fenStart); //vr is already at end of game - this.firstMoveNumber = Math.floor( - V.ParseFen(this.game.fenStart).movesCount / 2 - ); + // Post-processing: decorate each move with color, notation and FEN + let vr_tmp = new V(this.game.fenStart); + const parsedFen = V.ParseFen(this.game.fenStart); + const firstMoveColor = parsedFen.turn; + this.firstMoveNumber = Math.floor(parsedFen.movesCount / 2); this.moves.forEach(move => { - // NOTE: this is doing manually what play() function below achieve, - // but in a lighter "fast-forward" way move.color = vr_tmp.turn; move.notation = vr_tmp.getNotation(move); vr_tmp.play(move); move.fen = vr_tmp.getFen(); }); - if ( - (this.moves.length > 0 && this.moves[0].color == "b") || - (this.moves.length == 0 && vr_tmp.turn == "b") - ) { + if (firstMoveColor == "b") { // 'end' is required for Board component to check lastMove for e.p. this.moves.unshift({ color: "w", diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue index 0d566a81..b1d2b370 100644 --- a/client/src/components/ComputerGame.vue +++ b/client/src/components/ComputerGame.vue @@ -1,5 +1,6 @@ <template lang="pug"> BaseGame( + ref="basegame" :game="game" :vr="vr" @newmove="processMove" @@ -41,7 +42,6 @@ export default { } } }, - // Modal end of game, and then sub-components created: function() { // Computer moves web worker logic: this.compWorker = new Worker(); @@ -63,7 +63,7 @@ export default { let moveIdx = 0; let self = this; (function executeMove() { - self.$set(self.game, "moveToPlay", compMove[moveIdx++]); + self.$refs["basegame"].play(compMove[moveIdx++]); if (moveIdx >= compMove.length) { self.compThink = false; if (self.game.score != "*") @@ -95,6 +95,8 @@ export default { if (mycolor != "w" || this.gameInfo.mode == "auto") this.playComputerMove(); }, + // NOTE: a "goto" action could lead to an error when comp is thinking, + // but it's OK because from the user viewpoint the game just stops. playComputerMove: function() { this.timeStart = Date.now(); this.compThink = true; diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue index fba1f79b..f85c49fe 100644 --- a/client/src/components/GameList.vue +++ b/client/src/components/GameList.vue @@ -32,7 +32,8 @@ export default { data: function() { return { st: store.state, - showCadence: true + deleted: {}, //mark deleted games + showCadence: window.innerWidth >= 425 //TODO: arbitrary value }; }, mounted: function() { @@ -42,7 +43,7 @@ export default { if (!timeoutLaunched) { timeoutLaunched = true; setTimeout(() => { - this.showCadence = window.innerWidth >= 425; //TODO: arbitrary + this.showCadence = window.innerWidth >= 425; timeoutLaunched = false; }, 500); } @@ -53,37 +54,39 @@ export default { // Show in order: games where it's my turn, my running games, my games, other games let minCreated = Number.MAX_SAFE_INTEGER; let maxCreated = 0; - let augmentedGames = this.games.map(g => { - let priority = 0; - let myColor = undefined; - if ( - g.players.some( - p => p.uid == this.st.user.id || p.sid == this.st.user.sid - ) - ) { - priority++; - myColor = - g.players[0].uid == this.st.user.id || - g.players[0].sid == this.st.user.sid - ? "w" - : "b"; - if (g.score == "*") { + let augmentedGames = this.games + .filter(g => !this.deleted[g.id]) + .map(g => { + let priority = 0; + let myColor = undefined; + if ( + g.players.some( + p => p.uid == this.st.user.id || p.sid == this.st.user.sid + ) + ) { priority++; - // I play in this game, so g.fen will be defined - // NOTE: this is a fragile way to detect turn, - // but since V isn't defined let's do that for now. (TODO:) - //if (V.ParseFen(g.fen).turn == myColor) - if (g.fen.match(" " + myColor + " ")) priority++; + myColor = + g.players[0].uid == this.st.user.id || + g.players[0].sid == this.st.user.sid + ? "w" + : "b"; + if (g.score == "*") { + priority++; + // I play in this game, so g.fen will be defined + // NOTE: this is a fragile way to detect turn, + // but since V isn't defined let's do that for now. (TODO:) + //if (V.ParseFen(g.fen).turn == myColor) + if (g.fen.match(" " + myColor + " ")) priority++; + } } - } - if (g.created < minCreated) minCreated = g.created; - if (g.created > maxCreated) maxCreated = g.created; - return Object.assign({}, g, { - priority: priority, - myTurn: priority == 3, - myColor: myColor + if (g.created < minCreated) minCreated = g.created; + if (g.created > maxCreated) maxCreated = g.created; + return Object.assign({}, g, { + priority: priority, + myTurn: priority == 3, + myColor: myColor + }); }); - }); const deltaCreated = maxCreated - minCreated; return augmentedGames.sort((g1, g2) => { return ( @@ -129,7 +132,14 @@ export default { }, deleteGame: function(game, e) { if (game.score != "*") { - if (confirm(this.st.tr["Remove game?"])) GameStorage.remove(game.id); + if (confirm(this.st.tr["Remove game?"])) { + GameStorage.remove( + game.id, + () => { + this.$set(this.deleted, game.id, true); + } + ); + } e.stopPropagation(); } } diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue index 30ed2338..48f082f4 100644 --- a/client/src/components/MoveList.vue +++ b/client/src/components/MoveList.vue @@ -4,58 +4,6 @@ export default { name: "my-move-list", props: ["moves", "cursor", "score", "message", "firstNum"], render(h) { - if (this.moves.length == 0) return h("div"); - let tableContent = []; - let moveCounter = 0; - let tableRow = undefined; - let moveCells = undefined; - let curCellContent = ""; - let firstIndex = 0; - for (let i = 0; i < this.moves.length; i++) { - if (this.moves[i].color == "w") { - if (i == 0 || (i > 0 && this.moves[i - 1].color == "b")) { - if (tableRow) { - tableRow.children = moveCells; - tableContent.push(tableRow); - } - moveCells = [ - h("td", { domProps: { innerHTML: ++moveCounter + "." } }) - ]; - tableRow = h("tr", {}); - curCellContent = ""; - firstIndex = i; - } - } - // Next condition is fine because even if the first move is black, - // there will be the "..." which count as white move. - else if (this.moves[i].color == "b" && this.moves[i - 1].color == "w") - firstIndex = i; - curCellContent += this.moves[i].notation; - if ( - i < this.moves.length - 1 && - this.moves[i + 1].color == this.moves[i].color - ) - curCellContent += ","; - //color change - else { - moveCells.push( - h("td", { - domProps: { innerHTML: curCellContent }, - on: { click: () => this.gotoMove(i) }, - class: { - "highlight-lm": this.cursor >= firstIndex && this.cursor <= i - } - }) - ); - curCellContent = ""; - } - } - // Complete last row, which might not be full: - if (moveCells.length - 1 == 1) { - moveCells.push(h("td", { domProps: { innerHTML: "" } })); - } - tableRow.children = moveCells; - tableContent.push(tableRow); let rootElements = []; if (!!this.score && this.score != "*") { const scoreDiv = h( @@ -70,37 +18,90 @@ export default { ); rootElements.push(scoreDiv); } - rootElements.push( - h( - "table", - { - class: { - "moves-list": true + if (this.moves.length > 0) { + let tableContent = []; + let moveCounter = 0; + let tableRow = undefined; + let moveCells = undefined; + let curCellContent = ""; + let firstIndex = 0; + for (let i = 0; i < this.moves.length; i++) { + if (this.moves[i].color == "w") { + if (i == 0 || (i > 0 && this.moves[i - 1].color == "b")) { + if (tableRow) { + tableRow.children = moveCells; + tableContent.push(tableRow); + } + moveCells = [ + h( + "div", + { + "class": {td: true}, + domProps: { innerHTML: ++moveCounter + "." } + } + ) + ]; + tableRow = h("div", {"class": {tr: true}}); + curCellContent = ""; + firstIndex = i; } - }, - tableContent - ) - ); + } + // Next condition is fine because even if the first move is black, + // there will be the "..." which count as white move. + else if (this.moves[i].color == "b" && this.moves[i - 1].color == "w") + firstIndex = i; + curCellContent += this.moves[i].notation; + if ( + i < this.moves.length - 1 && + this.moves[i + 1].color == this.moves[i].color + ) + curCellContent += ","; + else { + // Color change + moveCells.push( + h( + "div", + { + "class": { + td: true, + "highlight-lm": this.cursor >= firstIndex && this.cursor <= i + }, + domProps: { innerHTML: curCellContent }, + on: { click: () => this.gotoMove(i) } + } + ) + ); + curCellContent = ""; + } + } + // Complete last row, which might not be full: + if (moveCells.length - 1 == 1) { + moveCells.push(h("div", {"class": {td: true}})); + } + tableRow.children = moveCells; + tableContent.push(tableRow); + rootElements.push( + h( + "div", + { + class: { + "moves-list": true + } + }, + tableContent + ) + ); + } return h("div", {}, rootElements); }, watch: { cursor: function(newCursor) { if (window.innerWidth <= 767) return; //scrolling would hide chessboard - // Count grouped moves until the cursor (if multi-moves): - let groupsCount = 0; - let curCol = undefined; - for (let i = 0; i < newCursor; i++) { - const m = this.moves[i]; - if (m.color != curCol) { - groupsCount++; - curCol = m.color; - } - } // $nextTick to wait for table > tr to be rendered this.$nextTick(() => { - let rows = document.querySelectorAll("#movesList tr"); - if (rows.length > 0) { - rows[Math.floor(groupsCount / 2)].scrollIntoView({ + let curMove = document.querySelector(".td.highlight-lm"); + if (curMove) { + curMove.scrollIntoView({ behavior: "auto", block: "nearest" }); @@ -118,67 +119,28 @@ export default { <style lang="sass" scoped> .moves-list - min-width: 250px + cursor: pointer + min-height: 1px + max-height: 500px + overflow: auto + background-color: white + width: 280px + & > .tr + clear: both + border-bottom: 1px solid lightgrey + & > .td + float: left + padding: 2% 0 2% 1% + &:first-child + color: grey + width: 15% + &:not(first-child) + width: 41% + +@media screen and (max-width: 767px) + .moves-list + width: 100% -td.highlight-lm +.td.highlight-lm background-color: plum </style> - -<!-- TODO: use template function + multi-moves: much easier -<template lang="pug"> -div - #scoreInfo(v-if="score!='*'") - p {{ score }} - p {{ message }} - table.moves-list - tbody - tr(v-for="moveIdx in evenNumbers") - td {{ firstNum + moveIdx / 2 + 1 }} - td(:class="{'highlight-lm': cursor == moveIdx}" - @click="() => gotoMove(moveIdx)") - | {{ moves[moveIdx].notation }} - td(v-if="moveIdx < moves.length-1" - :class="{'highlight-lm': cursor == moveIdx+1}" - @click="() => gotoMove(moveIdx+1)") - | {{ moves[moveIdx+1].notation }} - // Else: just add an empty cell - td(v-else) -</template> - -<script> -// Component for moves list on the right -export default { - name: 'my-move-list', - props: ["moves","cursor","score","message","firstNum"], - watch: { - cursor: function(newValue) { - if (window.innerWidth <= 767) - return; //moves list is below: scrolling would hide chessboard - if (newValue < 0) - newValue = 0; //avoid rows[-1] => error - // $nextTick to wait for table > tr to be rendered - this.$nextTick( () => { - let rows = document.querySelectorAll('#movesList tr'); - if (rows.length > 0) - { - rows[Math.floor(newValue/2)].scrollIntoView({ - behavior: "auto", - block: "nearest", - }); - } - }); - }, - }, - computed: { - evenNumbers: function() { - return [...Array(this.moves.length).keys()].filter(i => i%2==0); - }, - }, - methods: { - gotoMove: function(index) { - this.$emit("goto-move", index); - }, - }, -}; -</script> ---> diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 5afbc59a..0b2d7892 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -10,6 +10,7 @@ export const translations = { Apply: "Apply", "Back to list": "Back to list", "Black to move": "Black to move", + "Black surrender": "Black surrender", "Black win": "Black win", "Board colors": "Board colors", "Board size": "Board size", @@ -24,7 +25,8 @@ export const translations = { Contact: "Contact", "Correspondance challenges": "Correspondance challenges", "Correspondance games": "Correspondance games", - "Database error:": "Database error:", + "Database error: stop private browsing, or update your browser": + "Database error: stop private browsing, or update your browser", Delete: "Delete", Download: "Download", Draw: "Draw", @@ -33,15 +35,13 @@ export const translations = { Email: "Email", "Email sent!": "Email sent!", "Empty message": "Empty message", - "Error while loading database:": "Error while loading database:", "Example game": "Example game", - "Game retrieval failed:": "Game retrieval failed:", - "Game removal failed:": "Game removal failed:", Go: "Go", green: "green", Hall: "Hall", "Highlight last move and checks?": "Highlight last move and checks?", Instructions: "Instructions", + "is not online": "is not online", Language: "Language", "Live challenges": "Live challenges", "Live games": "Live games", @@ -96,6 +96,7 @@ export const translations = { "Show possible moves?": "Show possible moves?", "Show solution": "Show solution", Solution: "Solution", + Stop: "Stop", "Stop game": "Stop game", Subject: "Subject", "Terminate game?": "Terminate game?", @@ -109,6 +110,7 @@ export const translations = { Variants: "Variants", Versus: "Versus", "White to move": "White to move", + "White surrender": "White surrender", "White win": "White win", "Who's there?": "Who's there?", With: "With", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index fd9f28ea..17438e50 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -11,6 +11,7 @@ export const translations = { "Back to list": "Volver a la lista", Black: "Negras", "Black to move": "Juegan las negras", + "Black surrender": "Las negras abandonan", "Black win": "Las negras gagnan", "Board colors": "Colores del tablero", "Board size": "Tamaño del tablero", @@ -25,7 +26,8 @@ export const translations = { Contact: "Contacto", "Correspondance challenges": "DesafÃos por correspondencia", "Correspondance games": "Partidas por correspondencia", - "Database error:": "Error de la base de datos:", + "Database error: stop private browsing, or update your browser": + "Error de la base de datos: detener la navegación privada, o actualizar su navegador", Delete: "Borrar", Download: "Descargar", Draw: "Tablas", @@ -34,15 +36,13 @@ export const translations = { Email: "Email", "Email sent!": "¡Email enviado!", "Empty message": "Mensaje vacio", - "Error while loading database:": "Error al cargar la base de datos:", "Example game": "Ejemplo de partida", - "Game retrieval failed:": "La recuperación de la partida falló:", - "Game removal failed:": "La eliminación de la partida falló:", Go: "Go", green: "verde", Hall: "Salón", "Highlight last move and checks?": "¿Resaltar el último movimiento y jaques?", Instructions: "Instrucciones", + "is not online": "no está en lÃnea", Language: "Idioma", "Live challenges": "DesafÃos en vivo", "Live games": "Partidas en vivo", @@ -97,6 +97,7 @@ export const translations = { "Show possible moves?": "¿Mostrar posibles movimientos?", "Show solution": "Mostrar la solución", Solution: "Solución", + Stop: "Interrupción", "Stop game": "Terminar la partida", Subject: "Asunto", "Terminate game?": "¿Terminar la partida?", @@ -111,6 +112,7 @@ export const translations = { Versus: "Contra", White: "Blancas", "White to move": "Juegan las blancas", + "White surrender": "Las blancas abandonan", "White win": "Las blancas gagnan", "Who's there?": "¿Quién está ahÃ?", With: "Con", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 0d776b4e..e02cf5e9 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -11,6 +11,7 @@ export const translations = { "Back to list": "Retour à la liste", Black: "Noirs", "Black to move": "Trait aux noirs", + "Black surrender": "Les noirs abandonnent", "Black win": "Les noirs gagnent", "Board colors": "Couleurs de l'échiquier", "Board size": "Taille de l'échiquier", @@ -25,7 +26,8 @@ export const translations = { Contact: "Contact", "Correspondance challenges": "Défis par correspondance", "Correspondance games": "Parties par correspondance", - "Database error:": "Erreur de base de données :", + "Database error: stop private browsing, or update your browser": + "Erreur de base de données : arrêtez la navigation privée, ou mettez à jour votre navigateur", Delete: "Supprimer", Download: "Télécharger", Draw: "Nulle", @@ -35,17 +37,14 @@ export const translations = { Email: "Email", "Email sent!": "Email envoyé !", "Empty message": "Message vide", - "Error while loading database:": - "Erreur lors du chargement de la base de données :", "Example game": "Partie exemple", - "Game retrieval failed:": "Ãchec de la récupération de la partie :", - "Game removal failed:": "Ãchec de la suppresion de la partie :", Go: "Go", green: "vert", Hall: "Salon", "Highlight last move and checks?": "Mettre en valeur le dernier coup et les échecs ?", Instructions: "Instructions", + "is not online": "n'est pas en ligne", Language: "Langue", "Live challenges": "Défis en direct", "Live games": "Parties en direct", @@ -100,6 +99,7 @@ export const translations = { "Show possible moves?": "Montrer les coups possibles ?", "Show solution": "Montrer la solution", Solution: "Solution", + Stop: "Arrêt", "Stop game": "Arrêter la partie", Subject: "Sujet", "Terminate game?": "Stopper la partie ?", @@ -114,6 +114,7 @@ export const translations = { Versus: "Contre", White: "Blancs", "White to move": "Trait aux blancs", + "White surrender": "Les blancs abandonnent", "White win": "Les blancs gagnent", "Who's there?": "Qui est là ?", With: "Avec", diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index 6e9fc818..cc7dca5b 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -24,25 +24,18 @@ function dbOperation(callback) { let DBOpenRequest = window.indexedDB.open("vchess", 4); DBOpenRequest.onerror = function(event) { - alert(store.state.tr["Database error:"] + " " + event.target.errorCode); + alert(store.state.tr["Database error: stop private browsing, or update your browser"]); + callback("error",null); }; DBOpenRequest.onsuccess = function() { db = DBOpenRequest.result; - callback(db); + callback(null,db); db.close(); }; DBOpenRequest.onupgradeneeded = function(event) { let db = event.target.result; - db.onerror = function(event) { - alert( - store.state.tr["Error while loading database:"] + - " " + - event.target.errorCode - ); - }; - // Create objectStore for vchess->games let objectStore = db.createObjectStore("games", { keyPath: "id" }); objectStore.createIndex("score", "score"); //to search by game result }; @@ -51,19 +44,15 @@ function dbOperation(callback) { export const GameStorage = { // Optional callback to get error status add: function(game, callback) { - dbOperation(db => { - let transaction = db.transaction("games", "readwrite"); - if (callback) { - transaction.oncomplete = function() { - callback({}); //everything's fine - }; - transaction.onerror = function() { - callback({ - errmsg: - store.state.tr["Game retrieval failed:"] + " " + transaction.error - }); - }; + dbOperation((err,db) => { + if (err) { + callback("error"); + return; } + let transaction = db.transaction("games", "readwrite"); + transaction.oncomplete = function() { + callback(); //everything's fine + }; let objectStore = transaction.objectStore("games"); objectStore.add(game); }); @@ -88,17 +77,20 @@ export const GameStorage = { }); } else { // live - dbOperation(db => { + dbOperation((err,db) => { let objectStore = db .transaction("games", "readwrite") .objectStore("games"); objectStore.get(gameId).onsuccess = function(event) { - const game = event.target.result; - Object.keys(obj).forEach(k => { - if (k == "move") game.moves.push(obj[k]); - else game[k] = obj[k]; - }); - objectStore.put(game); //save updated data + // Ignoring error silently: shouldn't happen now. TODO? + if (event.target.result) { + const game = event.target.result; + Object.keys(obj).forEach(k => { + if (k == "move") game.moves.push(obj[k]); + else game[k] = obj[k]; + }); + objectStore.put(game); //save updated data + } }; }); } @@ -106,7 +98,7 @@ export const GameStorage = { // Retrieve all local games (running, completed, imported...) getAll: function(callback) { - dbOperation(db => { + dbOperation((err,db) => { let objectStore = db.transaction("games").objectStore("games"); let games = []; objectStore.openCursor().onsuccess = function(event) { @@ -132,42 +124,29 @@ export const GameStorage = { }); callback(game); }); - } //local game + } else { - dbOperation(db => { + // Local game + dbOperation((err,db) => { let objectStore = db.transaction("games").objectStore("games"); objectStore.get(gameId).onsuccess = function(event) { - callback(event.target.result); + if (event.target.result) + callback(event.target.result); }; }); } }, - getCurrent: function(callback) { - dbOperation(db => { - let objectStore = db.transaction("games").objectStore("games"); - objectStore.get("*").onsuccess = function(event) { - callback(event.target.result); - }; - }); - }, - // Delete a game in indexedDB remove: function(gameId, callback) { - dbOperation(db => { - let transaction = db.transaction(["games"], "readwrite"); - if (callback) { + dbOperation((err,db) => { + if (!err) { + let transaction = db.transaction(["games"], "readwrite"); transaction.oncomplete = function() { callback({}); //everything's fine }; - transaction.onerror = function() { - callback({ - errmsg: - store.state.tr["Game removal failed:"] + " " + transaction.error - }); - }; + transaction.objectStore("games").delete(gameId); } - transaction.objectStore("games").delete(gameId); }); } }; diff --git a/client/src/variants/Dark.js b/client/src/variants/Dark.js index cdacf9d2..d6515efd 100644 --- a/client/src/variants/Dark.js +++ b/client/src/variants/Dark.js @@ -4,7 +4,7 @@ import { randInt } from "@/utils/alea"; export const VariantRules = class DarkRules extends ChessRules { // Analyse in Dark mode makes no sense - static get CanAnalyse() { + static get CanAnalyze() { return false; } diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 9a971006..2108493c 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -56,6 +56,7 @@ main | {{ game.players[1].name || "@nonymous" }} span.time(v-if="game.score=='*'") {{ virtualClocks[1] }} BaseGame( + ref="basegame" :game="game" :vr="vr" @newmove="processMove" @@ -138,10 +139,10 @@ export default { // Socket init required before loading remote game: const socketInit = callback => { if (!!this.conn && this.conn.readyState == 1) - //1 == OPEN state + // 1 == OPEN state callback(); - //socket not ready yet (initial loading) else { + // Socket not ready yet (initial loading) // NOTE: it's important to call callback without arguments, // otherwise first arg is Websocket object and loadGame fails. this.conn.onopen = () => { @@ -150,10 +151,10 @@ export default { } }; if (!this.gameRef.rid) - //game stored locally or on server + // Game stored locally or on server this.loadGame(null, () => socketInit(this.roomInit)); - //game stored remotely: need socket to retrieve it else { + // Game stored remotely: need socket to retrieve it // NOTE: the callback "roomInit" will be lost, so we don't provide it. // --> It will be given when receiving "fullgame" socket event. // A more general approach would be to store it somewhere. @@ -333,14 +334,16 @@ export default { if (this.game.type == "live" && !!this.game.mycolor) GameStorage.update(this.gameRef.id, { drawOffer: "" }); } - this.$set(this.game, "moveToPlay", move); + this.$refs["basegame"].play(move, "received"); break; } case "resign": - this.gameOver(data.side == "b" ? "1-0" : "0-1", "Resign"); + const score = data.side == "b" ? "1-0" : "0-1"; + const side = data.side == "w" ? "White" : "Black"; + this.gameOver(score, side + " surrender"); break; case "abort": - this.gameOver("?", "Abort"); + this.gameOver("?", "Stop"); break; case "draw": this.gameOver("1/2", data.data); @@ -368,9 +371,7 @@ export default { const L = this.game.moves.length; if (data.movesCount > L) { // Just got last move from him - this.$set( - this.game, - "moveToPlay", + this.$refs["basegame"].play( Object.assign({ initime: data.initime }, data.lastMove) ); } @@ -404,14 +405,16 @@ export default { }, abortGame: function() { if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) return; - this.gameOver("?", "Abort"); + this.gameOver("?", "Stop"); this.send("abort"); }, resign: function() { if (!this.game.mycolor || !confirm(this.st.tr["Resign the game?"])) return; this.send("resign", { data: this.game.mycolor }); - this.gameOver(this.game.mycolor == "w" ? "0-1" : "1-0", "Resign"); + const score = this.game.mycolor == "w" ? "0-1" : "1-0"; + const side = this.game.mycolor == "w" ? "White" : "Black"; + this.gameOver(score, side + " surrender"); }, // 3 cases for loading a game: // - from indexedDB (running or completed live game I play) @@ -437,7 +440,7 @@ export default { game.players[0] ]; } - // corr game: needs to compute the clocks + initime + // corr game: need to compute the clocks + initime // NOTE: clocks in seconds, initime in milliseconds game.clocks = [tc.mainTime, tc.mainTime]; game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of @@ -456,31 +459,17 @@ export default { } if (L >= 1) game.initime[L % 2] = game.moves[L - 1].played; } - const reformattedMoves = game.moves.map(m => { - const s = m.squares; - return { - appear: s.appear, - vanish: s.vanish, - start: s.start, - end: s.end - }; - }); // Sort chat messages from newest to oldest game.chats.sort((c1, c2) => { return c2.added - c1.added; }); if (myIdx >= 0 && game.chats.length > 0) { - // TODO: group multi-moves into an array, to deduce color from index - // and not need this (also repeated in BaseGame::re_setVariables()) - let vr_tmp = new V(game.fenStart); //vr is already at end of game - for (let i = 0; i < reformattedMoves.length; i++) { - game.moves[i].color = vr_tmp.turn; - vr_tmp.play(reformattedMoves[i]); - } - // Blue background on chat button if last chat message arrived after my last move. + // Did a chat message arrive after my last move? + let vr_tmp = new V(game.fen); //start from last position let dtLastMove = 0; for (let midx = game.moves.length - 1; midx >= 0; midx--) { - if (game.moves[midx].color == mycolor) { + vr_tmp.undo(game.moves[midx]); + if (vr_tmp.turn == mycolor) { dtLastMove = game.moves[midx].played; break; } @@ -489,10 +478,10 @@ export default { document.getElementById("chatBtn").classList.add("somethingnew"); } // Now that we used idx and played, re-format moves as for live games - game.moves = reformattedMoves; + game.moves = game.moves.map(m => m.squares); } if (gtype == "live" && game.clocks[0] < 0) { - //game unstarted + // Game is unstarted game.clocks = [tc.mainTime, tc.mainTime]; if (game.score == "*") { game.initime[0] = Date.now(); @@ -507,11 +496,11 @@ export default { } if (game.drawOffer) { if (game.drawOffer == "t") - //three repetitions + // Three repetitions this.drawOffer = "threerep"; else { + // Draw offered by any of the players: if (myIdx < 0) this.drawOffer = "received"; - //by any of the players else { // I play in this game: if ( @@ -519,13 +508,10 @@ export default { (game.drawOffer == "b" && myIdx == 1) ) this.drawOffer = "sent"; - //all other cases else this.drawOffer = "received"; } } } - if (game.scoreMsg) game.scoreMsg = this.st.tr[game.scoreMsg]; //stored in english - delete game["moveToPlay"]; //in case of! this.game = Object.assign( {}, game, @@ -571,6 +557,7 @@ export default { this.send("askfullgame", { target: this.gameRef.rid }); } else { // Local or corr game + // NOTE: afterRetrieval() is never called if game not found GameStorage.get(this.gameRef.id, afterRetrieval); } }, @@ -581,6 +568,7 @@ export default { return; } const currentTurn = this.vr.turn; + const currentMovesCount = this.game.moves.length; const colorIdx = ["w", "b"].indexOf(currentTurn); let countdown = this.game.clocks[colorIdx] - @@ -593,13 +581,13 @@ export default { let clockUpdate = setInterval(() => { if ( countdown < 0 || - this.vr.turn != currentTurn || + this.game.moves.length > currentMovesCount || this.game.score != "*" ) { clearInterval(clockUpdate); if (countdown < 0) this.gameOver( - this.vr.turn == "w" ? "0-1" : "1-0", + currentTurn == "w" ? "0-1" : "1-0", this.st.tr["Time"] ); } else @@ -610,7 +598,7 @@ export default { ); }, 1000); }, - // Post-process a move (which was just played in BaseGame) + // Post-process a (potentially partial) move (which was just played in BaseGame) processMove: function(move) { if (this.game.type == "corr" && move.color == this.game.mycolor) { if ( @@ -622,7 +610,7 @@ export default { this.st.tr["Are you sure?"] ) ) { - this.$set(this.game, "moveToUndo", move); + this.$refs["basegame"].undo(move); return; } } @@ -631,7 +619,7 @@ export default { // https://stackoverflow.com/a/38750895 if (this.game.mycolor) { const allowed_fields = ["appear", "vanish", "start", "end"]; - // NOTE: 'var' to see this variable outside this block + // NOTE: 'var' to see that variable outside this block var filtered_move = Object.keys(move) .filter(key => allowed_fields.includes(key)) .reduce((obj, key) => { @@ -665,7 +653,8 @@ export default { this.game.clocks[colorIdx] += addTime; // move.initime is set only when I receive a "lastate" move from opponent this.game.initime[nextIdx] = move.initime || Date.now(); - this.re_setClocks(); + //if (colorIdx != nextIdx) + this.re_setClocks(); // If repetition detected, consider that a draw offer was received: const fenObj = V.ParseFen(move.fen); let repIdx = fenObj.position + "_" + fenObj.turn; @@ -676,7 +665,7 @@ 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.mycolor && + this.game.mycolor && (this.game.type == "live" || move.color == this.game.mycolor) ) { let drawCode = ""; @@ -699,10 +688,12 @@ export default { played: Date.now(), idx: this.game.moves.length - 1 }, - drawOffer: drawCode || "n" //"n" for "None" to force reset (otherwise it's ignored) + // Code "n" for "None" to force reset (otherwise it's ignored) + drawOffer: drawCode || "n" }); - } //live + } else { + // Live game: GameStorage.update(this.gameRef.id, { fen: move.fen, move: filtered_move, @@ -725,14 +716,12 @@ export default { }, gameOver: function(score, scoreMsg) { this.game.score = score; - this.game.scoreMsg = this.st.tr[ - scoreMsg ? scoreMsg : getScoreMessage(score) - ]; + this.$set(this.game, "scoreMsg", scoreMsg || getScoreMessage(score)); const myIdx = this.game.players.findIndex(p => { return p.sid == this.st.user.sid || p.uid == this.st.user.id; }); if (myIdx >= 0) { - //OK, I play in this game + // OK, I play in this game GameStorage.update(this.gameRef.id, { score: score, scoreMsg: scoreMsg diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 68d3b48b..6cc58e97 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -28,9 +28,9 @@ main fieldset label(for="cadence") {{ st.tr["Cadence"] }} * div#predefinedCadences - button 3+2 - button 5+3 - button 15+5 + button(type="button") 3+2 + button(type="button") 5+3 + button(type="button") 15+5 input#cadence( type="text" v-model="newchallenge.cadence" @@ -627,11 +627,24 @@ export default { }, // Challenge lifecycle: newChallenge: async function() { + if (this.newchallenge.cadence.match(/^[0-9]+$/)) + this.newchallenge.cadence += "+0"; //assume minutes, no increment + const ctype = this.classifyObject(this.newchallenge); + // TODO: cadence still unchecked so ctype could be wrong... let error = ""; - if (this.newchallenge.vid == "") + if (!this.newchallenge.vid) error = this.st.tr["Please select a variant"]; - else if (!!this.newchallenge.to && this.newchallenge.to == this.st.user.name) - error = this.st.tr["Self-challenge is forbidden"]; + else if (ctype == "corr" && this.st.user.id <= 0) + error = this.st.tr["Please log in to play correspondance games"]; + else if (this.newchallenge.to) { + if (this.newchallenge.to == this.st.user.name) + error = this.st.tr["Self-challenge is forbidden"]; + else if ( + ctype == "live" && + Object.values(this.people).every(p => p.name != this.newchallenge.to) + ) + error = this.newchallenge.to + " " + this.st.tr["is not online"]; + } if (error) { alert(error); return; @@ -639,12 +652,7 @@ export default { const vname = this.getVname(this.newchallenge.vid); const vModule = await import("@/variants/" + vname + ".js"); window.V = vModule.VariantRules; - if (this.newchallenge.cadence.match(/^[0-9]+$/)) - this.newchallenge.cadence += "+0"; //assume minutes, no increment - const ctype = this.classifyObject(this.newchallenge); error = checkChallenge(this.newchallenge); - if (!error && ctype == "corr" && this.st.user.id <= 0) - error = this.st.tr["Please log in to play correspondance games"]; if (error) { alert(error); return; @@ -789,10 +797,14 @@ export default { initime: [0, 0], //initialized later score: "*" }); - GameStorage.add(game); - if (this.st.settings.sound >= 1) - new Audio("/sounds/newgame.mp3").play().catch(() => {}); - this.$router.push("/game/" + gameInfo.id); + GameStorage.add(game, (err) => { + // If an error occurred, game is not added: abort + if (!err) { + if (this.st.settings.sound >= 1) + new Audio("/sounds/newgame.mp3").play().catch(() => {}); + this.$router.push("/game/" + gameInfo.id); + } + }); } } }; diff --git a/server/routes/challenges.js b/server/routes/challenges.js index 5d0068df..4bbce8e2 100644 --- a/server/routes/challenges.js +++ b/server/routes/challenges.js @@ -35,7 +35,7 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => { { UserModel.getOne("name", challenge.to, (err,user) => { if (!!err || !user) - return res.json(err | {errmsg: "Typo in player name"}); + return res.json(err || {errmsg: "Typo in player name"}); challenge.to = user.id; //ready now to insert challenge insertChallenge(); if (user.notify) diff --git a/server/sockets.js b/server/sockets.js index 1cc47aeb..42a8840f 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -153,7 +153,7 @@ module.exports = function(wss) { { const pg = obj.page || page; //required for askidentity and askgame // In cas askfullgame to wrong SID for example, would crash: - if (!!clients[pg][obj.target]) + if (clients[pg] && clients[pg][obj.target]) { const tmpIds = Object.keys(clients[pg][obj.target]); if (obj.target == sid) //targetting myself @@ -207,7 +207,10 @@ module.exports = function(wss) { case "lastate": { const pg = obj.target[2] || page; //required for identity and game - send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data}); + // NOTE: if in game we ask identity to opponent still in Hall, + // but leaving Hall, clients[pg] or clients[pg][target] could be ndefined + if (clients[pg] && clients[pg][obj.target[0]]) + send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data}); break; } }