X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FGame.vue;h=5c4630b018a71dced0340e3996df9a4bb2a885ad;hb=feaf1bf73fa8c6054e353585dee0b8a4fdcfbc4e;hp=0741d31753f8df5c66cd067b91c5ba09516413c1;hpb=e5c1d0fb0ed18632e9172c932d6e5c2c8c5742b8;p=vchess.git diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 0741d317..5c4630b0 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -30,32 +30,62 @@ main #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-name {{ game.vname }} - button#chatBtn(onClick="window.doClick('modalChat')") Chat + span#nextGame( + v-if="nextIds.length > 0" + @click="showNextGame()" + ) + | {{ st.tr["Next_g"] }} + button#chatBtn.tooltip( + onClick="window.doClick('modalChat')" + aria-label="Chat" + ) + img(src="/images/icons/chat.svg") #actions(v-if="game.score=='*'") - button( + button.tooltip( @click="clickDraw()" :class="{['draw-' + drawOffer]: true}" + :aria-label="st.tr['Draw']" ) - | {{ st.tr["Draw"] }} - button( + img(src="/images/icons/draw.svg") + button.tooltip( v-if="!!game.mycolor" @click="abortGame()" + :aria-label="st.tr['Abort']" ) - | {{ st.tr["Abort"] }} - button( + img(src="/images/icons/abort.svg") + button.tooltip( v-if="!!game.mycolor" @click="resign()" + :aria-label="st.tr['Resign']" ) - | {{ st.tr["Resign"] }} + img(src="/images/icons/resign.svg") + button.tooltip( + v-else-if="!!game.mycolor" + @click="rematch()" + :aria-label="st.tr['Rematch']" + ) + img(src="/images/icons/rematch.svg") #playersInfo p span.name(:class="{connected: isConnected(0)}") | {{ game.players[0].name || "@nonymous" }} - span.time(v-if="game.score=='*'") {{ virtualClocks[0] }} + 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.split-names - span.name(:class="{connected: isConnected(1)}") | {{ game.players[1].name || "@nonymous" }} - span.time(v-if="game.score=='*'") {{ virtualClocks[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" @@ -100,7 +130,8 @@ export default { chats: [], rendered: false }, - virtualClocks: [0, 0], //initialized with true game.clocks + nextIds: [], + virtualClocks: [[0,0], [0,0]], //initialized with true game.clocks vr: null, //"variant rules" object initialized from FEN drawOffer: "", people: {}, //players + observers @@ -111,7 +142,8 @@ export default { conn: null, roomInitialized: false, // If newmove has wrong index: ask fullgame again: - fullGamerequested: false, + askGameTime: 0, + gameIsLoading: false, // If asklastate got no reply, ask again: gotLastate: false, gotMoveIdx: -1, //last move index received @@ -136,7 +168,11 @@ export default { const my = this.st.user; this.$set(this.people, my.sid, { id: my.id, name: my.name }); this.gameRef.id = this.$route.params["id"]; - this.gameRef.rid = this.$route.query["rid"]; //may be undefined + // rid = remote ID to find an observed live game, + // next = next corr games IDs to navigate faster + // (Both might be undefined) + this.gameRef.rid = this.$route.query["rid"]; + this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); // Initialize connection this.connexionString = params.socketUrl + @@ -226,45 +262,64 @@ export default { if (this.game.type == "corr") { 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) notifyTurn: function(sid) { const player = this.people[sid]; const colorIdx = this.game.players.findIndex( - p => p.sid == sid || p.id == player.id); + p => p.sid == sid || p.uid == 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 }); }, + showNextGame: function() { + // Did I play in current game? If not, add it to nextIds list + if (this.game.score == "*" && this.vr.turn == this.game.mycolor) + this.nextIds.unshift(this.game.id); + const nextGid = this.nextIds.pop(); + this.$router.push( + "/game/" + nextGid + "/?next=" + JSON.stringify(this.nextIds)); + }, + rematch: function() { + alert("Unimplemented yet (soon :) )"); + // TODO: same logic as for draw, but re-click remove rematch offer (toggle) + }, + askGameAgain: function() { + this.gameIsLoading = true; + const doAskGame = () => { + 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); + })(); + } + }; + // Delay of at least 2s between two game requests + const now = Date.now(); + const delay = Math.max(2000 - (now - this.askGameTime), 0); + this.askGameTime = now; + setTimeout(doAskGame, delay); + }, socketMessageListener: function(msg) { if (!this.conn) return; const data = JSON.parse(msg.data); switch (data.code) { case "pollclients": data.sockIds.forEach(sid => { - 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": @@ -324,6 +379,28 @@ export default { } 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(); + }, + 1000 + ); + })(); + } + } break; } case "askgame": @@ -345,18 +422,27 @@ export default { } break; case "askfullgame": - this.send("fullgame", { data: this.game, target: data.from }); + const gameToSend = Object.keys(this.game) + .filter(k => + [ + "id","fen","players","vid","cadence","fenStart","vname", + "moves","clocks","initime","score","drawOffer" + ].includes(k)) + .reduce( + (obj, k) => { + obj[k] = this.game[k]; + return obj; + }, + {} + ); + this.send("fullgame", { data: gameToSend, target: data.from }); 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 != "*" || @@ -366,8 +452,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", @@ -376,47 +462,46 @@ 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": { - -console.log(data.data); - - const move = data.data; - if (move.index > this.game.movesCount && !this.fullGameRequested) { - // This can only happen if I'm an observer and missed a move: - // just ask fullgame again, this is much simpler. - (function askIfPeerConnected() { - if (!!this.people[this.gameRef.rid]) - this.send("askfullgame", { target: this.gameRef.rid }); - else setTimeout(askIfPeerConnected, 1000); - })(); - this.fullGameRequested = true; - } else { + 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. + if (this.gotMoveIdx < movePlus.index) + this.gotMoveIdx = movePlus.index; + if (!this.gameIsLoading) this.askGameAgain(); + } + else { if ( - move.index < this.game.movesCount || - this.gotMoveIdx >= move.index + 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: move.index, target: data.from}); + this.send("gotmove", {data: movePlus.index, target: data.from}); } else { - this.gotMoveIdx = move.index; - const receiveMyMove = ( - !!this.game.mycolor && - move.index == this.game.movesCount - ); + 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: move.index, target: data.from}); - if (move.cancelDrawOffer) { + 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 @@ -428,12 +513,11 @@ console.log(data.data); GameStorage.update(this.gameRef.id, { drawOffer: "" }); } } - this.$refs["basegame"].play( - move.move, - "received", - null, + this.$refs["basegame"].play(movePlus.move, "received", null, true); + this.processMove( + movePlus.move, { - addTime: move.addTime, + addTime: movePlus.addTime, receiveMyMove: receiveMyMove } ); @@ -445,11 +529,6 @@ console.log(data.data); this.opponentGotMove = true; break; } -/// TODO: same strategy for askLastate -// --> the message could not have been received, -// or maybe we ddn't receive it back. - - case "resign": const score = data.side == "b" ? "1-0" : "0-1"; const side = data.side == "w" ? "White" : "Black"; @@ -485,10 +564,11 @@ console.log(data.data); 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 != "*") { @@ -508,7 +588,7 @@ console.log(data.data); 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; } @@ -557,10 +637,10 @@ console.log(data.data); } // 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.clocks = [tc.mainTime, tc.mainTime]; game.initime = [0, 0]; if (L >= 1) { const gameLastupdate = game.moves[L-1].played; @@ -609,7 +689,7 @@ console.log(data.data); } } } - if (game.drawOffer) { + if (!!game.drawOffer) { if (game.drawOffer == "t") // Three repetitions this.drawOffer = "threerep"; @@ -649,20 +729,27 @@ console.log(data.data); // 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.fullGameRequested) - // Second (or more) time the full game is asked: - this.fullGameRequested = false; + 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(); 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) { afterRetrieval(game); @@ -678,9 +765,9 @@ console.log(data.data); } }, 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)); + this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':')); return; } const currentTurn = this.vr.turn; @@ -692,7 +779,7 @@ console.log(data.data); 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); + return ppt(this.game.clocks[i] - removeTime).split(':'); }); let clockUpdate = setInterval(() => { if ( @@ -710,67 +797,39 @@ console.log(data.data); this.$set( this.virtualClocks, colorIdx, - ppt(Math.max(0, --countdown)) + ppt(Math.max(0, --countdown)).split(':') ); }, 1000); }, - // Post-process a (potentially partial) move (which was just played in BaseGame) + // Update variables and storage after a move: 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 && !data.receiveMyMove) { - // 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 = (this.game.type == "live") ? data.addTime : 0; + 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, - index: this.game.movesCount, - 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); } - // Update current game object (no need for moves stack): + // Update current game object: playMove(move, this.vr); - this.game.movesCount++; // 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: @@ -785,6 +844,10 @@ console.log(data.data); 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 && !data.receiveMyMove && @@ -808,23 +871,62 @@ console.log(data.data); 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: - GameStorage.update(this.gameRef.id, { - fen: this.game.fen, - move: filtered_move, - clocks: this.game.clocks, - initime: this.game.initime, - drawOffer: drawCode - }); + const updateStorage = () => { + GameStorage.update(this.gameRef.id, { + 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(); + // Small random delay otherwise + else setTimeout(updateStorage, 500 + 1000 * Math.random()); } } + // 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}); + }, + 1000 + ); + } }; if ( this.game.type == "corr" && @@ -896,9 +998,16 @@ console.log(data.data); #actions display: inline-block margin: 0 - button - display: inline-block - margin: 0 + +button + display: inline-block + margin: 0 + display: inline-flex + img + height: 24px + display: flex + @media screen and (max-width: 767px) + height: 18px @media screen and (max-width: 767px) #aboveBoard @@ -914,14 +1023,35 @@ console.log(data.data); font-weight: bold padding-right: 10px -.name +span#nextGame + background-color: #edda99 + cursor: pointer + display: inline-block + margin-right: 10px + +span.name font-size: 1.5rem - padding: 1px + padding: 0 3px -.time +span.time font-size: 2rem display: inline-block - margin-left: 10px + .time-left + margin-left: 10px + .time-right + margin-left: 5px + .time-separator + margin-left: 5px + position: relative + top: -1px + +span.yourturn + color: #831B1B + .time-separator + animation: blink-animation 2s steps(3, start) infinite +@keyframes blink-animation + to + visibility: hidden .split-names display: inline-block @@ -932,9 +1062,6 @@ console.log(data.data); max-width: 767px border: none; -#chatBtn - margin: 0 10px 0 0 - .draw-sent, .draw-sent:hover background-color: lightyellow