From f54f4c26b4c820b14aca298e94644efb20beeed6 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 19 Mar 2020 03:29:40 +0100 Subject: [PATCH] Show check(mate) indicators in moves list. No longer require the odd ?rid=... in games refs. Fix missing askgame in Hall at gconnect --- client/src/components/BaseGame.vue | 29 +++-- client/src/utils/gameStorage.js | 4 +- client/src/views/Game.vue | 182 +++++++++++++---------------- client/src/views/Hall.vue | 90 +++++++------- server/sockets.js | 38 ++++-- 5 files changed, 176 insertions(+), 167 deletions(-) diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index c4ffe5ca..3446a036 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -178,12 +178,15 @@ export default { const parsedFen = V.ParseFen(game.fenStart); const firstMoveColor = parsedFen.turn; this.firstMoveNumber = Math.floor(parsedFen.movesCount / 2); + let L = this.moves.length; this.moves.forEach(move => { // Strategy working also for multi-moves: if (!Array.isArray(move)) move = [move]; - move.forEach(m => { + move.forEach((m,idx) => { m.notation = this.vr.getNotation(m); this.vr.play(m); + if (idx < L - 1 && this.vr.getCheckSquares(this.vr.turn).length > 0) + m.notation += "+"; }); }); if (firstMoveColor == "b") { @@ -194,9 +197,15 @@ export default { end: { x: -1, y: -1 }, fen: game.fenStart }); + L++; } 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 += "+"; }, positionCursorTo: function(index) { this.cursor = index; @@ -349,6 +358,12 @@ export default { }; const computeScore = () => { const score = this.vr.getCurrentScore(); + if (!navigate) { + if (["1-0","0-1"].includes(score)) + this.lastMove.notation += "#"; + else if (this.vr.getCheckSquares(this.vr.turn).length > 0) + this.lastMove.notation += "+"; + } if (score != "*" && this.game.mode == "analyze") { const message = getScoreMessage(score); // Just show score on screen (allow undo) @@ -366,15 +381,15 @@ export default { this.incheck = this.vr.getCheckSquares(this.vr.turn); this.emitFenIfAnalyze(); this.inMultimove = false; - if (!noemit) var score = computeScore(); + this.score = computeScore(); if (this.game.mode != "analyze") { - const L = this.moves.length; - if (!noemit) + if (!noemit) { // Post-processing (e.g. computer play). + const L = this.moves.length; // NOTE: always emit the score, even in unfinished, // to tell Game::processMove() that it's not a received move. - this.$emit("newmove", this.moves[L-1], { score: score }); - else { + this.$emit("newmove", this.moves[L-1], { score: this.score }); + } else { this.inPlay = false; if (this.stackToPlay.length > 0) // Move(s) arrived in-between @@ -394,7 +409,7 @@ export default { if (!light) { this.lastMove = move[move.length-1]; this.incheck = this.vr.getCheckSquares(this.vr.turn); - computeScore(); + this.score = computeScore(); this.emitFenIfAnalyze(); } this.cursor++; diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index ba189491..12986679 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -111,8 +111,8 @@ export const GameStorage = { dbOperation((err,db) => { let objectStore = db.transaction("games").objectStore("games"); objectStore.get(gameId).onsuccess = function(event) { - if (event.target.result) - callback(event.target.result); + // event.target.result is null if game not found + callback(event.target.result); }; }); }, diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 5785abdd..68c8c94d 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -153,11 +153,8 @@ export default { data: function() { return { st: store.state, - gameRef: { - // rid = remote (socket) ID - id: "", - rid: "" - }, + // gameRef can point to a corr game, local game or remote live game + gameRef: "", nextIds: [], game: {}, //passed to BaseGame // virtualClocks will be initialized from true game.clocks @@ -205,10 +202,8 @@ export default { 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.fetchGame(); + this.loadGame(this.game); } } }, @@ -244,11 +239,8 @@ export default { }, 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.gameRef = this.$route.params["id"]; + // next = next corr games IDs to navigate faster (if applicable) this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); // Always add myself to players' list const my = this.st.user; @@ -307,18 +299,18 @@ export default { callback(); else // Socket not ready yet (initial loading) - // NOTE: it's important to call callback without arguments, - // otherwise first arg is Websocket object and fetchGame fails. + // NOTE: first arg is Websocket object, unused here: this.conn.onopen = () => callback(); }; - if (!this.gameRef.rid) - // Game stored locally or on server - this.fetchGame(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.fetchGame); + this.fetchGame((game) => { + if (!!game) + this.loadVariantThenGame(game, () => socketInit(this.roomInit)); + else + // Live 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.send("askfullgame"); }); + }); }, cleanBeforeDestroy: function() { if (!!this.askLastate) @@ -432,13 +424,15 @@ export default { const currentUrl = document.location.href; const doAskGame = () => { if (document.location.href != currentUrl) return; //page change - if (!this.gameRef.rid) - // This is my game: just reload. - this.fetchGame(); - else - // Just ask fullgame again (once!), this is much simpler. - // If this fails, the user could just reload page :/ - this.send("askfullgame", { target: this.gameRef.rid }); + this.fetchGame((game) => { + if (!!game) + // This is my game: just reload. + this.loadGame(game); + else + // Just ask fullgame again (once!), this is much simpler. + // If this fails, the user could just reload page :/ + this.send("askfullgame"); + }); }; // Delay of at least 2s between two game requests const now = Date.now(); @@ -563,8 +557,7 @@ export default { players: this.game.players, vid: this.game.vid, cadence: this.game.cadence, - score: this.game.score, - rid: this.st.user.sid //useful in Hall if I'm an observer + score: this.game.score }; this.send("game", { data: myGame, target: data.from }); } @@ -587,7 +580,7 @@ export default { break; case "fullgame": // Callback "roomInit" to poll clients only after game is loaded - this.fetchGame(data.data, this.roomInit); + this.loadVariantThenGame(data.data, this.roomInit); break; case "asklastate": // Sending informative last state if I played a move or score != "*" @@ -639,7 +632,7 @@ export default { !!this.game.mycolor && !receiveMyMove ) { - GameStorage.update(this.gameRef.id, { drawOffer: "" }); + GameStorage.update(this.gameRef, { drawOffer: "" }); } } this.$refs["basegame"].play(movePlus.move, "received", null, true); @@ -676,10 +669,22 @@ export default { case "drawoffer": // NOTE: observers don't know who offered draw this.drawOffer = "received"; + if (this.game.type == "live") { + GameStorage.update( + this.gameRef, + { drawOffer: V.GetOppCol(this.game.mycolor) } + ); + } break; case "rematchoffer": // NOTE: observers don't know who offered rematch this.rematchOffer = data.data ? "received" : ""; + if (this.game.type == "live") { + GameStorage.update( + this.gameRef, + { rematchOffer: V.GetOppCol(this.game.mycolor) } + ); + } break; case "newgame": { // A game started, redirect if I'm playing in @@ -696,17 +701,7 @@ export default { ) { this.$router.push("/game/" + gameInfo.id); } else { - let urlRid = ""; - if (gameInfo.cadence.indexOf('d') === -1) { - urlRid = "/?rid="; - // Select sid of any of the online players: - let onlineSid = []; - gameInfo.players.forEach(p => { - if (!!this.people[p.sid]) onlineSid.push(p.sid); - }); - urlRid += onlineSid[Math.floor(Math.random() * onlineSid.length)]; - } - this.rematchId = gameInfo.id + urlRid; + this.rematchId = gameInfo.id; document.getElementById("modalInfo").checked = true; } break; @@ -729,7 +724,7 @@ export default { "PUT", { data: { - gid: this.gameRef.id, + gid: this.gameRef, newObj: obj }, success: () => { @@ -804,7 +799,7 @@ export default { this.send("drawoffer"); if (this.game.type == "live") { GameStorage.update( - this.gameRef.id, + this.gameRef, { drawOffer: this.game.mycolor } ); } else this.updateCorrGame({ drawOffer: this.game.mycolor }); @@ -879,7 +874,7 @@ export default { this.send("rematchoffer", { data: true }); if (this.game.type == "live") { GameStorage.update( - this.gameRef.id, + this.gameRef, { rematchOffer: this.game.mycolor } ); } else this.updateCorrGame({ rematchOffer: this.game.mycolor }); @@ -889,7 +884,7 @@ export default { this.send("rematchoffer", { data: false }); if (this.game.type == "live") { GameStorage.update( - this.gameRef.id, + this.gameRef, { rematchOffer: '' } ); } else this.updateCorrGame({ rematchOffer: 'n' }); @@ -908,10 +903,6 @@ export default { 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) - // - from server (one correspondance game I play[ed] or not) - // - from remote peer (one live game I don't play, finished or not) loadGame: function(game, callback) { this.vr = new V(game.fen); const gtype = this.getGameType(game); @@ -1052,45 +1043,36 @@ export default { } if (!!callback) callback(); }, - fetchGame: function(game, callback) { - const afterRetrieval = async (game) => { - await import("@/variants/" + game.vname + ".js") - .then((vModule) => { - window.V = vModule[game.vname + "Rules"]; - this.loadGame(game, callback); - }); - }; - if (!!game) { - afterRetrieval(game); - return; - } - if (this.gameRef.rid) - // Remote live game: forgetting about callback func... (TODO: design) - this.send("askfullgame", { target: this.gameRef.rid }); - else { - // Local or corr game on server. - // NOTE: afterRetrieval() is never called if game not found - const gid = this.gameRef.id; - if (Number.isInteger(gid) || !isNaN(parseInt(gid))) { - // corr games identifiers are integers - ajax( - "/games", - "GET", - { - data: { gid: gid }, - success: (res) => { - res.game.moves.forEach(m => { - m.squares = JSON.parse(m.squares); - }); - afterRetrieval(res.game); - } + loadVariantThenGame: async function(game, callback) { + await import("@/variants/" + game.vname + ".js") + .then((vModule) => { + window.V = vModule[game.vname + "Rules"]; + this.loadGame(game, callback); + }); + }, + // 3 cases for loading a game: + // - from indexedDB (running or completed live game I play) + // - from server (one correspondance game I play[ed] or not) + // - from remote peer (one live game I don't play, finished or not) + fetchGame: function(callback) { + if (Number.isInteger(this.gameRef) || !isNaN(parseInt(this.gameRef))) { + // corr games identifiers are integers + ajax( + "/games", + "GET", + { + data: { gid: this.gameRef }, + success: (res) => { + res.game.moves.forEach(m => { + m.squares = JSON.parse(m.squares); + }); + callback(res.game); } - ); - } - else - // Local game - GameStorage.get(this.gameRef.id, afterRetrieval); - } + } + ); + } else + // Local game (or live remote) + GameStorage.get(this.gameRef, callback); }, re_setClocks: function() { if (this.game.moves.length < 2 || this.game.score != "*") { @@ -1155,11 +1137,11 @@ export default { playMove(move, this.vr); // The move is played: stop clock clearInterval(this.clockUpdate); - if (!data.score) { - // Received move, score has not been computed in BaseGame (!!noemit) - const score = this.vr.getCurrentScore(); - if (score != "*") this.gameOver(score); - } + if (!data.score) + // Received move, score is computed in BaseGame, but maybe not yet. + // ==> Compute it here, although this is redundant (TODO) + data.score = this.vr.getCurrentScore(); + if (data.score != "*") this.gameOver(data.score); this.game.moves.push(move); this.game.fen = this.vr.getFen(); if (this.game.type == "live") { @@ -1194,7 +1176,7 @@ export default { this.notifyMyGames( "turn", { - gid: this.gameRef.id, + gid: this.gameRef, turn: this.vr.turn } ); @@ -1232,7 +1214,7 @@ export default { } else { const updateStorage = () => { - GameStorage.update(this.gameRef.id, { + GameStorage.update(this.gameRef, { fen: this.game.fen, move: filtered_move, moveIdx: origMovescount, @@ -1370,7 +1352,7 @@ export default { scoreMsg: scoreMsg }; if (this.game.type == "live") { - GameStorage.update(this.gameRef.id, scoreObj); + GameStorage.update(this.gameRef, scoreObj); if (!!callback) callback(); } else this.updateCorrGame(scoreObj, callback); @@ -1380,7 +1362,7 @@ export default { this.notifyMyGames( "score", { - gid: this.gameRef.id, + gid: this.gameRef, score: score } ); diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 0840cd91..d11da8b1 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -558,10 +558,7 @@ export default { showGame: function(g) { // NOTE: we are an observer, since only games I don't play are shown here // ==> Moves sent by connected remote player(s) if live game - let url = "/game/" + g.id; - if (g.type == "live") - url += "?rid=" + g.rids[Math.floor(Math.random() * g.rids.length)]; - this.$router.push(url); + this.$router.push("/game/" + g.id); }, resetSocialColor: function() { // TODO: this is called twice, once on opening an once on closing @@ -612,15 +609,17 @@ export default { case "connect": case "gconnect": { const page = data.page || "/"; - // Only ask game / challenges if first connexion: - if (!this.people[data.from]) { - this.people[data.from] = { pages: [{ path: page, focus: true }] }; - if (data.code == "connect") + if (data.code == "connect") { + // Ask challenges only on first connexion: + if (!this.people[data.from]) this.send("askchallenges", { target: data.from }); - // Ask game only if live: - else if (!page.match(/\/[0-9]+$/)) - this.send("askgame", { target: data.from, page: page }); - } else { + } + // Ask game only if live: + else if (!page.match(/\/[0-9]+$/)) + this.send("askgame", { target: data.from, page: page }); + if (!this.people[data.from]) + this.people[data.from] = { pages: [{ path: page, focus: true }] }; + else { // Append page if not already in list if (!(this.people[data.from].pages.find(p => p.path == page))) this.people[data.from].pages.push({ path: page, focus: true }); @@ -638,6 +637,10 @@ export default { // the first reload won't have time to connect but will trigger a "close" event anyway. // ==> Next check is required. if (!this.people[data.from]) return; + const page = data.page || "/"; + ArrayFun.remove(this.people[data.from].pages, p => p.path == page); + if (this.people[data.from].pages.length == 0) + this.$delete(this.people, data.from); // Disconnect means no more tmpIds: if (data.code == "disconnect") { // Remove the live challenges sent by this player: @@ -648,22 +651,16 @@ export default { ); } else { // Remove the matching live game if now unreachable - const gid = data.page.match(/[a-zA-Z0-9]+$/)[0]; + const gid = page.match(/[a-zA-Z0-9]+$/)[0]; // Corr games are always reachable: if (!gid.match(/^[0-9]+$/)) { - const gidx = this.games.findIndex(g => g.id == gid); - // NOTE: gidx should always be >= 0 (TODO?) - if (gidx >= 0) { - const game = this.games[gidx]; - ArrayFun.remove(game.rids, rid => rid == data.from); - if (game.rids.length == 0) this.games.splice(gidx, 1); + // Live games are reachable as long as someone is on the game page + if (Object.values(this.people).every(p => + p.pages.every(pg => pg.path != page))) { + ArrayFun.remove(this.games, g => g.id == gid); } } } - const page = data.page || "/"; - ArrayFun.remove(this.people[data.from].pages, p => p.path == page); - if (this.people[data.from].pages.length == 0) - this.$delete(this.people, data.from); break; } case "getfocus": @@ -776,32 +773,27 @@ export default { case "game": // Individual request case "newgame": { const game = data.data; - // Ignore games where I play (will go in MyGames page) - if (game.players.every(p => - p.sid != this.st.user.sid && p.id != this.st.user.id)) - { - let locGame = this.games.find(g => g.id == game.id); - if (!locGame) { - let newGame = game; - newGame.type = this.classifyObject(game); - newGame.vname = this.getVname(game.vid); - if (!game.score) - // New game from Hall - newGame.score = "*"; - newGame.rids = [game.rid]; - delete newGame["rid"]; - this.games.push(newGame); - if ( - (newGame.type == "live" && this.gdisplay == "corr") || - (newGame.type == "corr" && this.gdisplay == "live") - ) { - document - .getElementById("btnG" + newGame.type) - .classList.add("somethingnew"); - } - } else { - // Append rid (if not already in list) - if (!locGame.rids.includes(game.rid)) locGame.rids.push(game.rid); + // Ignore games where I play (will go in MyGames page), + // and also games that I already received. + if ( + game.players.every(p => + p.sid != this.st.user.sid && p.id != this.st.user.id) && + this.games.findIndex(g => g.id == game.id) == -1 + ) { + let newGame = game; + newGame.type = this.classifyObject(game); + newGame.vname = this.getVname(game.vid); + if (!game.score) + // New game from Hall + newGame.score = "*"; + this.games.push(newGame); + if ( + (newGame.type == "live" && this.gdisplay == "corr") || + (newGame.type == "corr" && this.gdisplay == "live") + ) { + document + .getElementById("btnG" + newGame.type) + .classList.add("somethingnew"); } } break; diff --git a/server/sockets.js b/server/sockets.js index 4907965d..f904311c 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -155,22 +155,42 @@ module.exports = function(wss) { case "askidentity": case "asklastate": case "askchallenges": - case "askgame": - case "askfullgame": { + case "askgame": { const pg = obj.page || page; //required for askidentity and askgame - // In cas askfullgame to wrong SID for example, would crash: if (!!clients[pg] && !!clients[pg][obj.target]) { - const tmpIds = Object.keys(clients[pg][obj.target]); + let tmpIds = Object.keys(clients[pg][obj.target]); if (obj.target == sid) { // Targetting myself const idx_myTmpid = tmpIds.findIndex(x => x == tmpId); if (idx_myTmpid >= 0) tmpIds.splice(idx_myTmpid, 1); } - const tmpId_idx = Math.floor(Math.random() * tmpIds.length); - send( - clients[pg][obj.target][tmpIds[tmpId_idx]].socket, - { code: obj.code, from: [sid,tmpId,page] } - ); + if (tmpIds.length > 0) { + const ttmpId = tmpIds[Math.floor(Math.random() * tmpIds.length)]; + send( + clients[pg][obj.target][ttmpId].socket, + { code: obj.code, from: [sid,tmpId,page] } + ); + } + } + break; + } + + // Special situation of the previous "case": + // Full game can be asked to any observer. + case "askfullgame": { + if (!!clients[page]) { + let sids = Object.keys(clients[page]).filter(k => k != sid); + if (sids.length > 0) { + // Pick a SID at random in this set, and ask full game: + const rid = sids[Math.floor(Math.random() * sids.length)]; + // ..to a random tmpId: + const tmpIds = Object.keys(clients[page][rid]); + const rtmpId = tmpIds[Math.floor(Math.random() * tmpIds.length)]; + send( + clients[page][rid][rtmpId].socket, + { code: "askfullgame", from: [sid,tmpId] } + ); + } } break; } -- 2.44.0