X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FGame.vue;h=5145e5883461fc61e1c1a4ae949b635ef94dc5a9;hp=a7cb6a90f676b8ee54e31f6b386b15e5bdcff5a3;hb=1ef65040168ab7d55ce921abc9d63644a937d689;hpb=c292ebb2a014646005b01e27253c162f1d639387 diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index a7cb6a90..5145e588 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -7,10 +7,14 @@ main ) .card.text-center label.modal-close(for="modalInfo") - p(v-html="infoMessage") + a( + :href="'#/game/' + rematchId" + onClick="document.getElementById('modalInfo').checked=false" + ) + | {{ st.tr["Rematch in progress"] }} input#modalChat.modal( type="checkbox" - @click="resetChatColor()" + @click="toggleChat()" ) div#chatWrap( role="dialog" @@ -22,18 +26,14 @@ 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" :pastChats="game.chats" - :newChat="newChat" @mychat="processChat" @chatcleared="clearChat" ) @@ -57,7 +57,7 @@ main span {{ st.tr["Cancel"] }} .row #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2 - span.variant-cadence {{ game.cadence }} + span.variant-cadence(v-if="game.type!='import'") {{ game.cadence }} span.variant-name {{ game.vname }} span#nextGame( v-if="nextIds.length > 0" @@ -89,14 +89,14 @@ main ) img(src="/images/icons/resign.svg") button.tooltip( - v-else-if="!!game.mycolor" + v-else @click="clickRematch()" :class="{['rematch-' + rematchOffer]: true}" :aria-label="st.tr['Rematch']" ) img(src="/images/icons/rematch.svg") #playersInfo - p + p(v-if="isLargeScreen()") span.name(:class="{connected: isConnected(0)}") | {{ game.players[0].name || "@nonymous" }} span.time( @@ -118,6 +118,29 @@ main span.time-separator(v-if="!!virtualClocks[1][1]") : span.time-right(v-if="!!virtualClocks[1][1]") | {{ virtualClocks[1][1] }} + p(v-else) + span.name(:class="{connected: isConnected(0)}") + | {{ game.players[0].name || "@nonymous" }} + span.split-names - + span.name(:class="{connected: isConnected(1)}") + | {{ game.players[1].name || "@nonymous" }} + br + span.time( + v-if="game.score=='*'" + :class="{yourturn: !!vr && vr.turn == 'w'}" + ) + 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( + v-if="game.score=='*'" + :class="{yourturn: !!vr && vr.turn == 'b'}" + ) + 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] }} BaseGame( ref="basegame" :game="game" @@ -130,7 +153,9 @@ import BaseGame from "@/components/BaseGame.vue"; import Chat from "@/components/Chat.vue"; import { store } from "@/store"; import { GameStorage } from "@/utils/gameStorage"; +import { ImportgameStorage } from "@/utils/importgameStorage"; import { ppt } from "@/utils/datetime"; +import { notify } from "@/utils/notifications"; import { ajax } from "@/utils/ajax"; import { extractTime } from "@/utils/timeControl"; import { getRandString } from "@/utils/alea"; @@ -150,24 +175,22 @@ 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 + focus: !document.hidden, //will not always work... TODO // virtualClocks will be initialized from true game.clocks virtualClocks: [], vr: null, //"variant rules" object initialized from FEN drawOffer: "", + rematchId: "", rematchOffer: "", + lastateAsked: false, people: {}, //players + observers - onMygames: [], //opponents (or me) on "MyGames" page lastate: undefined, //used if opponent send lastate before game is ready repeat: {}, //detect position repetition curDiag: "", //for corr moves confirmation - newChat: "", conn: null, roomInitialized: false, // If newmove has wrong index: ask fullgame again: @@ -179,6 +202,7 @@ export default { // If newmove got no pingback, send again: opponentGotMove: false, connexionString: "", + socketCloseListener: 0, // Incomplete info games: show move played moveNotation: "", // Intervals from setInterval(): @@ -186,13 +210,15 @@ export default { retrySendmove: null, clockUpdate: null, // Related to (killing of) self multi-connects: - newConnect: {}, - killed: {} + newConnect: {} }; }, watch: { $route: function(to, from) { - if (from.params["id"] != to.params["id"]) { + if (to.path.length < 6 || to.path.substr(0, 6) != "/game/") + // Page change + this.cleanBeforeDestroy(); + else if (from.params["id"] != to.params["id"]) { // Change everything: this.cleanBeforeDestroy(); let boardDiv = document.querySelector(".game"); @@ -200,13 +226,9 @@ export default { // In case of incomplete information variant: boardDiv.style.visibility = "hidden"; this.atCreation(); - } else { + } 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) @@ -214,47 +236,92 @@ export default { this.atCreation(); }, mounted: function() { - document.addEventListener('visibilitychange', this.visibilityChange); - document - .getElementById("chatWrap") + document.getElementById("chatWrap") + .addEventListener("click", (e) => { + processModalClick(e, () => { + this.toggleChat("close") + }); + }); + document.getElementById("infoDiv") .addEventListener("click", processModalClick); if ("ontouchstart" in window) { // Disable tooltips on smartphones: - document.getElementsByClassName("tooltip").forEach(elt => { + document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => { elt.classList.remove("tooltip"); }); } }, beforeDestroy: function() { - document.removeEventListener('visibilitychange', this.visibilityChange); this.cleanBeforeDestroy(); }, methods: { + cleanBeforeDestroy: function() { + clearInterval(this.socketCloseListener); + document.removeEventListener('visibilitychange', this.visibilityChange); + window.removeEventListener('focus', this.onFocus); + window.removeEventListener('blur', this.onBlur); + 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.send("disconnect"); + this.conn = null; + }, visibilityChange: function() { // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27 - this.send( - document.visibilityState == "visible" - ? "getfocus" - : "losefocus" + this.focus = (document.visibilityState == "visible"); + if (!this.focus && !!this.rematchOffer) { + this.rematchOffer = ""; + this.send("rematchoffer", { data: false }); + // Do not remove rematch offer from (local) storage + } + this.send(this.focus ? "getfocus" : "losefocus"); + }, + onFocus: function() { + this.focus = true; + this.send("getfocus"); + }, + onBlur: function() { + this.focus = false; + if (!!this.rematchOffer) { + this.rematchOffer = ""; + this.send("rematchoffer", { data: false }); + } + this.send("losefocus"); + }, + isLargeScreen: function() { + return window.innerWidth >= 500; + }, + 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); + window.addEventListener('focus', this.onFocus); + window.addEventListener('blur', this.onBlur); // 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; + const tmpId = getRandString(); this.$set( this.people, my.sid, { id: my.id, name: my.name, - focus: true + tmpIds: { + tmpId: { focus: true } + } } ); this.game = { @@ -267,10 +334,9 @@ export default { this.virtualClocks = [[0,0], [0,0]]; this.vr = null; this.drawOffer = ""; + this.lastateAsked = false; this.rematchOffer = ""; - this.onMygames = []; this.lastate = undefined; - this.newChat = ""; this.roomInitialized = false; this.askGameTime = 0; this.gameIsLoading = false; @@ -281,48 +347,47 @@ export default { this.retrySendmove = null; this.clockUpdate = null; this.newConnect = {}; - this.killed = {}; // 1] Initialize connection this.connexionString = params.socketUrl + - "/?sid=" + - this.st.user.sid + - "&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]); this.conn = new WebSocket(this.connexionString); - this.conn.onmessage = this.socketMessageListener; - this.conn.onclose = this.socketCloseListener; + this.conn.addEventListener("message", this.socketMessageListener); + this.socketCloseListener = setInterval( + () => { + if (this.conn.readyState == 3) { + this.conn.removeEventListener( + "message", this.socketMessageListener); + this.conn = new WebSocket(this.connexionString); + this.conn.addEventListener("message", this.socketMessageListener); + } + }, + 1000 + ); // Socket init required before loading remote game: const socketInit = callback => { - if (!!this.conn && this.conn.readyState == 1) + if (this.conn.readyState == 1) // 1 == OPEN state callback(); else // Socket not ready yet (initial loading) - // NOTE: it's important to call callback without arguments, - // otherwise first arg is Websocket object and loadGame fails. + // NOTE: first arg is Websocket object, unused here: 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.askLastate) - clearInterval(this.askLastate); - if (!!this.retrySendmove) - clearInterval(this.retrySendmove); - if (!!this.clockUpdate) - clearInterval(this.clockUpdate); - this.send("disconnect"); + 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 it's not provided. + // --> It will be given when receiving "fullgame" socket event. + socketInit(() => { this.send("askfullgame"); }); + }); }, roomInit: function() { if (!this.roomInitialized) { @@ -336,13 +401,13 @@ export default { } }, send: function(code, obj) { - if (!!this.conn) + if (!!this.conn && this.conn.readyState == 1) this.conn.send(JSON.stringify(Object.assign({ code: code }, obj))); }, isConnected: function(index) { const player = this.game.players[index]; // Is it me ? In this case no need to bother with focus - if (this.st.user.sid == player.sid || this.st.user.id == player.uid) + if (this.st.user.sid == player.sid || this.st.user.id == player.id) // Still have to check for name (because of potential multi-accounts // on same browser, although this should be rare...) return (!this.st.user.name || this.st.user.name == player.name); @@ -350,14 +415,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.uid && - Object.values(this.people).some(p => - p.id == player.uid && p.focus) + !!player.id && + Object.values(this.people).some(p => { + return ( + p.id == player.id && + Object.values(p.tmpIds).some(v => v.focus) + ); + }) ) ); }, @@ -372,40 +445,65 @@ export default { if (!!oppsid && !!this.people[oppsid]) return oppsid; return null; }, - resetChatColor: function() { - // TODO: this is called twice, once on opening an once on closing - document.getElementById("chatBtn").classList.remove("somethingnew"); + // NOTE: action if provided is always a closing action + toggleChat: function(action) { + if (!action && document.getElementById("modalChat").checked) + // Entering chat + document.getElementById("inputChat").focus(); + else { + document.getElementById("chatBtn").classList.remove("somethingnew"); + if (!!this.game.mycolor) { + // Update "chatRead" variable either on server or locally + if (this.game.type == "corr") + this.updateCorrGame({ chatRead: this.game.mycolor }); + else if (this.game.type == "live") + GameStorage.update(this.gameRef, { chatRead: true }); + } + } }, processChat: function(chat) { 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) - this.updateCorrGame({ chat: chat }); + if (!!this.game.mycolor) { + if (this.game.type == "corr") + this.updateCorrGame({ chat: chat }); + else { + // Live game + chat.added = Date.now(); + GameStorage.update(this.gameRef, { chat: chat }); + } + } }, clearChat: function() { - // Nothing more to do if game is live (chats not recorded) - if (this.game.type == "corr") { - if (!!this.game.mycolor) { + if (!!this.game.mycolor) { + if (this.game.type == "corr") { ajax( "/chats", "DELETE", { data: { gid: this.game.id } } ); + } else { + // Live game + GameStorage.update(this.gameRef, { delchat: true }); } this.$set(this.game, "chats", []); } }, - // Notify turn after a new move (to opponent and me on MyGames page) - notifyTurn: function(sid) { - const player = this.people[sid]; - const colorIdx = this.game.players.findIndex( - p => p.sid == sid || p.uid == player.id); - const color = ["w","b"][colorIdx]; - const movesCount = this.game.moves.length; - const yourTurn = - (color == "w" && movesCount % 2 == 0) || - (color == "b" && movesCount % 2 == 1); - this.send("turnchange", { target: sid, yourTurn: yourTurn }); + getGameType: function(game) { + if (!!game.id.match(/^i/)) return "import"; + return game.cadence.indexOf("d") >= 0 ? "corr" : "live"; + }, + // Notify something after a new move (to opponent and me on MyGames page) + notifyMyGames: function(thing, data) { + this.send( + "notify" + thing, + { + data: data, + targets: this.game.players.map(p => { + return { sid: p.sid, id: p.id }; + }) + } + ); }, showNextGame: function() { // Did I play in current game? If not, add it to nextIds list @@ -420,13 +518,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.loadGame(); - 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(); @@ -439,60 +539,61 @@ export default { const data = JSON.parse(msg.data); switch (data.code) { case "pollclients": - data.sockIds.forEach(sid => { + // TODO: shuffling and random filtering on server, + // if the room is really crowded. + Object.keys(data.sockIds).forEach(sid => { if (sid != this.st.user.sid) { - this.people[sid] = { focus: true }; this.send("askidentity", { target: sid }); + this.people[sid] = { tmpIds: data.sockIds[sid] }; + } else { + // Complete my tmpIds: + Object.assign(this.people[sid].tmpIds, data.sockIds[sid]); } }); break; case "connect": - if (!this.people[data.from]) { - this.people[data.from] = { focus: true }; - this.newConnect[data.from] = true; //for self multi-connects tests - this.send("askidentity", { target: data.from }); + 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 } + } + } + ); + // For self multi-connects tests: + this.newConnect[data.from[0]] = 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); - break; - case "mconnect": { - // TODO: from MyGames page : send mconnect message with the list of gid (live and corr) - // Either me (another tab) or opponent - const sid = data.from; - if (!this.onMygames.some(s => s == sid)) - { - this.onMygames.push(sid); - this.notifyTurn(sid); //TODO: this may require server ID (so, notify after receiving identity) - } - break; - if (!this.people[sid]) - this.send("askidentity", { target: sid }); - } - case "mdisconnect": - ArrayFun.remove(this.onMygames, sid => sid == 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; } - case "killed": - // I logged in elsewhere: - this.conn = null; - alert(this.st.tr["New connexion detected: tab now offline"]); - break; case "askidentity": { // Request for identification const me = { @@ -507,68 +608,68 @@ 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 // If I multi-connect, kill current connexion if no mark (I'm older) if (this.newConnect[user.sid]) { + delete this.newConnect[user.sid]; if ( user.id > 0 && user.id == this.st.user.id && - user.sid != this.st.user.sid && - !this.killed[this.st.user.sid] + user.sid != this.st.user.sid ) { - this.send("killme", { sid: this.st.user.sid }); - this.killed[this.st.user.sid] = true; + this.cleanBeforeDestroy(); + alert(this.st.tr["New connexion detected: tab now offline"]); + break; } - delete this.newConnect[user.sid]; } - if (!this.killed[this.st.user.sid]) { - // Ask potentially missed last state, if opponent and I play - if ( - !!this.game.mycolor && - this.game.type == "live" && - this.game.score == "*" && - this.game.players.some(p => p.sid == user.sid) - ) { - this.send("asklastate", { target: user.sid }); - let counter = 1; - this.askLastate = setInterval( - () => { - // Ask at most 3 times: - // if no reply after that there should be a network issue. - if ( - counter < 3 && - !this.gotLastate && - !!this.people[user.sid] - ) { - this.send("asklastate", { target: user.sid }); - counter++; - } else { - clearInterval(this.askLastate); - } - }, - 1500 - ); - } + // Ask potentially missed last state, if opponent and I play + if ( + !this.gotLastate && + !!this.game.mycolor && + this.game.type == "live" && + this.game.score == "*" && + this.game.players.some(p => p.sid == user.sid) + ) { + this.send("asklastate", { target: user.sid }); + let counter = 1; + this.askLastate = setInterval( + () => { + // Ask at most 3 times: + // if no reply after that there should be a network issue. + if ( + counter < 3 && + !this.gotLastate && + !!this.people[user.sid] + ) { + this.send("asklastate", { target: user.sid }); + counter++; + } else { + clearInterval(this.askLastate); + } + }, + 1500 + ); } break; } case "askgame": - // Send current (live) game if not asked by any of the players + // Send current (live or import) game, + // if not asked by any of the players if ( - this.game.type == "live" && + this.game.type != "corr" && this.game.players.every(p => p.sid != data.from[0]) ) { const myGame = { id: this.game.id, + // FEN is current position, unused for now fen: this.game.fen, 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 }); } @@ -578,7 +679,7 @@ export default { .filter(k => [ "id","fen","players","vid","cadence","fenStart","vname", - "moves","clocks","initime","score","drawOffer","rematchOffer" + "moves","clocks","score","drawOffer","rematchOffer" ].includes(k)) .reduce( (obj, k) => { @@ -590,47 +691,29 @@ export default { this.send("fullgame", { data: gameToSend, target: data.from }); break; case "fullgame": - // Callback "roomInit" to poll clients only after game is loaded - this.loadGame(data.data, this.roomInit); + if (!!data.data.empty) { + alert(this.st.tr["The game should be in another tab"]); + this.$router.go(-1); + } + else + // Callback "roomInit" to poll clients only after game is loaded + this.loadVariantThenGame(data.data, this.roomInit); break; case "asklastate": // Sending informative last state if I played a move or score != "*" - if ( - (this.game.moves.length > 0 && this.vr.turn != this.game.mycolor) || - this.game.score != "*" || - this.drawOffer == "sent" || - this.rematchOffer == "sent" - ) { - // Send our "last state" informations to opponent - const L = this.game.moves.length; - const myIdx = ["w", "b"].indexOf(this.game.mycolor); - const myLastate = { - lastMove: L > 0 ? this.game.moves[L - 1] : undefined, - clock: this.game.clocks[myIdx], - // Since we played a move (or abort or resign), - // only drawOffer=="sent" is possible - drawSent: this.drawOffer == "sent", - rematchSent: this.rematchOffer == "sent", - score: this.game.score, - score: this.game.scoreMsg, - movesCount: L, - initime: this.game.initime[1 - myIdx] //relevant only if I played - }; - this.send("lastate", { data: myLastate, target: data.from }); - } else { - this.send("lastate", { data: {nothing: true}, target: data.from }); - } + // If the game or moves aren't loaded yet, delay the sending: + // TODO: socket init after game load, so the game is supposedly ready + if (!this.game || !this.game.moves) this.lastateAsked = true; + else this.sendLastate(data.from); break; case "lastate": { // Got opponent infos about last move this.gotLastate = true; - if (!data.data.nothing) { - this.lastate = data.data; - if (this.game.rendered) - // Game is rendered (Board component) - this.processLastate(); - // Else: will be processed when game is ready - } + this.lastate = data.data; + if (this.game.rendered) + // Game is rendered (Board component) + this.processLastate(); + // Else: will be processed when game is ready break; } case "newmove": { @@ -653,9 +736,25 @@ export default { } else { this.gotMoveIdx = movePlus.index; const receiveMyMove = (movePlus.color == this.game.mycolor); - if (!receiveMyMove && !!this.game.mycolor) + const moveColIdx = ["w", "b"].indexOf(movePlus.color); + if (!receiveMyMove && !!this.game.mycolor) { // Notify opponent that I got the move: - this.send("gotmove", {data: movePlus.index, target: data.from}); + this.send( + "gotmove", + { data: movePlus.index, target: data.from } + ); + // And myself if I'm elsewhere: + if (!this.focus) { + notify( + "New move", + { + body: + (this.game.players[moveColIdx].name || "@nonymous") + + " just played." + } + ); + } + } if (movePlus.cancelDrawOffer) { // Opponent refuses draw this.drawOffer = ""; @@ -665,16 +764,15 @@ 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); + this.$refs["basegame"].play( + movePlus.move, "received", null, true); + this.game.clocks[moveColIdx] = movePlus.clock; this.processMove( movePlus.move, - { - clock: movePlus.clock, - receiveMyMove: receiveMyMove - } + { receiveMyMove: receiveMyMove } ); } } @@ -682,15 +780,17 @@ export default { } case "gotmove": { this.opponentGotMove = true; - // Now his clock starts running: + // Now his clock starts running on my side: const oppIdx = ['w','b'].indexOf(this.vr.turn); - this.game.initime[oppIdx] = Date.now(); + // 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; } case "resign": - const score = data.side == "b" ? "1-0" : "0-1"; - const side = data.side == "w" ? "White" : "Black"; + const score = (data.data == "b" ? "1-0" : "0-1"); + const side = (data.data == "w" ? "White" : "Black"); this.gameOver(score, side + " surrender"); break; case "abort": @@ -702,61 +802,64 @@ export default { case "drawoffer": // NOTE: observers don't know who offered draw this.drawOffer = "received"; + if (!!this.game.mycolor && 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.mycolor && 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 const gameInfo = data.data; + const gameType = this.getGameType(gameInfo); if ( - gameInfo.players.some(p => - p.sid == this.st.user.sid || p.uid == this.st.user.id) + gameType == "live" && + gameInfo.players.some(p => p.sid == this.st.user.sid) + ) { + this.addAndGotoLiveGame(gameInfo); + } else if ( + gameType == "corr" && + gameInfo.players.some(p => p.id == this.st.user.id) ) { 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.infoMessage = - this.st.tr["Rematch in progress:"] + - " " + - "#/game/" + - gameInfo.id + urlRid + - ""; + this.rematchId = gameInfo.id; document.getElementById("modalInfo").checked = true; } break; } - case "newchat": - this.newChat = data.data; + case "newchat": { + let chat = data.data; + this.$refs["chatcomp"].newChat(chat); + if (this.game.type == "live") { + chat.added = Date.now(); + if (!!this.game.mycolor) + GameStorage.update(this.gameRef, { chat: chat }); + } if (!document.getElementById("modalChat").checked) document.getElementById("chatBtn").classList.add("somethingnew"); break; + } } }, - socketCloseListener: function() { - this.conn = new WebSocket(this.connexionString); - this.conn.addEventListener("message", this.socketMessageListener); - this.conn.addEventListener("close", this.socketCloseListener); - }, updateCorrGame: function(obj, callback) { ajax( "/games", "PUT", { data: { - gid: this.gameRef.id, + gid: this.gameRef, newObj: obj }, success: () => { @@ -765,26 +868,51 @@ export default { } ); }, + sendLastate: function(target) { + // Send our "last state" informations to opponent + const L = this.game.moves.length; + const myIdx = ["w", "b"].indexOf(this.game.mycolor); + const myLastate = { + lastMove: + (L > 0 && this.vr.turn != this.game.mycolor) + ? this.game.moves[L - 1] + : undefined, + clock: this.game.clocks[myIdx], + // Since we played a move (or abort or resign), + // only drawOffer=="sent" is possible + drawSent: this.drawOffer == "sent", + rematchSent: this.rematchOffer == "sent", + score: this.game.score != "*" ? this.game.score : undefined, + scoreMsg: this.game.score != "*" ? this.game.scoreMsg : undefined, + movesCount: L + }; + this.send("lastate", { data: myLastate, target: target }); + }, // lastate was received, but maybe game wasn't ready yet: processLastate: function() { const data = this.lastate; this.lastate = undefined; //security... const L = this.game.moves.length; + const oppIdx = 1 - ["w", "b"].indexOf(this.game.mycolor); + this.game.clocks[oppIdx] = data.clock; if (data.movesCount > L) { // Just got last move from him this.$refs["basegame"].play(data.lastMove, "received", null, true); - this.processMove(data.lastMove, { clock: data.clock }); + this.processMove(data.lastMove); + } else { + if (!!this.clockUpdate) clearInterval(this.clockUpdate); + this.re_setClocks(); } if (data.drawSent) this.drawOffer = "received"; if (data.rematchSent) this.rematchOffer = "received"; - if (data.score != "*") { + if (!!data.score) { this.drawOffer = ""; if (this.game.score == "*") this.gameOver(data.score, data.scoreMsg); } }, clickDraw: function() { - if (!this.game.mycolor) return; //I'm just spectator + if (!this.game.mycolor || this.game.type == "import") return; if (["received", "threerep"].includes(this.drawOffer)) { if (!confirm(this.st.tr["Accept draw?"])) return; const message = @@ -804,14 +932,39 @@ 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 }); } }, + addAndGotoLiveGame: function(gameInfo, callback) { + const game = Object.assign( + {}, + gameInfo, + { + // (other) Game infos: constant + fenStart: gameInfo.fen, + vname: this.game.vname, + created: Date.now(), + // Game state (including FEN): will be updated + moves: [], + clocks: [-1, -1], //-1 = unstarted + score: "*" + } + ); + GameStorage.add(game, (err) => { + // No error expected. + if (!err) { + if (this.st.settings.sound) + new Audio("/sounds/newgame.flac").play().catch(() => {}); + if (!!callback) callback(); + this.$router.push("/game/" + gameInfo.id); + } + }); + }, clickRematch: function() { - if (!this.game.mycolor) return; //I'm just spectator + if (!this.game.mycolor || this.game.type == "import") return; if (this.rematchOffer == "received") { // Start a new game! let gameInfo = { @@ -821,33 +974,17 @@ export default { vid: this.game.vid, cadence: this.game.cadence }; - let oppsid = this.getOppsid(); //may be null - this.send("rnewgame", { data: gameInfo, oppsid: oppsid }); - if (this.game.type == "live") { - const game = Object.assign( - {}, - gameInfo, - { - // (other) Game infos: constant - fenStart: gameInfo.fen, - vname: this.game.vname, - created: Date.now(), - // Game state (including FEN): will be updated - moves: [], - clocks: [-1, -1], //-1 = unstarted - initime: [0, 0], //initialized later - score: "*" - } - ); - GameStorage.add(game, (err) => { - // No error expected. - if (!err) { - if (this.st.settings.sound) - new Audio("/sounds/newgame.flac").play().catch(() => {}); - this.$router.push("/game/" + gameInfo.id); - } - }); - } + const notifyNewGame = () => { + const oppsid = this.getOppsid(); //may be null + this.send("rnewgame", { data: gameInfo, oppsid: oppsid }); + // To main Hall if corr game: + if (this.game.type == "corr") + this.send("newgame", { data: gameInfo, page: "/" }); + // Also to MyGames page: + this.notifyMyGames("newgame", gameInfo); + }; + if (this.game.type == "live") + this.addAndGotoLiveGame(gameInfo, notifyNewGame); else { // corr game ajax( @@ -858,6 +995,7 @@ export default { data: { gameInfo: gameInfo }, success: (response) => { gameInfo.id = response.gameId; + notifyNewGame(); this.$router.push("/game/" + response.gameId); } } @@ -868,7 +1006,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 }); @@ -878,14 +1016,15 @@ 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' }); } }, abortGame: function() { - if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) return; + if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) + return; this.gameOver("?", "Stop"); this.send("abort"); }, @@ -893,290 +1032,270 @@ export default { if (!this.game.mycolor || !confirm(this.st.tr["Resign the game?"])) return; this.send("resign", { data: this.game.mycolor }); - const score = this.game.mycolor == "w" ? "0-1" : "1-0"; - const side = this.game.mycolor == "w" ? "White" : "Black"; + 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) - // - 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) { - const afterRetrieval = async game => { - const vModule = await import("@/variants/" + game.vname + ".js"); - window.V = vModule.VariantRules; - this.vr = new V(game.fen); - const gtype = game.cadence.indexOf("d") >= 0 ? "corr" : "live"; - const tc = extractTime(game.cadence); - const myIdx = game.players.findIndex(p => { - return p.sid == this.st.user.sid || p.uid == this.st.user.id; - }); - const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers - if (!game.chats) game.chats = []; //live games don't have chat history - if (gtype == "corr") { - if (game.players[0].color == "b") { - // Adopt the same convention for live and corr games: [0] = white - [game.players[0], game.players[1]] = [ - game.players[1], - game.players[0] - ]; - } - // NOTE: clocks in seconds, initime in milliseconds - game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of - game.clocks = [tc.mainTime, tc.mainTime]; - const L = game.moves.length; - if (game.score == "*") { - // Set clocks + initime - game.initime = [0, 0]; - if (L >= 1) { - const gameLastupdate = game.moves[L-1].played; - game.initime[L % 2] = gameLastupdate; - if (L >= 2) { - game.clocks[L % 2] = - tc.mainTime - (Date.now() - gameLastupdate) / 1000; - } - } - } - // Sort chat messages from newest to oldest - game.chats.sort((c1, c2) => { - return c2.added - c1.added; - }); - if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) { - // Did a chat message arrive after my last move? - let dtLastMove = 0; - if (L == 1 && myIdx == 0) - dtLastMove = game.moves[0].played; - else if (L >= 2) { - if (L % 2 == 0) { - // It's now white turn - dtLastMove = game.moves[L-1-(1-myIdx)].played; - } else { - // Black turn: - dtLastMove = game.moves[L-1-myIdx].played; - } - } - if (dtLastMove < game.chats[0].added) - document.getElementById("chatBtn").classList.add("somethingnew"); + const gtype = game.type || this.getGameType(game); + const tc = extractTime(game.cadence); + const myIdx = game.players.findIndex(p => { + return p.sid == this.st.user.sid || p.id == this.st.user.id; + }); + // "mycolor" is undefined for observers + const mycolor = [undefined, "w", "b"][myIdx + 1]; + if (gtype == "corr") { + if (mycolor == 'w') game.chatRead = game.chatReadWhite; + else if (mycolor == 'b') game.chatRead = game.chatReadBlack; + // NOTE: clocks in seconds + game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of + game.clocks = [tc.mainTime, tc.mainTime]; + const L = game.moves.length; + if (game.score == "*") { + // Adjust clocks + if (L >= 2) { + game.clocks[L % 2] -= + (Date.now() - game.moves[L-1].played) / 1000; } - // Now that we used idx and played, re-format moves as for live games - game.moves = game.moves.map(m => m.squares); } - if (gtype == "live" && game.clocks[0] < 0) { - // Game is unstarted + // Now that we used idx and played, re-format moves as for live games + game.moves = game.moves.map(m => m.squares); + } + else if (gtype == "live") { + if (game.clocks[0] < 0) { + // Game is unstarted. clock is ignored until move 2 game.clocks = [tc.mainTime, tc.mainTime]; - if (game.score == "*") { - game.initime[0] = Date.now(); - if (myIdx >= 0) { - // I play in this live game; corr games don't have clocks+initime - GameStorage.update(game.id, { - clocks: game.clocks, - initime: game.initime - }); - } - } - } - // TODO: merge next 2 "if" conditions - if (!!game.drawOffer) { - if (game.drawOffer == "t") - // Three repetitions - this.drawOffer = "threerep"; - else { - // Draw offered by any of the players: - if (myIdx < 0) this.drawOffer = "received"; - else { - // I play in this game: - if ( - (game.drawOffer == "w" && myIdx == 0) || - (game.drawOffer == "b" && myIdx == 1) - ) - this.drawOffer = "sent"; - else this.drawOffer = "received"; - } + if (myIdx >= 0) { + // I play in this live game + GameStorage.update(game.id, { + clocks: game.clocks + }); } + } else { + if (!!game.initime) + // It's my turn: clocks not updated yet + game.clocks[myIdx] -= (Date.now() - game.initime) / 1000; } - if (!!game.rematchOffer) { - if (myIdx < 0) this.rematchOffer = "received"; + } + else + // gtype == "import" + game.clocks = [tc.mainTime, tc.mainTime]; + // Live games before 26/03/2020 don't have chat history: + if (!game.chats) game.chats = []; //TODO: remove line + // Sort chat messages from newest to oldest + game.chats.sort((c1, c2) => c2.added - c1.added); + if ( + myIdx >= 0 && + game.chats.length > 0 && + (!game.chatRead || game.chatRead < game.chats[0].added) + ) { + // A chat message arrived since my last reading: + document.getElementById("chatBtn").classList.add("somethingnew"); + } + // TODO: merge next 2 "if" conditions + if (!!game.drawOffer) { + if (game.drawOffer == "t") + // Three repetitions + this.drawOffer = "threerep"; + else { + // Draw offered by any of the players: + if (myIdx < 0) this.drawOffer = "received"; else { // I play in this game: if ( - (game.rematchOffer == "w" && myIdx == 0) || - (game.rematchOffer == "b" && myIdx == 1) + (game.drawOffer == "w" && myIdx == 0) || + (game.drawOffer == "b" && myIdx == 1) ) - this.rematchOffer = "sent"; - else this.rematchOffer = "received"; + this.drawOffer = "sent"; + else this.drawOffer = "received"; } } - this.repeat = {}; //reset: scan past moves' FEN: - let repIdx = 0; - let vr_tmp = new V(game.fenStart); - let curTurn = "n"; - game.moves.forEach(m => { - playMove(m, vr_tmp); - const fenIdx = vr_tmp.getFen().replace(/ /g, "_"); - this.repeat[fenIdx] = this.repeat[fenIdx] - ? this.repeat[fenIdx] + 1 - : 1; - }); - if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep"; - this.game = Object.assign( - // NOTE: assign mycolor here, since BaseGame could also be VS computer - { - type: gtype, - increment: tc.increment, - mycolor: mycolor, - // opponent sid not strictly required (or available), but easier - // at least oppsid or oppid is available anyway: - oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid, - oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].uid - }, - game, - ); - if (this.gameIsLoading) - // Re-load game because we missed some moves: - // artificially reset BaseGame (required if moves arrived in wrong order) - this.$refs["basegame"].re_setVariables(); + } + if (!!game.rematchOffer) { + if (myIdx < 0) this.rematchOffer = "received"; else { - // Initial loading: - this.gotMoveIdx = game.moves.length - 1; - // If we arrive here after 'nextGame' action, the board might be hidden - let boardDiv = document.querySelector(".game"); - if (!!boardDiv && boardDiv.style.visibility == "hidden") - boardDiv.style.visibility = "visible"; - } - this.re_setClocks(); - this.$nextTick(() => { - this.game.rendered = true; - // Did lastate arrive before game was rendered? - if (this.lastate) this.processLastate(); - }); - if (this.gameIsLoading) { - this.gameIsLoading = false; - if (this.gotMoveIdx >= game.moves.length) - // Some moves arrived meanwhile... - this.askGameAgain(); + // I play in this game: + if ( + (game.rematchOffer == "w" && myIdx == 0) || + (game.rematchOffer == "b" && myIdx == 1) + ) + this.rematchOffer = "sent"; + else this.rematchOffer = "received"; } - if (!!callback) 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) => { - let g = res.game; - g.moves.forEach(m => { - m.squares = JSON.parse(m.squares); - }); - afterRetrieval(g); - } + this.repeat = {}; //reset: scan past moves' FEN: + let repIdx = 0; + this.vr = new V(game.fenStart); + let curTurn = "n"; + game.moves.forEach(m => { + playMove(m, this.vr); + const fenIdx = this.vr.getFenForRepeat(); + this.repeat[fenIdx] = this.repeat[fenIdx] + ? this.repeat[fenIdx] + 1 + : 1; + }); + // Imported games don't have current FEN + if (!game.fen) game.fen = this.vr.getFen(); + if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep"; + this.game = Object.assign( + // NOTE: assign mycolor here, since BaseGame could also be VS computer + { + type: gtype, + increment: tc.increment, + mycolor: mycolor, + // opponent sid not strictly required (or available), but easier + // at least oppsid or oppid is available anyway: + oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid, + oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].id + }, + game + ); + this.$refs["basegame"].re_setVariables(this.game); + if (!this.gameIsLoading) { + // Initial loading: + this.gotMoveIdx = game.moves.length - 1; + // If we arrive here after 'nextGame' action, the board might be hidden + let boardDiv = document.querySelector(".game"); + if (!!boardDiv && boardDiv.style.visibility == "hidden") + boardDiv.style.visibility = "visible"; + } + this.re_setClocks(); + this.$nextTick(() => { + this.game.rendered = true; + // Did lastate arrive before game was rendered? + if (this.lastate) this.processLastate(); + }); + if (this.lastateAsked) { + this.lastateAsked = false; + this.sendLastate(game.oppsid); + } + if (this.gameIsLoading) { + this.gameIsLoading = false; + if (this.gotMoveIdx >= game.moves.length) + // Some moves arrived meanwhile... + this.askGameAgain(); + } + if (!!callback) callback(); + }, + 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) { + +console.log("fecth"); + console.log(this.gameRef); + console.log(this.gameRef.match(/^i/)); + + 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 if (!!this.gameRef.match(/^i/)) + // Game import (maybe remote) + ImportgameStorage.get(this.gameRef, callback); + else + // Local live game (or remote) + GameStorage.get(this.gameRef, callback); }, re_setClocks: function() { + this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':')); if (this.game.moves.length < 2 || this.game.score != "*") { // 1st move not completed yet, or game over: freeze time - this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':')); return; } const currentTurn = this.vr.turn; const currentMovesCount = this.game.moves.length; const colorIdx = ["w", "b"].indexOf(currentTurn); - let countdown = - this.game.clocks[colorIdx] - - (Date.now() - this.game.initime[colorIdx]) / 1000; - this.virtualClocks = [0, 1].map(i => { - const removeTime = - i == colorIdx ? (Date.now() - this.game.initime[colorIdx]) / 1000 : 0; - return ppt(this.game.clocks[i] - removeTime).split(':'); - }); this.clockUpdate = setInterval( () => { if ( - countdown < 0 || + this.game.clocks[colorIdx] < 0 || this.game.moves.length > currentMovesCount || this.game.score != "*" ) { clearInterval(this.clockUpdate); - if (countdown < 0) + this.clockUpdate = null; + if (this.game.clocks[colorIdx] < 0) this.gameOver( currentTurn == "w" ? "0-1" : "1-0", "Time" ); - } else + } else { this.$set( this.virtualClocks, colorIdx, - ppt(Math.max(0, --countdown)).split(':') + ppt(Math.max(0, --this.game.clocks[colorIdx])).split(':') ); + } }, 1000 ); }, // Update variables and storage after a move: processMove: function(move, data) { + if (this.game.type == "import") + // Shouldn't receive any messages in this mode: + return; if (!data) data = {}; const moveCol = this.vr.turn; + const colorIdx = ["w", "b"].indexOf(moveCol); + const nextIdx = 1 - colorIdx; const doProcessMove = () => { - const colorIdx = ["w", "b"].indexOf(moveCol); - const nextIdx = 1 - colorIdx; const origMovescount = this.game.moves.length; - let addTime = 0; //for live games + // 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 this.drawOffer = ""; if (this.game.type == "live" && origMovescount >= 2) { - const elapsed = Date.now() - this.game.initime[colorIdx]; - // elapsed time is measured in milliseconds - addTime = this.game.increment - elapsed / 1000; + this.game.clocks[colorIdx] += this.game.increment; + // For a correct display in casqe of disconnected opponent: + this.$set( + this.virtualClocks, + colorIdx, + ppt(this.game.clocks[colorIdx]).split(':') + ); + GameStorage.update(this.gameRef, { + // It's not my turn anymore: + initime: null + }); } } // Update current game object: playMove(move, this.vr); - // The move is played: stop clock - clearInterval(this.clockUpdate); - if (!data.score) { - // Received move, score has not been computed in BaseGame (!!noemit) - const score = this.vr.getCurrentScore(); - if (score != "*") this.gameOver(score); - } -// TODO: notifyTurn: "changeturn" message + 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") { - if (!!data.clock) this.game.clocks[colorIdx] = data.clock; - else this.game.clocks[colorIdx] += addTime; - } - // In corr games, just reset clock to mainTime: - else { + if (this.game.type == "corr") { + // In corr games, just reset clock to mainTime: this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime; } - // NOTE: opponent's initime is reset after "gotmove" is received - if ( - !this.game.mycolor || - moveCol != this.game.mycolor || - !!data.receiveMyMove - ) { - this.game.initime[nextIdx] = Date.now(); - } // If repetition detected, consider that a draw offer was received: const fenObj = this.vr.getFenForRepeat(); this.repeat[fenObj] = @@ -1189,8 +1308,31 @@ export default { // NOTE: 'var' to see that variable outside this block var filtered_move = getFilteredMove(move); } + if (moveCol == this.game.mycolor && !data.receiveMyMove) { + // Notify turn on MyGames page: + this.notifyMyGames( + "turn", + { + gid: this.gameRef, + turn: this.vr.turn + } + ); + } // Since corr games are stored at only one location, update should be // done only by one player for each move: + if ( + this.game.type == "live" && + !!this.game.mycolor && + moveCol != this.game.mycolor && + this.game.moves.length >= 2 + ) { + // Receive a move: update initime + this.game.initime = Date.now(); + GameStorage.update(this.gameRef, { + // It's my turn now! + initime: this.game.initime + }); + } if ( !!this.game.mycolor && !data.receiveMyMove && @@ -1214,7 +1356,6 @@ export default { fen: this.game.fen, move: { squares: filtered_move, - played: Date.now(), idx: origMovescount }, // Code "n" for "None" to force reset (otherwise it's ignored) @@ -1223,17 +1364,16 @@ export default { } else { const updateStorage = () => { - GameStorage.update(this.gameRef.id, { + GameStorage.update(this.gameRef, { fen: this.game.fen, move: filtered_move, moveIdx: origMovescount, clocks: this.game.clocks, - initime: this.game.initime, drawOffer: drawCode }); }; // The active tab can update storage immediately - if (!document.hidden) updateStorage(); + if (this.focus) updateStorage(); // Small random delay otherwise else setTimeout(updateStorage, 500 + 1000 * Math.random()); } @@ -1243,12 +1383,14 @@ export default { let sendMove = { move: filtered_move, index: origMovescount, - // color is required to check if this is my move (if several tabs opened) + // color is required to check if this is my move + // (if several tabs opened) color: moveCol, cancelDrawOffer: this.drawOffer == "" }; if (this.game.type == "live") sendMove["clock"] = this.game.clocks[colorIdx]; + // (Live) Clocks will re-start when the opponent pingback arrive this.opponentGotMove = false; this.send("newmove", {data: sendMove}); // If the opponent doesn't reply gotmove soon enough, re-send move: @@ -1296,6 +1438,7 @@ export default { // The board might have been hidden: if (boardDiv.style.visibility == "hidden") boardDiv.style.visibility = "visible"; + if (data.score == "*") this.re_setClocks(); } }; let el = document.querySelector("#buttonsConfirm > .acceptBtn"); @@ -1352,7 +1495,7 @@ export default { this.game.scoreMsg = scoreMsg; this.$set(this.game, "scoreMsg", scoreMsg); const myIdx = this.game.players.findIndex(p => { - return p.sid == this.st.user.sid || p.uid == this.st.user.id; + return p.sid == this.st.user.sid || p.id == this.st.user.id; }); if (myIdx >= 0) { // OK, I play in this game @@ -1361,12 +1504,28 @@ export default { scoreMsg: scoreMsg }; if (this.game.type == "live") { - GameStorage.update(this.gameRef.id, scoreObj); + GameStorage.update(this.gameRef, scoreObj); + // Notify myself locally if I'm elsewhere: + if (!this.focus) { + notify( + "Game over", + { body: score + " : " + scoreMsg } + ); + } if (!!callback) callback(); } else this.updateCorrGame(scoreObj, callback); - // Notify the score to main Hall. TODO: only one player (currently double send) + // Notify the score to main Hall. + // TODO: only one player (currently double send) this.send("result", { gid: this.game.id, score: score }); + // Also to MyGames page (TODO: doubled as well...) + this.notifyMyGames( + "score", + { + gid: this.gameRef, + score: score + } + ); } else if (!!callback) callback(); } @@ -1408,7 +1567,7 @@ button margin: 0 display: inline-flex img - height: 24px + height: 22px display: flex @media screen and (max-width: 767px) height: 18px