X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FGame.vue;h=55b36887e04a3ea0d6bb01254b42768c30800275;hb=5aa14a21484cf36838b5541afe2ee76b6d5c274b;hp=bbbb6760ef5b40de6a27a64ce70673b0046ca396;hpb=dcff8e82dd8bbc285d9c2d5d5e3b361a9ecfa9ac;p=vchess.git diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index bbbb6760..55b36887 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -8,7 +8,7 @@ main role="dialog" data-checkbox="modalChat" ) - #chat.card + .card label.modal-close(for="modalChat") #participants span {{ Object.keys(people).length + " " + st.tr["participant(s):"] }} @@ -20,47 +20,89 @@ main span.anonymous(v-if="Object.values(people).some(p => !p.name && p.id === 0)") | + @nonymous Chat( + ref="chatcomp" :players="game.players" :pastChats="game.chats" :newChat="newChat" @mychat="processChat" @chatcleared="clearChat" ) + input#modalConfirm.modal(type="checkbox") + div#confirmDiv(role="dialog") + .card + .diagram(v-html="curDiag") + .button-group#buttonsConfirm + // onClick for acceptBtn: set dynamically + button.acceptBtn + span {{ st.tr["Validate"] }} + button.refuseBtn(@click="cancelMove()") + 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-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" @newmove="processMove" - @gameover="gameOver" ) @@ -73,10 +115,11 @@ import { ppt } from "@/utils/datetime"; import { ajax } from "@/utils/ajax"; import { extractTime } from "@/utils/timeControl"; import { getRandString } from "@/utils/alea"; -import { processModalClick } from "@/utils/modalClick"; +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 { @@ -85,7 +128,6 @@ export default { BaseGame, Chat }, - // gameRef: to find the game in (potentially remote) storage data: function() { return { st: store.state, @@ -94,23 +136,22 @@ export default { id: "", rid: "" }, - game: { - // Passed to BaseGame - players: [{ name: "" }, { name: "" }], - chats: [], - rendered: false - }, - virtualClocks: [0, 0], //initialized with true game.clocks + nextIds: [], + game: {}, //passed to BaseGame + // virtualClocks will be initialized from true game.clocks + virtualClocks: [], vr: null, //"variant rules" object initialized from FEN drawOffer: "", 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: + askGameTime: 0, gameIsLoading: false, // If asklastate got no reply, ask again: gotLastate: false, @@ -118,66 +159,135 @@ export default { // If newmove got no pingback, send again: opponentGotMove: false, connexionString: "", + // Intervals from setInterval(): + // TODO: limit them to e.g. 3 retries ?! + askIfPeerConnected: null, + askLastate: null, + retrySendmove: null, + clockUpdate: null, // Related to (killing of) self multi-connects: newConnect: {}, killed: {} }; }, watch: { - $route: function(to) { - this.gameRef.id = to.params["id"]; - this.gameRef.rid = to.query["rid"]; - this.loadGame(); + $route: function(to, from) { + 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 + 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) created: function() { - // Always add myself to players' list - 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 - // Initialize connection - this.connexionString = - params.socketUrl + - "/?sid=" + - this.st.user.sid + - "&tmpId=" + - getRandString() + - "&page=" + - encodeURIComponent(this.$route.path); - this.conn = new WebSocket(this.connexionString); - this.conn.onmessage = this.socketMessageListener; - this.conn.onclose = this.socketCloseListener; - // Socket init required before loading remote game: - const socketInit = callback => { - if (!!this.conn && 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. - 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); + this.atCreation(); }, mounted: function() { document .getElementById("chatWrap") .addEventListener("click", processModalClick); + if ("ontouchstart" in window) { + // Disable tooltips on smartphones: + document.getElementsByClassName("tooltip").forEach(elt => { + elt.classList.remove("tooltip"); + }); + } }, beforeDestroy: function() { - this.send("disconnect"); + this.cleanBeforeDestroy(); }, methods: { + atCreation: function() { + // 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.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.game = { + players: [{ name: "" }, { name: "" }], + chats: [], + rendered: false + }; + let chatComp = this.$refs["chatcomp"]; + if (!!chatComp) chatComp.chats = []; + this.virtualClocks = [[0,0], [0,0]]; + this.vr = null; + this.drawOffer = ""; + this.onMygames = []; + this.lastate = undefined; + this.newChat = ""; + this.roomInitialized = false; + this.askGameTime = 0; + this.gameIsLoading = false; + this.gotLastate = false; + this.gotMoveIdx = -1; + this.opponentGotMove = false; + this.askIfPeerConnected = null; + this.askLastate = null; + this.retrySendmove = null; + this.clockUpdate = null; + this.newConnect = {}; + this.killed = {}; + // 1] Initialize connection + this.connexionString = + params.socketUrl + + "/?sid=" + + this.st.user.sid + + "&tmpId=" + + getRandString() + + "&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; + // Socket init required before loading remote game: + const socketInit = callback => { + if (!!this.conn && 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. + 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.askIfPeerConnected) + clearInterval(this.askIfPeerConnected); + if (!!this.askLastate) + clearInterval(this.askLastate); + if (!!this.retrySendmove) + clearInterval(this.retrySendmove); + if (!!this.clockUpdate) + clearInterval(this.clockUpdate); + this.send("disconnect"); + }, roomInit: function() { if (!this.roomInitialized) { // Notify the room only now that I connected, because @@ -197,7 +307,9 @@ export default { const player = this.game.players[index]; // Is it me ? if (this.st.user.sid == player.sid || this.st.user.id == player.uid) - return true; + // 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); // Try to find a match in people: return ( ( @@ -219,7 +331,7 @@ export default { 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) - GameStorage.update(this.gameRef.id, { chat: chat }); + this.updateCorrGame({ chat: chat }); }, clearChat: function() { // Nothing more to do if game is live (chats not recorded) @@ -233,7 +345,7 @@ export default { 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 = @@ -241,6 +353,50 @@ export default { (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 currentUrl = document.location.href; + const doAskGame = () => { + if (currentUrl != document.location.href) 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.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(); + 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); @@ -316,18 +472,17 @@ export default { 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 - ); - })(); + this.send("asklastate", { target: user.sid }); + this.askLastate = setInterval( + () => { + // Ask until we got a reply (or opponent disconnect): + if (!this.gotLastate && !!this.people[user.sid]) + this.send("asklastate", { target: user.sid }); + else + clearInterval(this.askLastate); + }, + 1000 + ); } } break; @@ -351,7 +506,20 @@ 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 @@ -399,23 +567,11 @@ export default { 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.gameIsLoading) { - 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); - })(); - } - } - } else { + if (this.gotMoveIdx < movePlus.index) + this.gotMoveIdx = movePlus.index; + if (!this.gameIsLoading) this.askGameAgain(); + } + else { if ( movePlus.index < movesCount || this.gotMoveIdx >= movePlus.index @@ -441,10 +597,9 @@ export default { GameStorage.update(this.gameRef.id, { drawOffer: "" }); } } - this.$refs["basegame"].play( + this.$refs["basegame"].play(movePlus.move, "received", null, true); + this.processMove( movePlus.move, - "received", - null, { addTime: movePlus.addTime, receiveMyMove: receiveMyMove @@ -485,6 +640,19 @@ export default { this.conn.addEventListener("message", this.socketMessageListener); this.conn.addEventListener("close", this.socketCloseListener); }, + updateCorrGame: function(obj, callback) { + ajax( + "/games", + "PUT", + { + gid: this.gameRef.id, + newObj: obj + }, + () => { + if (!!callback) callback(); + } + ); + }, // lastate was received, but maybe game wasn't ready yet: processLastate: function() { const data = this.lastate; @@ -502,7 +670,9 @@ export default { if (data.drawSent) this.drawOffer = "received"; if (data.score != "*") { this.drawOffer = ""; - if (this.game.score == "*") this.gameOver(data.score); + if (this.game.score == "*") + // TODO: also pass scoreMsg in lastate + this.gameOver(data.score); } }, clickDraw: function() { @@ -517,14 +687,19 @@ 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; } if (!confirm(this.st.tr["Offer draw?"])) return; this.drawOffer = "sent"; this.send("drawoffer"); - GameStorage.update(this.gameRef.id, { drawOffer: this.game.mycolor }); + if (this.game.type == "live") { + GameStorage.update( + this.gameRef.id, + { drawOffer: this.game.mycolor } + ); + } else this.updateCorrGame({ drawOffer: this.game.mycolor }); } }, abortGame: function() { @@ -566,10 +741,10 @@ export default { } // 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; @@ -618,7 +793,7 @@ export default { } } } - if (game.drawOffer) { + if (!!game.drawOffer) { if (game.drawOffer == "t") // Three repetitions this.drawOffer = "threerep"; @@ -664,15 +839,28 @@ export default { ); if (this.gameIsLoading) // Re-load game because we missed some moves: - // artificially reset BaseGame (required if moves arrive too quickly) + // artificially reset BaseGame (required if moves arrived in wrong order) this.$refs["basegame"].re_setVariables(); + 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(); }); - this.gameIsLoading = false; + if (this.gameIsLoading) { + this.gameIsLoading = false; + if (this.gotMoveIdx >= game.moves.length) + // Some moves arrived meanwhile... + this.askGameAgain(); + } if (!!callback) callback(); }; if (!!game) { @@ -683,15 +871,28 @@ export default { // Remote live game: forgetting about callback func... (TODO: design) this.send("askfullgame", { target: this.gameRef.rid }); } else { - // Local or corr game + // Local or corr game on server. // NOTE: afterRetrieval() is never called if game not found - GameStorage.get(this.gameRef.id, afterRetrieval); + 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); + }); + } + else + // Local game + GameStorage.get(this.gameRef.id, afterRetrieval); } }, re_setClocks: function() { 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; @@ -703,15 +904,15 @@ export default { 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(() => { + this.clockUpdate = setInterval(() => { if ( countdown < 0 || this.game.moves.length > currentMovesCount || this.game.score != "*" ) { - clearInterval(clockUpdate); + clearInterval(this.clockUpdate); if (countdown < 0) this.gameOver( currentTurn == "w" ? "0-1" : "1-0", @@ -721,11 +922,11 @@ export default { 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; @@ -749,6 +950,11 @@ export default { } // Update current game object: playMove(move, this.vr); + 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 @@ -760,18 +966,17 @@ export default { 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(); - this.re_setClocks(); // 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; 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); } + // 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 && @@ -790,7 +995,8 @@ export default { break; } if (this.game.type == "corr") { - GameStorage.update(this.gameRef.id, { + // corr: only move, fen and score + this.updateCorrGame({ fen: this.game.fen, move: { squares: filtered_move, @@ -801,15 +1007,21 @@ export default { drawOffer: drawCode || "n" }); } - else if (!document.hidden) { - // Live game: consider only the active tab - GameStorage.update(this.gameRef.id, { - fen: this.game.fen, - move: filtered_move, - clocks: this.game.clocks, - initime: this.game.initime, - drawOffer: drawCode - }); + else { + 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) @@ -825,22 +1037,25 @@ export default { 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); + this.retrySendmove = setInterval( + () => { + if (this.opponentGotMove) { + clearInterval(this.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(this.retrySendmove); + else this.send("newmove", {data: sendMove, target: oppsid}); + }, + 1000 + ); } }; if ( @@ -848,42 +1063,101 @@ export default { 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é) + const afterSetScore = () => { + doProcessMove(); + if (this.st.settings.gotonext && this.nextIds.length > 0) + this.showNextGame(); + else { + this.re_setClocks(); + // The board might have been hidden: + let boardDiv = document.querySelector(".game"); + if (boardDiv.style.visibility == "hidden") + boardDiv.style.visibility = "visible"; + } + }; + if (["all","byrow"].includes(V.ShowMoves)) { + let el = document.querySelector("#buttonsConfirm > .acceptBtn"); + // We may play several moves in a row: in case of, remove listener: + let elClone = el.cloneNode(true); + el.parentNode.replaceChild(elClone, el); + elClone.addEventListener( + "click", + () => { + document.getElementById("modalConfirm").checked = false; + 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" + }); + document.getElementById("modalConfirm").checked = true; + } else { + // Incomplete information: just ask confirmation + // Hide the board, because otherwise it could be revealed (TODO?) + let boardDiv = document.querySelector(".game"); + boardDiv.style.visibility = "hidden"; if ( !confirm( - this.st.tr["Move played:"] + - " " + - getFullNotation(move) + - "\n" + - this.st.tr["Are you sure?"] + this.st.tr["Move played:"] + " " + + getFullNotation(move) + "\n" + + this.st.tr["Are you sure?"] ) ) { this.$refs["basegame"].cancelLastMove(); + boardDiv.style.visibility = "visible"; return; } + if (!!data.score && data.score != "*") + this.gameOver(data.score, null, afterSetScore); + else afterSetScore(); + } + } + else { + // Normal situation + const afterSetScore = () => { doProcessMove(); - // Let small time to finish drawing current move attempt: - }, 500); + this.re_setClocks(); + }; + if (!!data.score && data.score != "*") + this.gameOver(data.score, null, afterSetScore); + else afterSetScore(); } - else doProcessMove(); }, - gameOver: function(score, scoreMsg) { + cancelMove: function() { + document.getElementById("modalConfirm").checked = false; + this.$refs["basegame"].cancelLastMove(); + }, + // 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.$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; }); if (myIdx >= 0) { // OK, I play in this game - GameStorage.update(this.gameRef.id, { + const scoreObj = { score: score, scoreMsg: scoreMsg - }); + }; + if (this.game.type == "live") { + GameStorage.update(this.gameRef.id, 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(); } } }; @@ -913,9 +1187,16 @@ export default { #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 @@ -931,26 +1212,48 @@ export default { 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 margin: 0 15px -#chat +#chatWrap > .card padding-top: 20px max-width: 767px - border: none; + border: none -#chatBtn - margin: 0 10px 0 0 +#confirmDiv > .card + max-width: 767px + max-height: 100% .draw-sent, .draw-sent:hover background-color: lightyellow @@ -963,4 +1266,21 @@ export default { .somethingnew background-color: #c5fefe + +.diagram + margin: 0 auto + max-width: 400px + // width: 100% required for Firefox + width: 100% + +#buttonsConfirm + margin: 0 + & > button > span + width: 100% + text-align: center + +button.acceptBtn + background-color: lightgreen +button.refuseBtn + background-color: red