X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FGame.vue;h=fbdf4e635ec503faa464e88f2f43cbb61d1e7197;hb=d6f08e5642e0bdbe3805ffd1a155ecc753178d1e;hp=6c289e61be7fa380368d30a16b98a8194d8699df;hpb=1051336271ad4d4128ef1f66953f54973601d774;p=vchess.git diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 6c289e61..fbdf4e63 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -14,10 +14,10 @@ main span {{ Object.keys(people).length + " " + st.tr["participant(s):"] }} span( v-for="p in Object.values(people)" - v-if="!!p.name" + v-if="p.name" ) | {{ p.name }} - span.anonymous(v-if="Object.values(people).some(p => !p.name)") + span.anonymous(v-if="Object.values(people).some(p => !p.name && p.id === 0)") | + @nonymous Chat( :players="game.players" @@ -109,6 +109,14 @@ export default { repeat: {}, //detect position repetition newChat: "", conn: null, + roomInitialized: false, + // If newmove has wrong index: ask fullgame again: + gameIsLoading: false, + // If asklastate got no reply, ask again: + gotLastate: false, + gotMoveIdx: -1, //last move index received + // If newmove got no pingback, send again: + opponentGotMove: false, connexionString: "", // Related to (killing of) self multi-connects: newConnect: {}, @@ -171,13 +179,18 @@ export default { }, methods: { roomInit: function() { - // Notify the room only now that I connected, because - // messages might be lost otherwise (if game loading is slow) - this.send("connect"); - this.send("pollclients"); + if (!this.roomInitialized) { + // Notify the room only now that I connected, because + // messages might be lost otherwise (if game loading is slow) + this.send("connect"); + this.send("pollclients"); + // We may ask fullgame several times if some moves are lost, + // but room should be init only once: + this.roomInitialized = true; + } }, send: function(code, obj) { - if (this.conn) + if (!!this.conn) this.conn.send(JSON.stringify(Object.assign({ code: code }, obj))); }, isConnected: function(index) { @@ -211,9 +224,9 @@ export default { 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) ajax("/chats", "DELETE", {gid: this.game.id}); - this.game.chats = []; + this.$set(this.game, "chats", []); } }, // Notify turn after a new move (to opponent and me on MyGames page) @@ -222,44 +235,40 @@ export default { const colorIdx = this.game.players.findIndex( p => p.sid == sid || p.id == player.id); const color = ["w","b"][colorIdx]; + const movesCount = this.game.moves.length; const yourTurn = - ( - color == "w" && - this.game.movesCount % 2 == 0 - ) - || - ( - color == "b" && - this.game.movesCount % 2 == 1 - ); + (color == "w" && movesCount % 2 == 0) || + (color == "b" && movesCount % 2 == 1); this.send("turnchange", { target: sid, yourTurn: yourTurn }); }, + askGameAgain: function() { + this.gameIsLoading = true; + if (!this.gameRef.rid) + // This is my game: just reload. + this.loadGame(); + else { + // Just ask fullgame again (once!), this is much simpler. + // If this fails, the user could just reload page :/ + let self = this; + (function askIfPeerConnected() { + if (!!self.people[self.gameRef.rid]) + self.send("askfullgame", { target: self.gameRef.rid }); + else setTimeout(askIfPeerConnected, 1000); + })(); + } + }, socketMessageListener: function(msg) { if (!this.conn) return; const data = JSON.parse(msg.data); switch (data.code) { case "pollclients": data.sockIds.forEach(sid => { - this.$set(this.people, sid, { id: 0, name: "" }); - if (sid != this.st.user.sid) { + if (sid != this.st.user.sid) this.send("askidentity", { target: 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 == sid) - ) { - this.send("asklastate", { target: sid }); - } - } }); break; case "connect": - if (!this.people[data.from]) - // TODO: people array should be init only after identity is known - this.$set(this.people, data.from, { name: "", id: 0 }); - if (!this.people[data.from].name) { + if (!this.people[data.from]) { this.newConnect[data.from] = true; //for self multi-connects tests this.send("askidentity", { target: data.from }); } @@ -267,8 +276,7 @@ export default { case "disconnect": this.$delete(this.people, data.from); break; - case "mconnect": - { + 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; @@ -290,7 +298,7 @@ export default { alert(this.st.tr["New connexion detected: tab now offline"]); break; case "askidentity": { - // Request for identification (TODO: anonymous shouldn't need to reply) + // Request for identification const me = { // Decompose to avoid revealing email name: this.st.user.name, @@ -301,30 +309,43 @@ export default { break; } case "identity": { - // TODO: init people array here. const user = data.data; - if (user.name) { - // If I multi-connect, kill current connexion if no mark (I'm older) + this.$set(this.people, user.sid, { name: user.name, id: user.id }); + // If I multi-connect, kill current connexion if no mark (I'm older) + if (this.newConnect[user.sid]) { if ( - this.newConnect[user.sid] && user.id > 0 && user.id == this.st.user.id && - user.sid != this.st.user.sid + user.sid != this.st.user.sid && + !this.killed[this.st.user.sid] ) { - if (!this.killed[this.st.user.sid]) { this.send("killme", { sid: this.st.user.sid }); this.killed[this.st.user.sid] = true; - } } - if (user.sid != this.st.user.sid) { - //I already know my identity... - this.$set(this.people, user.sid, { - id: user.id, - name: user.name - }); + 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) + ) { + let self = this; + (function askLastate() { + self.send("asklastate", { target: user.sid }); + setTimeout( + () => { + // Ask until we got a reply (or opponent disconnect): + if (!self.gotLastate && !!self.people[user.sid]) + askLastate(); + }, + 750 + ); + })(); } } - delete this.newConnect[user.sid]; break; } case "askgame": @@ -350,14 +371,10 @@ export default { break; case "fullgame": // Callback "roomInit" to poll clients only after game is loaded - let game = data.data; - // Move format isn't the same in storage and in browser, - // because of the 'addTime' field. - game.moves = game.moves.map(m => { return m.move || m; }); - this.loadGame(game, this.roomInit); + this.loadGame(data.data, this.roomInit); break; case "asklastate": - // Sending last state if I played a move or score != "*" + // 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 != "*" || @@ -367,8 +384,8 @@ export default { const L = this.game.moves.length; const myIdx = ["w", "b"].indexOf(this.game.mycolor); const myLastate = { - // NOTE: lastMove (when defined) includes addTime lastMove: L > 0 ? this.game.moves[L - 1] : undefined, + addTime: L > 0 ? this.game.addTimes[L - 1] : undefined, // Since we played a move (or abort or resign), // only drawOffer=="sent" is possible drawSent: this.drawOffer == "sent", @@ -377,29 +394,71 @@ export default { 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 }); } break; - case "lastate": //got opponent infos about last move - this.lastate = data.data; - if (this.game.rendered) - // Game is rendered (Board component) - this.processLastate(); - // Else: will be processed when game is ready + 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 + } break; + } case "newmove": { - const move = data.data; - if (move.cancelDrawOffer) { - // Opponent refuses draw - this.drawOffer = ""; - // NOTE for corr games: drawOffer reset by player in turn - if (this.game.type == "live" && !!this.game.mycolor) - GameStorage.update(this.gameRef.id, { drawOffer: "" }); + const movePlus = data.data; + const movesCount = this.game.moves.length; + if (movePlus.index > movesCount) { + // This can only happen if I'm an observer and missed a move. + this.gotMoveIdx = movePlus.index; + if (!this.gameIsLoading) this.askGameAgain(); + } + else { + if ( + movePlus.index < movesCount || + this.gotMoveIdx >= movePlus.index + ) { + // Opponent re-send but we already have the move: + // (maybe he didn't receive our pingback...) + this.send("gotmove", {data: movePlus.index, target: data.from}); + } else { + this.gotMoveIdx = movePlus.index; + const receiveMyMove = (movePlus.color == this.game.mycolor); + if (!receiveMyMove && !!this.game.mycolor) + // Notify opponent that I got the move: + this.send("gotmove", {data: movePlus.index, target: data.from}); + if (movePlus.cancelDrawOffer) { + // Opponent refuses draw + this.drawOffer = ""; + // NOTE for corr games: drawOffer reset by player in turn + if ( + this.game.type == "live" && + !!this.game.mycolor && + !receiveMyMove + ) { + GameStorage.update(this.gameRef.id, { drawOffer: "" }); + } + } + this.$refs["basegame"].play( + movePlus.move, + "received", + null, + { + addTime: movePlus.addTime, + receiveMyMove: receiveMyMove + } + ); + } } - this.$refs["basegame"].play( - move.move, - "received", - null, - {addTime: move.addTime}); + break; + } + case "gotmove": { + this.opponentGotMove = true; break; } case "resign": @@ -437,10 +496,11 @@ export default { if (data.movesCount > L) { // Just got last move from him this.$refs["basegame"].play( - data.lastMove.move, + data.lastMove, "received", null, - {addTime: data.lastMove.addTime, initime: data.initime}); + {addTime: data.addTime, initime: data.initime} + ); } if (data.drawSent) this.drawOffer = "received"; if (data.score != "*") { @@ -460,7 +520,7 @@ export default { this.gameOver("1/2", message); } else if (this.drawOffer == "") { // No effect if drawOffer == "sent" - if (this.game.mycolor != this.vr.turn) { + if (!!this.game.mycolor != this.vr.turn) { alert(this.st.tr["Draw offer only in your turn"]); return; } @@ -517,8 +577,10 @@ export default { if (L >= 1) { const gameLastupdate = game.moves[L-1].played; game.initime[L % 2] = gameLastupdate; - if (L >= 2) - game.clocks[L % 2] = Date.now() - gameLastupdate; + if (L >= 2) { + game.clocks[L % 2] = + tc.mainTime - (Date.now() - gameLastupdate) / 1000; + } } } // Sort chat messages from newest to oldest @@ -599,19 +661,29 @@ export default { // 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, - movesCount: game.moves.length + addTimes: [], //used for live games }, game, ); + if (this.gameIsLoading) + // Re-load game because we missed some moves: + // artificially reset BaseGame (required if moves arrive too quickly) + this.$refs["basegame"].re_setVariables(); this.re_setClocks(); this.$nextTick(() => { this.game.rendered = true; // Did lastate arrive before game was rendered? if (this.lastate) this.processLastate(); }); - if (callback) callback(); + if (this.gameIsLoading) { + this.gameIsLoading = false; + if (this.gotMoveIdx >= game.moves.length) + // Some moves arrived meanwhile... + this.askGameAgain(); + } + if (!!callback) callback(); }; - if (game) { + if (!!game) { afterRetrieval(game); return; } @@ -625,7 +697,7 @@ export default { } }, re_setClocks: function() { - if (this.game.movesCount < 2 || this.game.score != "*") { + 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)); return; @@ -662,66 +734,55 @@ export default { }, 1000); }, // Post-process a (potentially partial) move (which was just played in BaseGame) - // TODO?: wait for AJAX return to finish processing a move, - // and for opponent pingback in case of live game : if none received after e.g. 500ms, re-send newmove - // ...and provide move index with newmove event for basic check after receiving processMove: function(move, data) { + if (!data) data = {}; const moveCol = this.vr.turn; const doProcessMove = () => { const colorIdx = ["w", "b"].indexOf(moveCol); const nextIdx = 1 - colorIdx; - if (this.game.mycolor) { - // NOTE: 'var' to see that variable outside this block - var filtered_move = getFilteredMove(move); - } - // Send move ("newmove" event) to people in the room (if our turn) - let addTime = (data && this.game.type == "live") ? data.addTime : 0; - if (moveCol == this.game.mycolor) { + const origMovescount = this.game.moves.length; + let addTime = + this.game.type == "live" + ? (data.addTime || 0) + : undefined; + if (moveCol == this.game.mycolor && !data.receiveMyMove) { if (this.drawOffer == "received") // I refuse draw this.drawOffer = ""; - // 'addTime' is irrelevant for corr games: - if (this.game.type == "live" && this.game.movesCount >= 2) { + 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; } - const sendMove = { - move: filtered_move, - addTime: addTime, //undefined for corr games - cancelDrawOffer: this.drawOffer == "", - // Players' SID required for /mygames page - // TODO: precompute and add this field to game object? - players: this.game.players.map(p => p.sid) - }; - this.send("newmove", { data: sendMove }); } - // Update current game object (no need for moves stack): + // Update current game object: playMove(move, this.vr); - this.game.movesCount++; - // TODO: notifyTurn +// TODO: notifyTurn: "changeturn" message + this.game.moves.push(move); // (add)Time indication: useful in case of lastate infos requested - this.game.moves.push(this.game.type == "live" - ? {move:move, addTime:addTime} - : move); + if (this.game.type == "live") + this.game.addTimes.push(addTime); this.game.fen = this.vr.getFen(); if (this.game.type == "live") this.game.clocks[colorIdx] += addTime; // In corr games, just reset clock to mainTime: else this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime; // data.initime is set only when I receive a "lastate" move from opponent - this.game.initime[nextIdx] = (data && data.initime) ? data.initime : Date.now(); + this.game.initime[nextIdx] = data.initime || Date.now(); this.re_setClocks(); // If repetition detected, consider that a draw offer was received: - const fenObj = V.ParseFen(this.game.fen); - let repIdx = fenObj.position + "_" + fenObj.turn; - if (fenObj.flags) repIdx += "_" + fenObj.flags; - this.repeat[repIdx] = this.repeat[repIdx] ? this.repeat[repIdx] + 1 : 1; - if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep"; + const fenObj = this.vr.getFenForRepeat(); + this.repeat[fenObj] = this.repeat[fenObj] ? this.repeat[fenObj] + 1 : 1; + if (this.repeat[fenObj] >= 3) this.drawOffer = "threerep"; else if (this.drawOffer == "threerep") this.drawOffer = ""; // Since corr games are stored at only one location, update should be // done only by one player for each move: + if (!!this.game.mycolor && !data.receiveMyMove) { + // NOTE: 'var' to see that variable outside this block + var filtered_move = getFilteredMove(move); + } if ( - this.game.mycolor && + !!this.game.mycolor && + !data.receiveMyMove && (this.game.type == "live" || moveCol == this.game.mycolor) ) { let drawCode = ""; @@ -742,14 +803,14 @@ export default { move: { squares: filtered_move, played: Date.now(), - idx: this.game.moves.length - 1 + idx: origMovescount }, // Code "n" for "None" to force reset (otherwise it's ignored) drawOffer: drawCode || "n" }); } - else { - // Live game: + else if (!document.hidden) { + // Live game: consider only the active tab GameStorage.update(this.gameRef.id, { fen: this.game.fen, move: filtered_move, @@ -759,9 +820,45 @@ export default { }); } } + // Send move ("newmove" event) to people in the room (if our turn) + if (moveCol == this.game.mycolor && !data.receiveMyMove) { + const sendMove = { + move: filtered_move, + index: origMovescount, + // color is required to check if this is my move (if several tabs opened) + color: moveCol, + addTime: addTime, //undefined for corr games + cancelDrawOffer: this.drawOffer == "" + }; + this.opponentGotMove = false; + this.send("newmove", {data: sendMove}); + // If the opponent doesn't reply gotmove soon enough, re-send move: + let retrySendmove = setInterval( () => { + if (this.opponentGotMove) { + clearInterval(retrySendmove); + return; + } + let oppsid = this.game.players[nextIdx].sid; + if (!oppsid) { + oppsid = Object.keys(this.people).find( + sid => this.people[sid].id == this.game.players[nextIdx].uid + ); + } + if (!oppsid || !this.people[oppsid]) + // Opponent is disconnected: he'll ask last state + clearInterval(retrySendmove); + else this.send("newmove", {data: sendMove, target: oppsid}); + }, 750); + } }; - if (this.game.type == "corr" && moveCol == this.game.mycolor) { + if ( + this.game.type == "corr" && + moveCol == this.game.mycolor && + !data.receiveMyMove + ) { setTimeout(() => { + // TODO: remplacer cette confirm box par qqch de plus discret + // (et de même pour challenge accepté / refusé) if ( !confirm( this.st.tr["Move played:"] +