From: Benjamin Auder <benjamin.auder@somewhere> Date: Sat, 7 Mar 2020 12:31:42 +0000 (+0100) Subject: Experimental update: preview corr move + allow deletion of any game X-Git-Url: https://git.auder.net/variants/current/doc/css/app_dev.php/%7B%7B%20pkg.url%20%7D%7D?a=commitdiff_plain;h=aae89b49a846b2c101d74db7dff9151392d6db34;p=vchess.git Experimental update: preview corr move + allow deletion of any game --- diff --git a/TODO b/TODO index 65bcbd11..2f909766 100644 --- a/TODO +++ b/TODO @@ -41,6 +41,7 @@ Take(a)n(d)make : if capture a piece, take its power for the last of the turn an If a pawn taken: direction of the capturer. + Maxima, Interweave, Roccoco, Dynamo, Synchrone +Synchrone Chess: allow to anticipate en-passant capture as well :) S-chess https://en.wikipedia.org/wiki/Seirawan_chess diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index ac8b3236..e7977eb2 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -87,9 +87,10 @@ export default { }; }, watch: { - // game initial FEN changes when a new game starts - "game.fenStart": function() { - this.re_setVariables(); + // game initial FEN changes when a new game starts. + // NOTE: when game ID change on Game page, fenStart may be temporarily undefined + "game.fenStart": function(fenStart) { + if (!!fenStart) this.re_setVariables(); }, }, computed: { @@ -256,11 +257,7 @@ export default { }, showEndgameMsg: function(message) { this.endgameMessage = message; - let modalBox = document.getElementById("modalEog"); - modalBox.checked = true; - setTimeout(() => { - modalBox.checked = false; - }, 2000); + document.getElementById("modalEog").checked = true; }, // Animate an elementary move animateMove: function(move, callback) { diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue index aa361fb3..0a8bd8ce 100644 --- a/client/src/components/GameList.vue +++ b/client/src/components/GameList.vue @@ -28,6 +28,7 @@ div <script> import { store } from "@/store"; import { GameStorage } from "@/utils/gameStorage"; +import { ajax } from "@/utils/ajax"; export default { name: "my-game-list", props: ["games", "showBoth"], @@ -137,19 +138,42 @@ export default { }, deleteGame: function(game, e) { if ( - game.score != "*" && + // My game ? game.players.some(p => p.sid == this.st.user.sid || p.uid == this.st.user.id ) ) { - if (confirm(this.st.tr["Remove game?"])) { - GameStorage.remove( - game.id, - () => { - this.$set(this.deleted, game.id, true); - } - ); + const message = + game.score != "*" + ? "Remove game?" + : "Abort and remove game?"; + if (confirm(this.st.tr[message])) { + const afterDelete = () => { + if (game.score == "*") this.$emit("abort", game); + this.$set(this.deleted, game.id, true); + }; + if (game.type == "live") + // Effectively remove game: + GameStorage.remove(game.id, afterDelete); + else { + const mySide = + game.players[0].uid == this.st.user.id + ? "White" + : "Black"; + game["deletedBy" + mySide] = true; + // Mark the game for deletion on server + // If both people mark it, it is deleted + ajax( + "/games", + "PUT", + { + gid: game.id, + newObj: { removeFlag: true } + }, + afterDelete + ); + } } e.stopPropagation(); } diff --git a/client/src/components/Settings.vue b/client/src/components/Settings.vue index 6adf9b0b..7e90b0f2 100644 --- a/client/src/components/Settings.vue +++ b/client/src/components/Settings.vue @@ -52,6 +52,13 @@ div type="checkbox" v-model="st.settings.sound" ) + fieldset + label(for="setGotonext") + | {{ st.tr["Show next game after move?"] }} + input#setGotonext( + type="checkbox" + v-model="st.settings.gotonext" + ) fieldset label(for="setRandomness") {{ st.tr["Randomness against computer"] }} select#setRandomness(v-model="st.settings.randomness") diff --git a/client/src/store.js b/client/src/store.js index 705f473c..c98657ef 100644 --- a/client/src/store.js +++ b/client/src/store.js @@ -77,6 +77,7 @@ export const store = { sound: getItemDefaultTrue("sound"), hints: getItemDefaultTrue("hints"), highlight: getItemDefaultTrue("highlight"), + gotonext: getItemDefaultTrue("gotonext"), randomness: parseInt(localStorage.getItem("randomness")) }; if (isNaN(this.state.settings.randomness)) diff --git a/client/src/translations/en.js b/client/src/translations/en.js index ca7a0965..5b90af70 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -1,5 +1,6 @@ export const translations = { Abort: "Abort", + "Abort and remove game?": "Abort and Remove game?", About: "About", "Accept draw?": "Accept draw?", "Accept challenge?": "Accept challenge?", @@ -11,7 +12,6 @@ export const translations = { "Are you sure?": "Are you sure?", "Asymmetric random": "Asymmetric random", "Authentication successful!": "Authentication successful!", - "Back to Hall in 3 seconds...": "Back to Hall in 3 seconds...", "Back to list": "Back to list", "Black to move": "Black to move", "Black surrender": "Black surrender", @@ -115,6 +115,7 @@ export const translations = { "Self-challenge is forbidden": "Self-challenge is forbidden", "Send challenge": "Send challenge", Settings: "Settings", + "Show next game after move?": "Show next game after move?", "Show possible moves?": "Show possible moves?", "Show solution": "Show solution", Solution: "Solution", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index e72a36db..f647a9b2 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -1,5 +1,6 @@ export const translations = { Abort: "Terminar", + "Abort and remove game?": "¿Terminar y eliminar la partida?", About: "Acerca de", "Accept draw?": "¿Acceptar tablas?", "Accept challenge?": "¿Acceptar el desafÃo?", @@ -11,7 +12,6 @@ export const translations = { "Are you sure?": "¿Está usted seguro?", "Asymmetric random": "Aleatorio asimétrico", "Authentication successful!": "¡Autenticación exitosa!", - "Back to Hall in 3 seconds...": "Regreso al salón en 3 segundos...", "Back to list": "Volver a la lista", "Black to move": "Juegan las negras", "Black surrender": "Las negras abandonan", @@ -115,6 +115,7 @@ export const translations = { "Self-challenge is forbidden": "Auto desafÃo está prohibido", "Send challenge": "Enviar desafÃo", Settings: "Configuraciones", + "Show next game after move?": "¿Mostrar la siguiente partida después de una jugada?", "Show possible moves?": "¿Mostrar posibles movimientos?", "Show solution": "Mostrar la solución", Solution: "Solución", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index f777c3e8..e7121ca3 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -1,5 +1,6 @@ export const translations = { Abort: "Arrêter", + "Abort and remove game?": "Arrêter et supprimer la partie ?", About: "à propos", "Accept draw?": "Accepter la nulle ?", "Accept challenge?": "Accepter le défi ?", @@ -11,7 +12,6 @@ export const translations = { "Authentication successful!": "Authentification réussie !", "Are you sure?": "Ãtes vous sûr?", "Asymmetric random": "Aléatoire asymétrique", - "Back to Hall in 3 seconds...": "Retour au Hall dans 3 secondes...", "Back to list": "Retour à la liste", "Black to move": "Trait aux noirs", "Black surrender": "Les noirs abandonnent", @@ -115,10 +115,11 @@ export const translations = { "Self-challenge is forbidden": "Interdit de s'auto-défier", "Send challenge": "Envoyer défi", Settings: "Réglages", + "Show next game after move?": "Montrer la partie suivante après un coup ?", "Show possible moves?": "Montrer les coups possibles ?", "Show solution": "Montrer la solution", Solution: "Solution", - "Sound alert when game starts?": "Alerte sonore quand une partie démarre?", + "Sound alert when game starts?": "Alerte sonore quand une partie démarre ?", Stop: "Arrêt", "Stop game": "Arrêter la partie", Subject: "Sujet", diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index 480c73ba..5c094dc9 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -15,7 +15,6 @@ // score: string (several options; '*' == running), // } -import { ajax } from "@/utils/ajax"; import { store } from "@/store"; function dbOperation(callback) { @@ -57,44 +56,27 @@ export const GameStorage = { }); }, - // TODO: also option to takeback a move ? // obj: chat, move, fen, clocks, score[Msg], initime, ... update: function(gameId, obj) { - if (Number.isInteger(gameId) || !isNaN(parseInt(gameId))) { - // corr: only move, fen and score - ajax("/games", "PUT", { - gid: gameId, - newObj: { - // Some fields may be undefined: - chat: obj.chat, - move: obj.move, - fen: obj.fen, - score: obj.score, - scoreMsg: obj.scoreMsg, - drawOffer: obj.drawOffer + // live + dbOperation((err,db) => { + let objectStore = db + .transaction("games", "readwrite") + .objectStore("games"); + objectStore.get(gameId).onsuccess = function(event) { + // Ignoring error silently: shouldn't happen now. TODO? + if (event.target.result) { + let game = event.target.result; + // Hidden tabs are delayed, to prevent multi-updates: + if (obj.moveIdx < game.moves.length) return; + Object.keys(obj).forEach(k => { + if (k == "move") game.moves.push(obj[k]); + else game[k] = obj[k]; + }); + objectStore.put(game); //save updated data } - }); - } else { - // live - dbOperation((err,db) => { - let objectStore = db - .transaction("games", "readwrite") - .objectStore("games"); - objectStore.get(gameId).onsuccess = function(event) { - // Ignoring error silently: shouldn't happen now. TODO? - if (event.target.result) { - let game = event.target.result; - // Hidden tabs are delayed, to prevent multi-updates: - if (obj.moveIdx < game.moves.length) return; - Object.keys(obj).forEach(k => { - if (k == "move") game.moves.push(obj[k]); - else game[k] = obj[k]; - }); - objectStore.put(game); //save updated data - } - }; - }); - } + }; + }); }, // Retrieve all local games (running, completed, imported...) @@ -124,26 +106,14 @@ export const GameStorage = { // Retrieve any game from its identifiers (locally or on server) // NOTE: need callback because result is obtained asynchronously get: function(gameId, callback) { - // corr games identifiers are integers - if (Number.isInteger(gameId) || !isNaN(parseInt(gameId))) { - ajax("/games", "GET", { gid: gameId }, res => { - let game = res.game; - game.moves.forEach(m => { - m.squares = JSON.parse(m.squares); - }); - callback(game); - }); - } - else { - // Local game - dbOperation((err,db) => { - let objectStore = db.transaction("games").objectStore("games"); - objectStore.get(gameId).onsuccess = function(event) { - if (event.target.result) - callback(event.target.result); - }; - }); - } + // Local game + dbOperation((err,db) => { + let objectStore = db.transaction("games").objectStore("games"); + objectStore.get(gameId).onsuccess = function(event) { + if (event.target.result) + callback(event.target.result); + }; + }); }, // Delete a game in indexedDB diff --git a/client/src/views/Auth.vue b/client/src/views/Auth.vue index 7d682a0e..0c5afe9c 100644 --- a/client/src/views/Auth.vue +++ b/client/src/views/Auth.vue @@ -4,7 +4,6 @@ main .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 div(v-if="authOk") p {{ st.tr["Authentication successful!"] }} - p {{ st.tr["Back to Hall in 3 seconds..."] }} </template> <script> @@ -31,9 +30,6 @@ export default { this.st.user.notify = res.notify; localStorage["myname"] = res.name; localStorage["myid"] = res.id; - setTimeout(() => { - this.$router.replace("/"); - }, 3000); } ); } diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 086becb8..e0ae7e91 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -20,6 +20,7 @@ main span.anonymous(v-if="Object.values(people).some(p => !p.name && p.id === 0)") | + @nonymous Chat( + ref="chatcomp" :players="game.players" :pastChats="game.chats" :newChat="newChat" @@ -85,7 +86,8 @@ main ) span.time-left {{ virtualClocks[0][0] }} span.time-separator(v-if="!!virtualClocks[0][1]") : - span.time-right(v-if="!!virtualClocks[0][1]") {{ virtualClocks[0][1] }} + span.time-right(v-if="!!virtualClocks[0][1]") + | {{ virtualClocks[0][1] }} span.split-names - span.name(:class="{connected: isConnected(1)}") | {{ game.players[1].name || "@nonymous" }} @@ -95,7 +97,8 @@ main ) span.time-left {{ virtualClocks[1][0] }} span.time-separator(v-if="!!virtualClocks[1][1]") : - span.time-right(v-if="!!virtualClocks[1][1]") {{ virtualClocks[1][1] }} + span.time-right(v-if="!!virtualClocks[1][1]") + | {{ virtualClocks[1][1] }} BaseGame( ref="basegame" :game="game" @@ -125,7 +128,6 @@ export default { BaseGame, Chat }, - // gameRef: to find the game in (potentially remote) storage data: function() { return { st: store.state, @@ -134,14 +136,10 @@ export default { id: "", rid: "" }, - game: { - // Passed to BaseGame - players: [{ name: "" }, { name: "" }], - chats: [], - rendered: false - }, nextIds: [], - virtualClocks: [[0,0], [0,0]], //initialized with true game.clocks + game: {}, //passed to BaseGame + // virtualClocks will be initialized from true game.clocks + virtualClocks: [], vr: null, //"variant rules" object initialized from FEN drawOffer: "", people: {}, //players + observers @@ -161,61 +159,35 @@ export default { // If newmove got no pingback, send again: opponentGotMove: false, connexionString: "", + // Intervals from setInterval(): + // TODO: limit them to e.g. 3 retries ?! + askIfPeerConnected: null, + askLastate: null, + retrySendmove: null, + clockUpdate: null, // Related to (killing of) self multi-connects: newConnect: {}, killed: {} }; }, watch: { - $route: function(to) { - this.gameRef.id = to.params["id"]; - this.gameRef.rid = to.query["rid"]; - this.loadGame(); + $route: function(to, from) { + if (from.params["id"] != to.params["id"]) { + // Change everything: + this.cleanBeforeDestroy(); + this.atCreation(); + } else { + // Same game ID + this.gameRef.id = to.params["id"]; + this.gameRef.rid = to.query["rid"]; + this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); + this.loadGame(); + } } }, // NOTE: some redundant code with Hall.vue (mostly related to people array) created: function() { - // Always add myself to players' list - const my = this.st.user; - this.$set(this.people, my.sid, { id: my.id, name: my.name }); - this.gameRef.id = this.$route.params["id"]; - // rid = remote ID to find an observed live game, - // next = next corr games IDs to navigate faster - // (Both might be undefined) - this.gameRef.rid = this.$route.query["rid"]; - this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); - // Initialize connection - this.connexionString = - params.socketUrl + - "/?sid=" + - this.st.user.sid + - "&tmpId=" + - getRandString() + - "&page=" + - // 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; - // Socket init required before loading remote game: - const socketInit = callback => { - if (!!this.conn && this.conn.readyState == 1) - // 1 == OPEN state - callback(); - 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 = () => callback(); - }; - if (!this.gameRef.rid) - // Game stored locally or on server - this.loadGame(null, () => socketInit(this.roomInit)); - 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. - socketInit(this.loadGame); + this.atCreation(); }, mounted: function() { document @@ -229,9 +201,89 @@ export default { } }, beforeDestroy: function() { - this.send("disconnect"); + this.cleanBeforeDestroy(); }, methods: { + atCreation: function() { + // 0] (Re)Set variables + this.gameRef.id = this.$route.params["id"]; + // rid = remote ID to find an observed live game, + // next = next corr games IDs to navigate faster + // (Both might be undefined) + this.gameRef.rid = this.$route.query["rid"]; + this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); + // Always add myself to players' list + const my = this.st.user; + this.$set(this.people, my.sid, { id: my.id, name: my.name }); + this.game = { + players: [{ name: "" }, { name: "" }], + chats: [], + rendered: false + }; + let chatComp = this.$refs["chatcomp"]; + if (!!chatComp) chatComp.chats = []; + this.virtualClocks = [[0,0], [0,0]]; + this.vr = null; + this.drawOffer = ""; + this.onMygames = []; + this.lastate = undefined; + this.newChat = ""; + this.roomInitialized = false; + this.askGameTime = 0; + this.gameIsLoading = false; + this.gotLastate = false; + this.gotMoveIdx = -1; + this.opponentGotMove = false; + this.askIfPeerConnected = null; + this.askLastate = null; + this.retrySendmove = null; + this.clockUpdate = null; + this.newConnect = {}; + this.killed = {}; + // 1] Initialize connection + this.connexionString = + params.socketUrl + + "/?sid=" + + this.st.user.sid + + "&tmpId=" + + getRandString() + + "&page=" + + // 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; + // Socket init required before loading remote game: + const socketInit = callback => { + if (!!this.conn && this.conn.readyState == 1) + // 1 == OPEN state + callback(); + 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 = () => callback(); + }; + if (!this.gameRef.rid) + // Game stored locally or on server + this.loadGame(null, () => socketInit(this.roomInit)); + 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. + socketInit(this.loadGame); + }, + cleanBeforeDestroy: function() { + if (!!this.askIfPeerConnected) + clearInterval(this.askIfPeerConnected); + if (!!this.askLastate) + clearInterval(this.askLastate); + if (!!this.retrySendmove) + clearInterval(this.retrySendmove); + if (!!this.clockUpdate) + clearInterval(this.clockUpdate); + this.send("disconnect"); + }, roomInit: function() { if (!this.roomInitialized) { // Notify the room only now that I connected, because @@ -275,7 +327,7 @@ export default { this.send("newchat", { data: chat }); // NOTE: anonymous chats in corr games are not stored on server (TODO?) if (this.game.type == "corr" && this.st.user.id > 0) - GameStorage.update(this.gameRef.id, { chat: chat }); + this.updateCorrGame({ chat: chat }); }, clearChat: function() { // Nothing more to do if game is live (chats not recorded) @@ -298,6 +350,7 @@ export default { this.send("turnchange", { target: sid, yourTurn: yourTurn }); }, showNextGame: function() { + if (this.nextIds.length == 0) return; // Did I play in current game? If not, add it to nextIds list if (this.game.score == "*" && this.vr.turn == this.game.mycolor) this.nextIds.unshift(this.game.id); @@ -311,19 +364,28 @@ export default { }, askGameAgain: function() { this.gameIsLoading = true; + const currentUrl = document.location.href; const doAskGame = () => { + if (currentUrl != document.location.href) return; //page change if (!this.gameRef.rid) // This is my game: just reload. this.loadGame(); else { // Just ask fullgame again (once!), this is much simpler. // If this fails, the user could just reload page :/ - let self = this; - (function askIfPeerConnected() { - if (!!self.people[self.gameRef.rid]) - self.send("askfullgame", { target: self.gameRef.rid }); - else setTimeout(askIfPeerConnected, 1000); - })(); + this.send("askfullgame", { target: this.gameRef.rid }); + this.askIfPeerConnected = setInterval( + () => { + if ( + !!this.people[this.gameRef.rid] && + currentUrl != document.location.href + ) { + this.send("askfullgame", { target: this.gameRef.rid }); + clearInterval(this.askIfPeerConnected); + } + }, + 1000 + ); } }; // Delay of at least 2s between two game requests @@ -407,18 +469,17 @@ export default { this.game.score == "*" && this.game.players.some(p => p.sid == user.sid) ) { - let self = this; - (function askLastate() { - self.send("asklastate", { target: user.sid }); - setTimeout( - () => { - // Ask until we got a reply (or opponent disconnect): - if (!self.gotLastate && !!self.people[user.sid]) - askLastate(); - }, - 1000 - ); - })(); + this.send("asklastate", { target: user.sid }); + this.askLastate = setInterval( + () => { + // Ask until we got a reply (or opponent disconnect): + if (!this.gotLastate && !!this.people[user.sid]) + this.send("asklastate", { target: user.sid }); + else + clearInterval(this.askLastate); + }, + 1000 + ); } } break; @@ -576,6 +637,16 @@ export default { this.conn.addEventListener("message", this.socketMessageListener); this.conn.addEventListener("close", this.socketCloseListener); }, + updateCorrGame: function(obj) { + ajax( + "/games", + "PUT", + { + gid: this.gameRef.id, + newObj: obj + } + ); + }, // lastate was received, but maybe game wasn't ready yet: processLastate: function() { const data = this.lastate; @@ -615,7 +686,12 @@ export default { if (!confirm(this.st.tr["Offer draw?"])) return; this.drawOffer = "sent"; this.send("drawoffer"); - GameStorage.update(this.gameRef.id, { drawOffer: this.game.mycolor }); + if (this.game.type == "live") { + GameStorage.update( + this.gameRef.id, + { drawOffer: this.game.mycolor } + ); + } else this.updateCorrGame({ drawOffer: this.game.mycolor }); } }, abortGame: function() { @@ -757,6 +833,9 @@ export default { // Re-load game because we missed some moves: // artificially reset BaseGame (required if moves arrived in wrong order) this.$refs["basegame"].re_setVariables(); + else + // Initial loading: + this.gotMoveIdx = game.moves.length - 1; this.re_setClocks(); this.$nextTick(() => { this.game.rendered = true; @@ -779,9 +858,22 @@ export default { // Remote live game: forgetting about callback func... (TODO: design) this.send("askfullgame", { target: this.gameRef.rid }); } else { - // Local or corr game + // Local or corr game on server. // NOTE: afterRetrieval() is never called if game not found - GameStorage.get(this.gameRef.id, afterRetrieval); + const gid = this.gameRef.id; + if (Number.isInteger(gid) || !isNaN(parseInt(gid))) { + // corr games identifiers are integers + ajax("/games", "GET", { gid: gid }, res => { + let g = res.game; + g.moves.forEach(m => { + m.squares = JSON.parse(m.squares); + }); + afterRetrieval(g); + }); + } + else + // Local game + GameStorage.get(this.gameRef.id, afterRetrieval); } }, re_setClocks: function() { @@ -801,13 +893,13 @@ export default { i == colorIdx ? (Date.now() - this.game.initime[colorIdx]) / 1000 : 0; return ppt(this.game.clocks[i] - removeTime).split(':'); }); - let clockUpdate = setInterval(() => { + this.clockUpdate = setInterval(() => { if ( countdown < 0 || this.game.moves.length > currentMovesCount || this.game.score != "*" ) { - clearInterval(clockUpdate); + clearInterval(this.clockUpdate); if (countdown < 0) this.gameOver( currentTurn == "w" ? "0-1" : "1-0", @@ -856,18 +948,17 @@ export default { else this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime; // data.initime is set only when I receive a "lastate" move from opponent this.game.initime[nextIdx] = data.initime || Date.now(); - this.re_setClocks(); // If repetition detected, consider that a draw offer was received: const fenObj = this.vr.getFenForRepeat(); this.repeat[fenObj] = this.repeat[fenObj] ? this.repeat[fenObj] + 1 : 1; if (this.repeat[fenObj] >= 3) this.drawOffer = "threerep"; else if (this.drawOffer == "threerep") this.drawOffer = ""; - // Since corr games are stored at only one location, update should be - // done only by one player for each move: if (!!this.game.mycolor && !data.receiveMyMove) { // NOTE: 'var' to see that variable outside this block var filtered_move = getFilteredMove(move); } + // Since corr games are stored at only one location, update should be + // done only by one player for each move: if ( !!this.game.mycolor && !data.receiveMyMove && @@ -886,7 +977,8 @@ export default { break; } if (this.game.type == "corr") { - GameStorage.update(this.gameRef.id, { + // corr: only move, fen and score + this.updateCorrGame({ fen: this.game.fen, move: { squares: filtered_move, @@ -927,10 +1019,10 @@ export default { this.opponentGotMove = false; this.send("newmove", {data: sendMove}); // If the opponent doesn't reply gotmove soon enough, re-send move: - let retrySendmove = setInterval( + this.retrySendmove = setInterval( () => { if (this.opponentGotMove) { - clearInterval(retrySendmove); + clearInterval(this.retrySendmove); return; } let oppsid = this.game.players[nextIdx].sid; @@ -941,7 +1033,7 @@ export default { } if (!oppsid || !this.people[oppsid]) // Opponent is disconnected: he'll ask last state - clearInterval(retrySendmove); + clearInterval(this.retrySendmove); else this.send("newmove", {data: sendMove, target: oppsid}); }, 1000 @@ -962,18 +1054,24 @@ export default { () => { document.getElementById("modalConfirm").checked = false; doProcessMove(); + if (this.st.settings.gotonext) this.showNextGame(); + else this.re_setClocks(); } ); - this.vr.play(move); - const parsedFen = V.ParseFen(this.vr.getFen()); - this.vr.undo(move); + // PlayOnBoard is enough, and more appropriate for Synchrone Chess + V.PlayOnBoard(this.vr.board, move); + const position = this.vr.getBaseFen(); + V.UndoOnBoard(this.vr.board, move); this.curDiag = getDiagram({ - position: parsedFen.position, - orientation: this.game.mycolor + position: position, + orientation: V.CanFlip ? this.game.mycolor : "w" }); document.getElementById("modalConfirm").checked = true; } - else doProcessMove(); + else { + doProcessMove(); + this.re_setClocks(); + } }, cancelMove: function() { document.getElementById("modalConfirm").checked = false; @@ -987,10 +1085,13 @@ export default { }); if (myIdx >= 0) { // OK, I play in this game - GameStorage.update(this.gameRef.id, { + const scoreObj = { score: score, scoreMsg: scoreMsg - }); + }; + if (this.Game.type == "live") + GameStorage.update(this.gameRef.id, scoreObj); + else this.updateCorrGame(scoreObj); // Notify the score to main Hall. TODO: only one player (currently double send) this.send("result", { gid: this.game.id, score: score }); } diff --git a/client/src/views/Logout.vue b/client/src/views/Logout.vue index 1740c7da..0db08a93 100644 --- a/client/src/views/Logout.vue +++ b/client/src/views/Logout.vue @@ -4,7 +4,6 @@ main .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 div(v-if="logoutOk") p {{ st.tr["Logout successful!"] }} - p {{ st.tr["Back to Hall in 3 seconds..."] }} </template> <script> @@ -30,9 +29,6 @@ export default { this.st.user.notify = false; localStorage.removeItem("myid"); localStorage.removeItem("myname"); - setTimeout(() => { - this.$router.replace("/"); - }, 3000); } ); } diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index bcf7d182..4b5e33c2 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -16,6 +16,7 @@ main v-show="display=='corr'" :games="corrGames" @show-game="showGame" + @abort="abortGame" ) </template> @@ -23,6 +24,7 @@ main import { store } from "@/store"; import { GameStorage } from "@/utils/gameStorage"; import { ajax } from "@/utils/ajax"; +import { getScoreMessage } from "@/utils/scoring"; import params from "@/parameters"; import { getRandString } from "@/utils/alea"; import GameList from "@/components/GameList.vue"; @@ -43,13 +45,24 @@ export default { }, created: function() { GameStorage.getAll(true, localGames => { - localGames.forEach(g => (g.type = this.classifyObject(g))); + localGames.forEach(g => g.type = "live"); this.liveGames = localGames; }); if (this.st.user.id > 0) { - ajax("/games", "GET", { uid: this.st.user.id }, res => { - res.games.forEach(g => (g.type = this.classifyObject(g))); - this.corrGames = res.games; + ajax( + "/games", + "GET", + { uid: this.st.user.id }, + res => { + let serverGames = res.games.filter(g => { + const mySide = + g.players[0].uid == this.st.user.id + ? "White" + : "Black"; + return !g["deletedBy" + mySide]; + }); + serverGames.forEach(g => g.type = "corr"); + this.corrGames = serverGames; }); } // Initialize connection @@ -118,6 +131,40 @@ export default { } this.$router.push("/game/" + game.id + nextIds); }, + abortGame: function(game) { + // Special "trans-pages" case: from MyGames to Game + // TODO: also for corr games? (It's less important) + if (game.type == "live") { + const oppsid = + game.players[0].sid == this.st.user.sid + ? game.players[1].sid + : game.players[0].sid; + this.conn.send( + JSON.stringify( + { + code: "mabort", + gid: game.id, + // NOTE: target might not be online + target: oppsid + } + ) + ); + } + else if (!game.deletedByWhite || !game.deletedByBlack) { + // Set score if game isn't deleted on server: + ajax( + "/games", + "PUT", + { + gid: game.id, + newObj: { + score: "?", + scoreMsg: getScoreMessage("?") + } + } + ); + } + }, socketMessageListener: function(msg) { const data = JSON.parse(msg.data); if (data.code == "changeturn") { diff --git a/server/db/create.sql b/server/db/create.sql index e14e9a0f..c9f52324 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -55,11 +55,14 @@ create table Games ( vid integer, fenStart varchar, --initial state fen varchar, --current state - score varchar, + score varchar default '*', scoreMsg varchar, cadence varchar, created datetime, - drawOffer character, + drawOffer character default '', + rematchOffer character default '', + deletedByWhite boolean, + deletedByBlack boolean, foreign key (vid) references Variants(id) ); diff --git a/server/models/Game.js b/server/models/Game.js index 39459176..fc485910 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -48,9 +48,9 @@ const GameModel = db.serialize(function() { let query = "INSERT INTO Games " + - "(vid, fenStart, fen, score, cadence, created, drawOffer) " + + "(vid, fenStart, fen, cadence, created) " + "VALUES " + - "(" + vid + ",'" + fen + "','" + fen + "','*','" + cadence + "'," + Date.now() + ",'')"; + "(" + vid + ",'" + fen + "','" + fen + "','" + cadence + "'," + Date.now() + ")"; db.run(query, function(err) { if (err) cb(err) @@ -70,65 +70,46 @@ const GameModel = }, // TODO: some queries here could be async - getOne: function(id, light, cb) + getOne: function(id, cb) { // NOTE: ignoring errors (shouldn't happen at this stage) db.serialize(function() { let query = "SELECT g.id, g.vid, g.fen, g.fenStart, g.cadence, g.created, g.score, " + - "g.scoreMsg, g.drawOffer, v.name AS vname " + + "g.scoreMsg, g.drawOffer, g.rematchOffer, v.name AS vname " + "FROM Games g " + "JOIN Variants v " + " ON g.vid = v.id " + "WHERE g.id = " + id; - db.get(query, (err,gameInfo) => { + db.get(query, (err, gameInfo) => { query = "SELECT p.uid, p.color, u.name " + "FROM Players p " + "JOIN Users u " + " ON p.uid = u.id " + "WHERE p.gid = " + id; - db.all(query, (err2,players) => { - if (light) - { + db.all(query, (err2, players) => { + query = + "SELECT squares, played, idx " + + "FROM Moves " + + "WHERE gid = " + id; + db.all(query, (err3, moves) => { query = - "SELECT COUNT(*) AS nbMoves " + - "FROM Moves " + + "SELECT msg, name, added " + + "FROM Chats " + "WHERE gid = " + id; - db.get(query, (err,ret) => { + db.all(query, (err4, chats) => { const game = Object.assign({}, gameInfo, - {players: players}, - {movesCount: ret.nbMoves} + { + players: players, + moves: moves, + chats: chats, + } ); cb(null, game); }); - } - else - { - // Full game requested: - query = - "SELECT squares, played, idx " + - "FROM Moves " + - "WHERE gid = " + id; - db.all(query, (err3,moves) => { - query = - "SELECT msg, name, added " + - "FROM Chats " + - "WHERE gid = " + id; - db.all(query, (err4,chats) => { - const game = Object.assign({}, - gameInfo, - { - players: players, - moves: moves, - chats: chats, - } - ); - cb(null, game); - }); - }); - } + }); }); }); }); @@ -137,17 +118,49 @@ const GameModel = // For display on MyGames or Hall: no need for moves or chats getByUser: function(uid, excluded, cb) { + // Some fields are not required when showing a games list: + const getOneLight = (id, cb2) => { + let query = + "SELECT g.id, g.vid, g.fen, g.cadence, g.created, g.score, " + + "g.scoreMsg, g.deletedByWhite, g.deletedByBlack, v.name AS vname " + + "FROM Games g " + + "JOIN Variants v " + + " ON g.vid = v.id " + + "WHERE g.id = " + id; + db.get(query, (err, gameInfo) => { + query = + "SELECT p.uid, p.color, u.name " + + "FROM Players p " + + "JOIN Users u " + + " ON p.uid = u.id " + + "WHERE p.gid = " + id; + db.all(query, (err2, players) => { + query = + "SELECT COUNT(*) AS nbMoves " + + "FROM Moves " + + "WHERE gid = " + id; + db.get(query, (err,ret) => { + const game = Object.assign({}, + gameInfo, + { + players: players, + movesCount: ret.nbMoves + } + ); + cb2(game); + }); + }); + }); + }; db.serialize(function() { let query = ""; - if (uid == 0) - { + if (uid == 0) { // Special case anonymous user: show all games query = "SELECT id AS gid " + "FROM Games"; } - else - { + else { // Registered user: query = "SELECT gid " + @@ -157,15 +170,12 @@ const GameModel = (excluded ? " = 0" : " > 0"); } db.all(query, (err,gameIds) => { - if (err || gameIds.length == 0) - cb(err, []); - else - { + if (err || gameIds.length == 0) cb(err, []); + else { let gameArray = []; let gCounter = 0; - for (let i=0; i<gameIds.length; i++) - { - GameModel.getOne(gameIds[i]["gid"], true, (err2,game) => { + for (let i=0; i<gameIds.length; i++) { + getOneLight(gameIds[i]["gid"], (game) => { gameArray.push(game); gCounter++; //TODO: let's hope this is atomic?! // Call callback function only when gameArray is complete: @@ -230,12 +240,16 @@ const GameModel = obj.drawOffer = ""; modifs += "drawOffer = '" + obj.drawOffer + "',"; } - if (obj.fen) + if (!!obj.fen) modifs += "fen = '" + obj.fen + "',"; - if (obj.score) + if (!!obj.score) modifs += "score = '" + obj.score + "',"; - if (obj.scoreMsg) + if (!!obj.scoreMsg) modifs += "scoreMsg = '" + obj.scoreMsg + "',"; + if (!!obj.deletedBy) { + const myColor = obj.deletedBy == 'w' ? "White" : "Black"; + modifs += "deletedBy" + myColor + " = true,"; + } modifs = modifs.slice(0,-1); //remove last comma if (modifs.length > 0) { @@ -263,7 +277,7 @@ const GameModel = }); } else cb(null); - if (obj.chat) + if (!!obj.chat) { query = "INSERT INTO Chats (gid, msg, name, added) VALUES (" @@ -278,6 +292,21 @@ const GameModel = "WHERE gid = " + id; db.run(query); } + if (!!obj.deletedBy) { + // Did my opponent delete it too? + let selection = + "deletedBy" + + (obj.deletedBy == 'w' ? "Black" : "White") + + " AS deletedByOpp"; + query = + "SELECT " + selection + " " + + "FROM Games " + + "WHERE id = " + id; + db.get(query, (err,ret) => { + // If yes: just remove game + if (!!ret.deletedByOpp) GameModel.remove(id); + }); + } }); }, diff --git a/server/routes/games.js b/server/routes/games.js index 4000ac78..57656f41 100644 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -35,7 +35,7 @@ router.get("/games", access.ajax, (req,res) => { { if (gameId.match(/^[0-9]+$/)) { - GameModel.getOne(gameId, false, (err,game) => { + GameModel.getOne(gameId, (err,game) => { res.json({game: game}); }); } @@ -57,14 +57,19 @@ router.get("/games", access.ajax, (req,res) => { // FEN update + score(Msg) + draw status / and new move + chats router.put("/games", access.logged, access.ajax, (req,res) => { const gid = req.body.gid; - const obj = req.body.newObj; + let obj = req.body.newObj; if (gid.toString().match(/^[0-9]+$/) && GameModel.checkGameUpdate(obj)) { GameModel.getPlayers(gid, (err,players) => { - if (players.some(p => p.uid == req.userId)) - { + const myIdx = players.findIndex(p => p.uid == req.userId) + if (myIdx >= 0) { + // Did I mark the game for deletion? + if (!!obj.removeFlag) { + obj.deletedBy = ["w","b"][myIdx]; + delete obj["removeFlag"]; + } GameModel.update(gid, obj, (err) => { - if (!err && (obj.move || obj.score)) + if (!err && (!!obj.move || !!obj.score)) { // Notify opponent if he enabled notifications: const oppid = players[0].uid == req.userId diff --git a/server/sockets.js b/server/sockets.js index 406effee..ea95dc5d 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -35,7 +35,7 @@ module.exports = function(wss) { if (k == sid && x == tmpId) return; send( clients[page][k][x], - Object.assign({code: code, from: sid}, obj) + Object.assign({ code: code, from: sid }, obj) ); }); }); @@ -57,7 +57,7 @@ module.exports = function(wss) { // I effectively disconnected from this page: notifyRoom(page, "disconnect"); if (page.indexOf("/game/") >= 0) - notifyRoom("/", "gdisconnect", {page:page}); + notifyRoom("/", "gdisconnect", { page:page }); } }; const messageListener = (objtxt) => { @@ -69,7 +69,7 @@ module.exports = function(wss) { case "connect": { notifyRoom(page, "connect"); if (page.indexOf("/game/") >= 0) - notifyRoom("/", "gconnect", {page:page}); + notifyRoom("/", "gconnect", { page:page }); break; } case "disconnect": @@ -90,7 +90,7 @@ module.exports = function(wss) { Object.keys(clients[pg][k]).forEach(x => { send( clients[pg][k][x], - Object.assign({code: code, from: obj.sid}, o) + Object.assign({ code: code, from: obj.sid }, o) ); }); } @@ -101,7 +101,7 @@ module.exports = function(wss) { doKill(pg); disconnectFromOtherConnexion(pg, "disconnect"); if (pg.indexOf("/game/") >= 0 && clients["/"]) - disconnectFromOtherConnexion("/", "gdisconnect", {page: pg}); + disconnectFromOtherConnexion("/", "gdisconnect", { page: pg }); } }); break; @@ -113,7 +113,7 @@ module.exports = function(wss) { // Avoid polling myself: no new information to get if (k != sid) sockIds.push(k); }); - send(socket, {code: "pollclients", sockIds: sockIds}); + send(socket, { code: "pollclients", sockIds: sockIds }); break; } case "pollclientsandgamers": { @@ -128,11 +128,11 @@ module.exports = function(wss) { if (p != "/") { Object.keys(clients[p]).forEach(k => { // 'page' indicator is needed for gamers - if (k != sid) sockIds.push({sid:k, page:p}); + if (k != sid) sockIds.push({ sid:k, page:p }); }); } }); - send(socket, {code: "pollclientsandgamers", sockIds: sockIds}); + send(socket, { code: "pollclientsandgamers", sockIds: sockIds }); break; } @@ -155,7 +155,7 @@ module.exports = function(wss) { const tmpId_idx = Math.floor(Math.random() * tmpIds.length); send( clients[pg][obj.target][tmpIds[tmpId_idx]], - {code: obj.code, from: [sid,tmpId,page]} + { code: obj.code, from: [sid,tmpId,page] } ); } break; @@ -168,7 +168,7 @@ module.exports = function(wss) { if (obj.target != sid || x != tmpId) send( clients[page][obj.target][x], - {code: obj.code, data: obj.data} + { code: obj.code, data: obj.data } ); }); break; @@ -186,13 +186,13 @@ module.exports = function(wss) { break; case "newmove": { - const dataWithFrom = {from: [sid,tmpId], data: obj.data}; + const dataWithFrom = { from: [sid,tmpId], data: obj.data }; // Special case re-send newmove only to opponent: if (!!obj.target && !!clients[page][obj.target]) { Object.keys(clients[page][obj.target]).forEach(x => { send( clients[page][obj.target][x], - Object.assign({code: "newmove"}, dataWithFrom) + Object.assign({ code: "newmove" }, dataWithFrom) ); }); } else { @@ -208,14 +208,14 @@ module.exports = function(wss) { ) { send( clients[page][obj.target[0]][obj.target[1]], - {code: "gotmove", data: obj.data} + { code: "gotmove", data: obj.data } ); } break; case "result": // Special case: notify all, 'transroom': Game --> Hall - notifyRoom("/", "result", {gid: obj.gid, score: obj.score}); + notifyRoom("/", "result", { gid: obj.gid, score: obj.score }); break; case "mconnect": @@ -228,7 +228,7 @@ module.exports = function(wss) { Object.keys(clients[pg][s]).forEach(x => { send( clients[pg][s][x], - {code: "mconnect", data: obj.data} + { code: "mconnect", data: obj.data } ); }); }); @@ -238,6 +238,18 @@ module.exports = function(wss) { // TODO // Also TODO: pass newgame to MyGames, and gameover (result) break; + case "mabort": { + const gamePg = "/game/" + obj.gid; + if (!!clients[gamePg] && !!clients[gamePg][obj.target]) { + Object.keys(clients[gamePg][target]).forEach(x => { + send( + clients[gamePg][obj.target][x], + { code: "abort" } + ); + }); + } + break; + } // Passing, relaying something: from isn't needed, // but target is fully identified (sid + tmpId) @@ -250,8 +262,12 @@ module.exports = function(wss) { const pg = obj.target[2] || page; //required for identity and game // NOTE: if in game we ask identity to opponent still in Hall, // but leaving Hall, clients[pg] or clients[pg][target] could be undefined - if (!!clients[pg] && !!clients[pg][obj.target[0]]) - send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data}); + if (!!clients[pg] && !!clients[pg][obj.target[0]]) { + send( + clients[pg][obj.target[0]][obj.target[1]], + { code:obj.code, data:obj.data } + ); + } break; } } @@ -262,9 +278,9 @@ module.exports = function(wss) { }; // Update clients object: add new connexion if (!clients[page]) - clients[page] = {[sid]: {[tmpId]: socket}}; + clients[page] = { [sid]: {[tmpId]: socket } }; else if (!clients[page][sid]) - clients[page][sid] = {[tmpId]: socket}; + clients[page][sid] = { [tmpId]: socket }; else clients[page][sid][tmpId] = socket; socket.on("message", messageListener);