From: Benjamin Auder Date: Mon, 23 Mar 2020 00:47:45 +0000 (+0100) Subject: Fixing attempt: clients now track tmpIds to know who is online/onfocus. Bug fixed... X-Git-Url: https://git.auder.net/%7B%7B%20asset%28%27mixstore/css/user/doc/html/common.css?a=commitdiff_plain;h=7ebc0408a76b4a966273190a2ade49e0f97099be;p=vchess.git Fixing attempt: clients now track tmpIds to know who is online/onfocus. Bug fixed for timers and rematch button --- diff --git a/client/src/variants/Synchrone.js b/client/src/variants/Synchrone.js new file mode 100644 index 00000000..e47941bf --- /dev/null +++ b/client/src/variants/Synchrone.js @@ -0,0 +1,11 @@ +import { ChessRules } from "@/base_rules"; + +export class SynchroneRules extends ChessRules { + // TODO: getNotation retourne "?" si turn == "w" + // ==> byrows disparait, juste "showAll" et "None". + // + // play: si turn == "w", enregistrer le coup (whiteMove), + // mais ne rien faire ==> résolution après le coup noir. + // + // ==> un coup sur deux (coups blancs) est "vide" du point de vue de l'exécution. +}; diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 89edb96d..13eab11f 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -26,13 +26,10 @@ main span {{ st.tr["Participant(s):"] }} span( v-for="p in Object.values(people)" - v-if="p.focus && !!p.name" + v-if="participateInChat(p)" ) | {{ p.name }} - span.anonymous( - v-if="Object.values(people).some(p => p.focus && !p.name)" - ) - | + @nonymous + span.anonymous(v-if="someAnonymousPresent()") + @nonymous Chat( ref="chatcomp" :players="game.players" @@ -192,7 +189,7 @@ export default { }, watch: { $route: function(to, from) { - if (to.path.length < 6 || to.path.substr(6) != "/game/") + if (to.path.length < 6 || to.path.substr(0, 6) != "/game/") // Page change this.cleanBeforeDestroy(); else if (from.params["id"] != to.params["id"]) { @@ -230,12 +227,9 @@ export default { methods: { cleanBeforeDestroy: function() { document.removeEventListener('visibilitychange', this.visibilityChange); - if (!!this.askLastate) - clearInterval(this.askLastate); - if (!!this.retrySendmove) - clearInterval(this.retrySendmove); - if (!!this.clockUpdate) - clearInterval(this.clockUpdate); + if (!!this.askLastate) clearInterval(this.askLastate); + if (!!this.retrySendmove) clearInterval(this.retrySendmove); + if (!!this.clockUpdate) clearInterval(this.clockUpdate); this.conn.removeEventListener("message", this.socketMessageListener); this.conn.removeEventListener("close", this.socketCloseListener); this.send("disconnect"); @@ -249,6 +243,16 @@ export default { : "losefocus" ); }, + participateInChat: function(p) { + return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name; + }, + someAnonymousPresent: function() { + return ( + Object.values(this.people).some(p => + !p.name && Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) + ) + ); + }, atCreation: function() { document.addEventListener('visibilitychange', this.visibilityChange); // 0] (Re)Set variables @@ -257,13 +261,16 @@ export default { this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); // Always add myself to players' list const my = this.st.user; + const tmpId = getRandString(); this.$set( this.people, my.sid, { id: my.id, name: my.name, - focus: true + tmpIds: { + tmpId: { focus: true } + } } ); this.game = { @@ -293,12 +300,9 @@ export default { // 1] Initialize connection this.connexionString = params.socketUrl + - "/?sid=" + - this.st.user.sid + - "&id=" + - this.st.user.id + - "&tmpId=" + - getRandString() + + "/?sid=" + this.st.user.sid + + "&id=" + this.st.user.id + + "&tmpId=" + tmpId + "&page=" + // Discard potential "/?next=[...]" for page indication: encodeURIComponent(this.$route.path.match(/\/game\/[a-zA-Z0-9]+/)[0]); @@ -351,14 +355,22 @@ export default { return ( ( !!player.sid && - Object.keys(this.people).some(sid => - sid == player.sid && this.people[sid].focus) + Object.keys(this.people).some(sid => { + return ( + sid == player.sid && + Object.values(this.people[sid].tmpIds).some(v => v.focus) + ); + }) ) || ( !!player.id && - Object.values(this.people).some(p => - p.id == player.id && p.focus) + Object.values(this.people).some(p => { + return ( + p.id == player.id && + Object.values(p.tmpIds).some(v => v.focus) + ); + }) ) ); }, @@ -449,40 +461,54 @@ export default { const data = JSON.parse(msg.data); switch (data.code) { case "pollclients": - // TODO: shuffling and random filtering on server, if - // the room is really crowded. - data.sockIds.forEach(sid => { + // TODO: shuffling and random filtering on server, + // if the room is really crowded. + Object.keys(data.sockIds).forEach(sid => { + // TODO: test sid != user.sid was already done on server if (sid != this.st.user.sid) { - this.people[sid] = { focus: true }; + this.people[sid] = { tmpIds: data.sockIds[sid] }; this.send("askidentity", { target: sid }); } }); break; case "connect": - if (!this.people[data.from]) { - this.people[data.from] = { focus: true }; + if (!this.people[data.from[0]]) { + // focus depends on the tmpId (e.g. tab) + this.$set( + this.people, + data.from[0], + { + tmpIds: { + [data.from[1]]: { focus: true } + } + } + ); this.newConnect[data.from] = true; //for self multi-connects tests - this.send("askidentity", { target: data.from }); - } else if (!this.people[data.from].focus) { - this.people[data.from].focus = true; + this.send("askidentity", { target: data.from[0] }); + } else { + this.people[data.from[0]].tmpIds[data.from[1]] = { focus: true }; this.$forceUpdate(); //TODO: shouldn't be required } break; case "disconnect": - this.$delete(this.people, data.from); + if (!this.people[data.from[0]]) return; + delete this.people[data.from[0]].tmpIds[data.from[1]]; + if (Object.keys(this.people[data.from[0]].tmpIds).length == 0) + this.$delete(this.people, data.from[0]); + else this.$forceUpdate(); //TODO: shouldn't be required break; case "getfocus": { - let player = this.people[data.from]; + let player = this.people[data.from[0]]; if (!!player) { - player.focus = true; + player.tmpIds[data.from[1]].focus = true; this.$forceUpdate(); //TODO: shouldn't be required } break; } case "losefocus": { - let player = this.people[data.from]; + let player = this.people[data.from[0]]; if (!!player) { - player.focus = false; + player.tmpIds[data.from[1]].focus = false; this.$forceUpdate(); //TODO: shouldn't be required } break; @@ -508,7 +534,7 @@ export default { case "identity": { const user = data.data; let player = this.people[user.sid]; - // player.focus is already set + // player.tmpIds is already set player.name = user.name; player.id = user.id; this.$forceUpdate(); //TODO: shouldn't be required @@ -597,6 +623,7 @@ export default { case "asklastate": // Sending informative last state if I played a move or score != "*" // If the game or moves aren't loaded yet, delay the sending: + // TODO: since socket init after game load, the game is supposedly ready if (!this.game || !this.game.moves) this.lastateAsked = true; else this.sendLastate(data.from); break; @@ -660,6 +687,9 @@ export default { this.opponentGotMove = true; // Now his clock starts running on my side: const oppIdx = ['w','b'].indexOf(this.vr.turn); + // NOTE: next line to avoid multi-resetClocks when several tabs + // on same game, resulting in a faster countdown. + if (!!this.clockUpdate) clearInterval(this.clockUpdate); this.re_setClocks(); break; } @@ -773,7 +803,7 @@ export default { this.$refs["basegame"].play(data.lastMove, "received", null, true); this.processMove(data.lastMove); } else { - clearInterval(this.clockUpdate); + if (!!this.clockUpdate) clearInterval(this.clockUpdate); this.re_setClocks(); } if (data.drawSent) this.drawOffer = "received"; @@ -852,7 +882,7 @@ export default { this.send("rnewgame", { data: gameInfo, oppsid: oppsid }); // To main Hall if corr game: if (this.game.type == "corr") - this.send("newgame", { data: gameInfo }); + this.send("newgame", { data: gameInfo, page: "/" }); // Also to MyGames page: this.notifyMyGames("newgame", gameInfo); }; @@ -1100,6 +1130,7 @@ export default { this.game.score != "*" ) { clearInterval(this.clockUpdate); + this.clockUpdate = null; if (this.game.clocks[colorIdx] < 0) this.gameOver( currentTurn == "w" ? "0-1" : "1-0", @@ -1126,6 +1157,7 @@ export default { const origMovescount = this.game.moves.length; // The move is (about to be) played: stop clock clearInterval(this.clockUpdate); + this.clockUpdate = null; if (moveCol == this.game.mycolor && !data.receiveMyMove) { if (this.drawOffer == "received") // I refuse draw diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 24c3ac6a..e60d982f 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -208,7 +208,7 @@ import { checkChallenge } from "@/data/challengeCheck"; import { ArrayFun } from "@/utils/array"; import { ajax } from "@/utils/ajax"; import params from "@/parameters"; -import { getRandString, shuffle } from "@/utils/alea"; +import { getRandString, shuffle, randInt } from "@/utils/alea"; import { getDiagram } from "@/utils/printDiagram"; import Chat from "@/components/Chat.vue"; import GameList from "@/components/GameList.vue"; @@ -278,13 +278,16 @@ export default { if (this.st.variants.length > 0 && this.newchallenge.vid > 0) this.loadNewchallVariant(); const my = this.st.user; + const tmpId = getRandString(); this.$set( this.people, my.sid, { id: my.id, name: my.name, - pages: [{ path: "/", focus: true }] + tmpIds: { + tmpId: { page: "/", focus: true } + } } ); const connectAndPoll = () => { @@ -294,14 +297,11 @@ export default { // Initialize connection this.connexionString = params.socketUrl + - "/?sid=" + - this.st.user.sid + - "&id=" + - this.st.user.id + - "&tmpId=" + - getRandString() + + "/?sid=" + this.st.user.sid + + "&id=" + this.st.user.id + + "&tmpId=" + tmpId + "&page=" + - // Hall: path is "/" (could be hard-coded as well) + // Hall: path is "/" (TODO: could be hard-coded as well) encodeURIComponent(this.$route.path); this.conn = new WebSocket(this.connexionString); this.conn.onopen = connectAndPoll; @@ -515,15 +515,15 @@ export default { else elt.nextElementSibling.classList.remove("active"); }, isGamer: function(sid) { - return this.people[sid].pages - .some(p => p.focus && p.path.indexOf("/game/") >= 0); + return Object.values(this.people[sid].tmpIds) + .some(v => v.focus && v.page.indexOf("/game/") >= 0); }, isFocusedOnHall: function(sid) { return ( // This is meant to challenge people, thus the next 2 conditions: this.st.user.id > 0 && sid != this.st.user.sid && - this.people[sid].pages.some(p => p.path == "/" && p.focus) + Object.values(this.people[sid].tmpIds).some(v => v.focus && v.page == "/") ); }, challenge: function(sid) { @@ -537,9 +537,9 @@ export default { watchGame: function(sid) { // In some game, maybe playing maybe not: show a random one let gids = []; - this.people[sid].pages.forEach(p => { - if (p.focus) { - const matchGid = p.path.match(/[a-zA-Z0-9]+$/); + Object.values(this.people[sid].tmpIds).forEach(v => { + if (v.focus) { + const matchGid = v.page.match(/[a-zA-Z0-9]+$/); if (!!matchGid) gids.push(matchGid[0]); } }); @@ -575,29 +575,39 @@ export default { const data = JSON.parse(msg.data); switch (data.code) { case "pollclientsandgamers": { - // Since people can be both in Hall and Game, - // need to track "askIdentity" requests: - let identityAsked = {}; - // TODO: shuffling and random filtering on server, if - // the room is really crowded. - data.sockIds.forEach(s => { - const page = s.page || "/"; - if (s.sid != this.st.user.sid && !identityAsked[s.sid]) { - this.send("askidentity", { target: s.sid, page: page }); - identityAsked[s.sid] = true; + // TODO: shuffling and random filtering on server, + // if the room is really crowded. + Object.keys(data.sockIds).forEach(sid => { + // TODO: test sid != user.sid was already done on server + if (sid != this.st.user.sid) { + // Pick a target tmpId (+page) at random: + const pt = Object.values(data.sockIds[sid]); + const randPage = pt[randInt(pt.length)].page; + this.send( + "askidentity", + { + target: sid, + page: randPage + } + ); } - if (!this.people[s.sid]) { + if (!this.people[sid]) // Do not set name or id: identity unknown yet - this.people[s.sid] = { pages: [{path: page, focus: true}] }; - } - else if (!(this.people[s.sid].pages.find(p => p.path == page))) - this.people[s.sid].pages.push({ path: page, focus: true }); - if (!s.page) + this.people[sid] = { tmpIds: data.sockIds[sid] }; + else + Object.assign(this.people[s.sid].tmpIds, data.sockIds[sid]); + if (Object.values(data.sockIds[sid]).some(v => v.page == "/")) // Peer is in Hall - this.send("askchallenges", { target: s.sid }); - // Peer is in Game: ask only if live game - else if (!page.match(/\/[0-9]+$/)) - this.send("askgame", { target: s.sid, page: page }); + this.send("askchallenges", { target: sid }); + Object.values(data.sockIds[sid]).forEach(v => { + if ( + v.page.indexOf("/game/") >= 0 && + !v.page.match(/\/[0-9]+$/) + ) { + // Peer is in Game: ask only if live game + this.send("askgame", { target: sid, page: v.page }); + } + }); }); break; } @@ -607,25 +617,27 @@ export default { if (data.code == "connect") { // Ask challenges only on first connexion: if (!this.people[data.from]) - this.send("askchallenges", { target: data.from }); + this.send("askchallenges", { target: data.from[0] }); } // 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 - let ppage = this.people[data.from].pages.find(p => p.path == page); - if (!ppage) - this.people[data.from].pages.push({ path: page, focus: true }); - else ppage.focus = true; - this.$forceUpdate(); //TODO: shouldn't be required - } - if (!this.people[data.from].name && this.people[data.from].id !== 0) { - // Identity not known yet + this.send("askgame", { target: data.from[0], page: page }); + if (!this.people[data.from]) { + this.$set( + this.people, + data.from[0], + { + tmpIds: { + [data.from[1]]: { page: page, focus: true } + } + } + ); this.newConnect[data.from] = true; //for self multi-connects tests - this.send("askidentity", { target: data.from, page: page }); + this.send("askidentity", { target: data.from[0], page: page }); + } else { + this.people[data.from[0]].tmpIds[data.from[1]] = + { page: page, focus: true }; + this.$forceUpdate(); //TODO: shouldn't be required } break; } @@ -634,49 +646,50 @@ export default { // If the user reloads the page twice very quickly (experienced with Firefox), // 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); + if (!this.people[data.from[0]]) return; + delete this.people[data.from[0]].tmpIds[data.from[1]]; + if (Object.keys(this.people[data.from[0]].tmpIds).length == 0) + this.$delete(this.people, data.from[0]); else this.$forceUpdate(); //TODO: shouldn't be required - // Disconnect means no more tmpIds: if (data.code == "disconnect") { // Remove the live challenges sent by this player: ArrayFun.remove( this.challenges, - c => c.type == "live" && c.from.sid == data.from, + c => c.type == "live" && c.from.sid == data.from[0], "all" ); } else { // Remove the matching live game if now unreachable - const gid = page.match(/[a-zA-Z0-9]+$/)[0]; + const gid = data.page.match(/[a-zA-Z0-9]+$/)[0]; // Corr games are always reachable: if (!gid.match(/^[0-9]+$/)) { // 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))) { + Object.values(p.tmpIds).every(v => v.page != data.page)) + ) { ArrayFun.remove(this.games, g => g.id == gid); } } } break; } - case "getfocus": + case "getfocus": { + let player = this.people[data.from[0]]; // If user reload a page, focus may arrive earlier than connect - if (!!this.people[data.from]) { - this.people[data.from].pages - .find(p => p.path == data.page).focus = true; + if (!!player) { + player.tmpIds[data.from[1]].focus = true; this.$forceUpdate(); //TODO: shouldn't be required } break; - case "losefocus": - if (!!this.people[data.from]) { - this.people[data.from].pages - .find(p => p.path == data.page).focus = false; + } + case "losefocus": { + let player = this.people[data.from[0]]; + if (!!player) { + player.tmpIds[data.from[1]].focus = false; this.$forceUpdate(); //TODO: shouldn't be required } break; + } case "killed": // I logged in elsewhere: this.conn.removeEventListener("message", this.socketMessageListener); @@ -698,7 +711,7 @@ export default { case "identity": { const user = data.data; let player = this.people[user.sid]; - // player.pages is already set + // player.tmpIds is already set player.id = user.id; player.name = user.name; // TODO: this.$set(people, ...) fails. So forceUpdate. @@ -811,13 +824,9 @@ export default { this.startNewGame(gameInfo); else { this.infoMessage = - this.st.tr["New correspondance game:"] + - " " + - "#/game/" + - gameInfo.id + - ""; + this.st.tr["New correspondance game:"] + " " + + "" + + "#/game/" + gameInfo.id + ""; document.getElementById("modalInfo").checked = true; } break; diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index 7615308b..1e1c9e7c 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -53,7 +53,7 @@ export default { corr: Number.MAX_SAFE_INTEGER }, // hasMore == TRUE: a priori there could be more games to load - hasMore: { live: true, corr: true }, + hasMore: { live: true, corr: store.state.user.id > 0 }, conn: null, connexionString: "" }; @@ -129,12 +129,7 @@ export default { } } ); - } else { - this.loadMore( - "live", - () => this.loadMore("corr", adjustAndSetDisplay) - ); - } + } else this.loadMore("live", adjustAndSetDisplay); }); }, beforeDestroy: function() { @@ -297,7 +292,7 @@ export default { } }, loadMore: function(type, cb) { - if (type == "corr") { + if (type == "corr" && this.st.user.id > 0) { ajax( "/completedgames", "GET", diff --git a/server/sockets.js b/server/sockets.js index f904311c..65e41635 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -24,6 +24,7 @@ module.exports = function(wss) { // or "/mygames" for Mygames page (simpler: no 'people' array). // tmpId is required if a same user (browser) has different tabs let clients = {}; + // NOTE: only purpose of sidToPages = know when to delete keys in idToSid let sidToPages = {}; let idToSid = {}; wss.on("connection", (socket, req) => { @@ -41,7 +42,7 @@ module.exports = function(wss) { if (k == sid && x == tmpId) return; send( clients[page][k][x].socket, - Object.assign({ code: code, from: sid }, obj) + Object.assign({ code: code, from: [sid, tmpId] }, obj) ); }); }); @@ -67,11 +68,10 @@ module.exports = function(wss) { const doDisconnect = () => { deleteConnexion(); // Nothing to notify when disconnecting from MyGames page: - if (page != "/mygames" && (!clients[page] || !clients[page][sid])) { - // I effectively disconnected from this page: + if (page != "/mygames") { notifyRoom(page, "disconnect"); if (page.indexOf("/game/") >= 0) - notifyRoom("/", "gdisconnect", { page:page }); + notifyRoom("/", "gdisconnect", { page: page }); } }; const messageListener = (objtxt) => { @@ -83,7 +83,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": @@ -121,28 +121,48 @@ module.exports = function(wss) { break; } case "pollclients": { - // From Hall or Game - let sockIds = []; + // From Game + let sockIds = {}; Object.keys(clients[page]).forEach(k => { // Avoid polling myself: no new information to get - if (k != sid) sockIds.push(k); + if (k != sid) { + sockIds[k] = {}; + Object.keys(clients[page][k]).forEach(x => { + sockIds[k][x] = { focus: clients[page][k][x].focus }; + }); + } }); send(socket, { code: "pollclients", sockIds: sockIds }); break; } case "pollclientsandgamers": { // From Hall - let sockIds = []; + let sockIds = {}; Object.keys(clients["/"]).forEach(k => { // Avoid polling myself: no new information to get - if (k != sid) sockIds.push({sid:k}); + if (k != sid) { + sockIds[k] = {}; + Object.keys(clients[page][k]).forEach(x => { + sockIds[k][x] = { + page: "/", + focus: clients[page][k][x].focus + }; + }); + } }); // NOTE: a "gamer" could also just be an observer Object.keys(clients).forEach(p => { if (p.indexOf("/game/") >= 0) { Object.keys(clients[p]).forEach(k => { - // 'page' indicator is needed for gamers - if (k != sid) sockIds.push({ sid:k, page:p }); + if (k != sid) { + if (!sockIds[k]) sockIds[k] = {}; + Object.keys(clients[p][k]).forEach(x => { + sockIds[k][x] = { + page: p, + focus: clients[p][k][x].focus + }; + }); + } }); } }); @@ -217,7 +237,8 @@ module.exports = function(wss) { case "drawoffer": case "rematchoffer": case "draw": - notifyRoom(page, obj.code, {data: obj.data}, obj.excluded); + // "newgame" message can provide a page (corr Game --> Hall) + notifyRoom(obj.page || page, obj.code, {data: obj.data}, obj.excluded); break; case "rnewgame": @@ -295,6 +316,7 @@ module.exports = function(wss) { case "getfocus": case "losefocus": + clients[page][sid][tmpId].focus = (obj.code == "getfocus"); if (page == "/") notifyRoom("/", obj.code, { page: "/" }, [sid]); else { // Notify game room + Hall: