X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FGame.vue;h=d62e2ca77677a8b7ae4d33dd3552c5d8874e771a;hb=3ecfb65d520b9d64a59e46a3a2c286b031ee1c26;hp=e0ae7e91cac2e6886e25a249a6c863dc6ad75a79;hpb=aae89b49a846b2c101d74db7dff9151392d6db34;p=vchess.git diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index e0ae7e91..d62e2ca7 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -14,10 +14,12 @@ 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.focus && !!p.name" ) | {{ p.name }} - span.anonymous(v-if="Object.values(people).some(p => !p.name && p.id === 0)") + span.anonymous( + v-if="Object.values(people).some(p => p.focus && !p.name)" + ) | + @nonymous Chat( ref="chatcomp" @@ -30,7 +32,15 @@ main input#modalConfirm.modal(type="checkbox") div#confirmDiv(role="dialog") .card - .diagram(v-html="curDiag") + .diagram( + v-if="!!vr && ['all','byrow'].includes(vr.showMoves)" + v-html="curDiag" + ) + p.text-center(v-else) + span {{ st.tr["Move played:"] + " " }} + span.bold {{ moveNotation }} + br + span {{ st.tr["Are you sure?"] }} .button-group#buttonsConfirm // onClick for acceptBtn: set dynamically button.acceptBtn @@ -103,7 +113,6 @@ main ref="basegame" :game="game" @newmove="processMove" - @gameover="gameOver" ) @@ -116,10 +125,11 @@ import { ppt } from "@/utils/datetime"; import { ajax } from "@/utils/ajax"; import { extractTime } from "@/utils/timeControl"; import { getRandString } from "@/utils/alea"; +import { getScoreMessage } from "@/utils/scoring"; +import { getFullNotation } from "@/utils/notation"; import { getDiagram } from "@/utils/printDiagram"; import { processModalClick } from "@/utils/modalClick"; import { playMove, getFilteredMove } from "@/utils/playUndo"; -import { getScoreMessage } from "@/utils/scoring"; import { ArrayFun } from "@/utils/array"; import params from "@/parameters"; export default { @@ -159,9 +169,9 @@ export default { // If newmove got no pingback, send again: opponentGotMove: false, connexionString: "", + // Incomplete info games: show move played + moveNotation: "", // Intervals from setInterval(): - // TODO: limit them to e.g. 3 retries ?! - askIfPeerConnected: null, askLastate: null, retrySendmove: null, clockUpdate: null, @@ -175,6 +185,10 @@ export default { if (from.params["id"] != to.params["id"]) { // Change everything: this.cleanBeforeDestroy(); + let boardDiv = document.querySelector(".game"); + if (!!boardDiv) + // In case of incomplete information variant: + boardDiv.style.visibility = "hidden"; this.atCreation(); } else { // Same game ID @@ -190,6 +204,7 @@ export default { this.atCreation(); }, mounted: function() { + document.addEventListener('visibilitychange', this.visibilityChange); document .getElementById("chatWrap") .addEventListener("click", processModalClick); @@ -201,9 +216,18 @@ export default { } }, beforeDestroy: function() { + document.removeEventListener('visibilitychange', this.visibilityChange); this.cleanBeforeDestroy(); }, methods: { + visibilityChange: function() { + // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27 + this.send( + document.visibilityState == "visible" + ? "getfocus" + : "losefocus" + ); + }, atCreation: function() { // 0] (Re)Set variables this.gameRef.id = this.$route.params["id"]; @@ -214,7 +238,15 @@ export default { this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); // Always add myself to players' list const my = this.st.user; - this.$set(this.people, my.sid, { id: my.id, name: my.name }); + this.$set( + this.people, + my.sid, + { + id: my.id, + name: my.name, + focus: true + } + ); this.game = { players: [{ name: "" }, { name: "" }], chats: [], @@ -234,7 +266,6 @@ export default { this.gotLastate = false; this.gotMoveIdx = -1; this.opponentGotMove = false; - this.askIfPeerConnected = null; this.askLastate = null; this.retrySendmove = null; this.clockUpdate = null; @@ -274,8 +305,6 @@ export default { socketInit(this.loadGame); }, cleanBeforeDestroy: function() { - if (!!this.askIfPeerConnected) - clearInterval(this.askIfPeerConnected); if (!!this.askLastate) clearInterval(this.askLastate); if (!!this.retrySendmove) @@ -301,7 +330,7 @@ export default { }, isConnected: function(index) { const player = this.game.players[index]; - // Is it me ? + // 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) // Still have to check for name (because of potential multi-accounts // on same browser, although this should be rare...) @@ -309,13 +338,15 @@ export default { // Try to find a match in people: return ( ( - player.sid && - Object.keys(this.people).some(sid => sid == player.sid) + !!player.sid && + Object.keys(this.people).some(sid => + sid == player.sid && this.people[sid].focus) ) || ( player.uid && - Object.values(this.people).some(p => p.id == player.uid) + Object.values(this.people).some(p => + p.id == player.uid && p.focus) ) ); }, @@ -332,8 +363,13 @@ export default { clearChat: function() { // Nothing more to do if game is live (chats not recorded) if (this.game.type == "corr") { - if (!!this.game.mycolor) - ajax("/chats", "DELETE", {gid: this.game.id}); + if (!!this.game.mycolor) { + ajax( + "/chats", + "DELETE", + { data: { gid: this.game.id } } + ); + } this.$set(this.game, "chats", []); } }, @@ -350,7 +386,6 @@ export default { this.send("turnchange", { target: sid, yourTurn: yourTurn }); }, showNextGame: function() { - if (this.nextIds.length == 0) return; // 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); @@ -366,27 +401,14 @@ export default { this.gameIsLoading = true; const currentUrl = document.location.href; const doAskGame = () => { - if (currentUrl != document.location.href) return; //page change + if (document.location.href != currentUrl) return; //page change if (!this.gameRef.rid) // This is my game: just reload. this.loadGame(); - else { + 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.askIfPeerConnected = setInterval( - () => { - if ( - !!this.people[this.gameRef.rid] && - currentUrl != document.location.href - ) { - this.send("askfullgame", { target: this.gameRef.rid }); - clearInterval(this.askIfPeerConnected); - } - }, - 1000 - ); - } }; // Delay of at least 2s between two game requests const now = Date.now(); @@ -400,12 +422,15 @@ export default { switch (data.code) { case "pollclients": data.sockIds.forEach(sid => { - if (sid != this.st.user.sid) + if (sid != this.st.user.sid) { + this.people[sid] = { focus: true }; this.send("askidentity", { target: 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 }); } @@ -429,6 +454,22 @@ export default { case "mdisconnect": ArrayFun.remove(this.onMygames, sid => sid == data.from); break; + case "getfocus": { + let player = this.people[data.from]; + if (!!player) { + player.focus = true; + this.$forceUpdate(); //TODO: shouldn't be required + } + break; + } + case "losefocus": { + let player = this.people[data.from]; + if (!!player) { + player.focus = false; + this.$forceUpdate(); //TODO: shouldn't be required + } + break; + } case "killed": // I logged in elsewhere: this.conn = null; @@ -447,7 +488,11 @@ export default { } case "identity": { const user = data.data; - this.$set(this.people, user.sid, { name: user.name, id: user.id }); + let player = this.people[user.sid]; + // player.focus 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]) { if ( @@ -470,15 +515,23 @@ export default { this.game.players.some(p => p.sid == user.sid) ) { this.send("asklastate", { target: user.sid }); + let counter = 1; this.askLastate = setInterval( () => { - // Ask until we got a reply (or opponent disconnect): - if (!this.gotLastate && !!this.people[user.sid]) + // 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 }); - else + counter++; + } else { clearInterval(this.askLastate); + } }, - 1000 + 1500 ); } } @@ -534,11 +587,12 @@ export default { const myIdx = ["w", "b"].indexOf(this.game.mycolor); const myLastate = { lastMove: L > 0 ? this.game.moves[L - 1] : undefined, - addTime: L > 0 ? this.game.addTimes[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", score: this.game.score, + score: this.game.scoreMsg, movesCount: L, initime: this.game.initime[1 - myIdx] //relevant only if I played }; @@ -598,7 +652,7 @@ export default { this.processMove( movePlus.move, { - addTime: movePlus.addTime, + clock: movePlus.clock, receiveMyMove: receiveMyMove } ); @@ -608,6 +662,10 @@ export default { } case "gotmove": { this.opponentGotMove = true; + // Now his clock starts running: + const oppIdx = ['w','b'].indexOf(this.vr.turn); + this.game.initime[oppIdx] = Date.now(); + this.re_setClocks(); break; } case "resign": @@ -637,13 +695,18 @@ export default { this.conn.addEventListener("message", this.socketMessageListener); this.conn.addEventListener("close", this.socketCloseListener); }, - updateCorrGame: function(obj) { + updateCorrGame: function(obj, callback) { ajax( "/games", "PUT", { - gid: this.gameRef.id, - newObj: obj + data: { + gid: this.gameRef.id, + newObj: obj + }, + success: () => { + if (!!callback) callback(); + } } ); }, @@ -654,17 +717,14 @@ export default { const L = this.game.moves.length; if (data.movesCount > L) { // Just got last move from him - this.$refs["basegame"].play( - data.lastMove, - "received", - null, - {addTime: data.addTime, initime: data.initime} - ); + this.$refs["basegame"].play(data.lastMove, "received", null, true); + this.processMove(data.lastMove, { clock: data.clock }); } if (data.drawSent) this.drawOffer = "received"; if (data.score != "*") { this.drawOffer = ""; - if (this.game.score == "*") this.gameOver(data.score); + if (this.game.score == "*") + this.gameOver(data.score, data.scoreMsg); } }, clickDraw: function() { @@ -824,8 +884,7 @@ export default { // 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, - addTimes: [], //used for live games + oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].uid }, game, ); @@ -833,9 +892,14 @@ export default { // Re-load game because we missed some moves: // artificially reset BaseGame (required if moves arrived in wrong order) this.$refs["basegame"].re_setVariables(); - else + 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; @@ -863,13 +927,20 @@ export default { const gid = this.gameRef.id; if (Number.isInteger(gid) || !isNaN(parseInt(gid))) { // corr games identifiers are integers - ajax("/games", "GET", { gid: gid }, res => { - let g = res.game; - g.moves.forEach(m => { - m.squares = JSON.parse(m.squares); - }); - afterRetrieval(g); - }); + ajax( + "/games", + "GET", + { + data: { gid: gid }, + success: (res) => { + let g = res.game; + g.moves.forEach(m => { + m.squares = JSON.parse(m.squares); + }); + afterRetrieval(g); + } + } + ); } else // Local game @@ -893,25 +964,28 @@ export default { 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.moves.length > currentMovesCount || - this.game.score != "*" - ) { - clearInterval(this.clockUpdate); - if (countdown < 0) - this.gameOver( - currentTurn == "w" ? "0-1" : "1-0", - "Time" + this.clockUpdate = setInterval( + () => { + if ( + countdown < 0 || + this.game.moves.length > currentMovesCount || + this.game.score != "*" + ) { + clearInterval(this.clockUpdate); + if (countdown < 0) + this.gameOver( + currentTurn == "w" ? "0-1" : "1-0", + "Time" + ); + } else + this.$set( + this.virtualClocks, + colorIdx, + ppt(Math.max(0, --countdown)).split(':') ); - } else - this.$set( - this.virtualClocks, - colorIdx, - ppt(Math.max(0, --countdown)).split(':') - ); - }, 1000); + }, + 1000 + ); }, // Update variables and storage after a move: processMove: function(move, data) { @@ -921,10 +995,7 @@ export default { const colorIdx = ["w", "b"].indexOf(moveCol); const nextIdx = 1 - colorIdx; const origMovescount = this.game.moves.length; - let addTime = - this.game.type == "live" - ? (data.addTime || 0) - : undefined; + let addTime = 0; //for live games if (moveCol == this.game.mycolor && !data.receiveMyMove) { if (this.drawOffer == "received") // I refuse draw @@ -937,20 +1008,38 @@ export default { } // 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 this.game.moves.push(move); - // (add)Time indication: useful in case of lastate infos requested - 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; + 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 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.initime || Date.now(); + else { + 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] = this.repeat[fenObj] ? this.repeat[fenObj] + 1 : 1; + 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 = ""; if (!!this.game.mycolor && !data.receiveMyMove) { @@ -1008,20 +1097,29 @@ export default { } // Send move ("newmove" event) to people in the room (if our turn) if (moveCol == this.game.mycolor && !data.receiveMyMove) { - const sendMove = { + let 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 == "" }; + if (this.game.type == "live") + sendMove["clock"] = this.game.clocks[colorIdx]; this.opponentGotMove = false; this.send("newmove", {data: sendMove}); // If the opponent doesn't reply gotmove soon enough, re-send move: + // Do this at most 2 times, because mpore would mean network issues, + // opponent would then be expected to disconnect/reconnect. + let counter = 1; + const currentUrl = document.location.href; this.retrySendmove = setInterval( () => { - if (this.opponentGotMove) { + if ( + counter >= 3 || + this.opponentGotMove || + document.location.href != currentUrl //page change + ) { clearInterval(this.retrySendmove); return; } @@ -1034,17 +1132,34 @@ export default { if (!oppsid || !this.people[oppsid]) // Opponent is disconnected: he'll ask last state clearInterval(this.retrySendmove); - else this.send("newmove", {data: sendMove, target: oppsid}); + else { + this.send("newmove", { data: sendMove, target: oppsid }); + counter++; + } }, - 1000 + 1500 ); } + else + // Not my move or I'm an observer: just start other player's clock + this.re_setClocks(); }; if ( this.game.type == "corr" && moveCol == this.game.mycolor && !data.receiveMyMove ) { + let boardDiv = document.querySelector(".game"); + const afterSetScore = () => { + doProcessMove(); + if (this.st.settings.gotonext && this.nextIds.length > 0) + this.showNextGame(); + else { + // The board might have been hidden: + if (boardDiv.style.visibility == "hidden") + boardDiv.style.visibility = "visible"; + } + }; let el = document.querySelector("#buttonsConfirm > .acceptBtn"); // We may play several moves in a row: in case of, remove listener: let elClone = el.cloneNode(true); @@ -1053,33 +1168,49 @@ export default { "click", () => { document.getElementById("modalConfirm").checked = false; - doProcessMove(); - if (this.st.settings.gotonext) this.showNextGame(); - else this.re_setClocks(); + if (!!data.score && data.score != "*") + // Set score first + this.gameOver(data.score, null, afterSetScore); + else afterSetScore(); } ); // PlayOnBoard is enough, and more appropriate for Synchrone Chess V.PlayOnBoard(this.vr.board, move); const position = this.vr.getBaseFen(); V.UndoOnBoard(this.vr.board, move); - this.curDiag = getDiagram({ - position: position, - orientation: V.CanFlip ? this.game.mycolor : "w" - }); + if (["all","byrow"].includes(V.ShowMoves)) { + this.curDiag = getDiagram({ + position: position, + orientation: V.CanFlip ? this.game.mycolor : "w" + }); + } else { + // Incomplete information: just ask confirmation + // Hide the board, because otherwise it could reveal infos + boardDiv.style.visibility = "hidden"; + this.moveNotation = getFullNotation(move); + } document.getElementById("modalConfirm").checked = true; } else { - doProcessMove(); - this.re_setClocks(); + // Normal situation + if (!!data.score && data.score != "*") + this.gameOver(data.score, null, doProcessMove); + else doProcessMove(); } }, cancelMove: function() { + let boardDiv = document.querySelector(".game"); + if (boardDiv.style.visibility == "hidden") + boardDiv.style.visibility = "visible"; document.getElementById("modalConfirm").checked = false; this.$refs["basegame"].cancelLastMove(); }, - gameOver: function(score, scoreMsg) { + // In corr games, callback to change page only after score is set: + gameOver: function(score, scoreMsg, callback) { this.game.score = score; - this.$set(this.game, "scoreMsg", scoreMsg || getScoreMessage(score)); + if (!scoreMsg) scoreMsg = getScoreMessage(score); + 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; }); @@ -1089,12 +1220,15 @@ export default { score: score, scoreMsg: scoreMsg }; - if (this.Game.type == "live") + if (this.game.type == "live") { GameStorage.update(this.gameRef.id, scoreObj); - else this.updateCorrGame(scoreObj); + if (!!callback) callback(); + } + else this.updateCorrGame(scoreObj, callback); // Notify the score to main Hall. TODO: only one player (currently double send) this.send("result", { gid: this.game.id, score: score }); } + else if (!!callback) callback(); } } }; @@ -1206,8 +1340,6 @@ span.yourturn .diagram margin: 0 auto - max-width: 400px - // width: 100% required for Firefox width: 100% #buttonsConfirm