X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FGame.vue;h=0741d31753f8df5c66cd067b91c5ba09516413c1;hb=e5c1d0fb0ed18632e9172c932d6e5c2c8c5742b8;hp=758759dc8b891be35246a1cadbae821ac63dc553;hpb=92240cf0fbf76ddf8a030ba1f846d6c62b1e9979;p=vchess.git diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 758759dc..0741d317 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" @@ -77,6 +77,7 @@ import { processModalClick } from "@/utils/modalClick"; import { getFullNotation } from "@/utils/notation"; import { playMove, getFilteredMove } from "@/utils/playUndo"; import { getScoreMessage } from "@/utils/scoring"; +import { ArrayFun } from "@/utils/array"; import params from "@/parameters"; export default { name: "my-game", @@ -89,12 +90,12 @@ export default { return { st: store.state, gameRef: { - //given in URL (rid = remote ID) + // rid = remote (socket) ID id: "", rid: "" }, game: { - //passed to BaseGame + // Passed to BaseGame players: [{ name: "" }, { name: "" }], chats: [], rendered: false @@ -103,10 +104,19 @@ export default { 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 newChat: "", conn: null, + roomInitialized: false, + // If newmove has wrong index: ask fullgame again: + fullGamerequested: 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: {}, @@ -144,25 +154,20 @@ export default { if (!!this.conn && this.conn.readyState == 1) // 1 == OPEN state callback(); - else { + 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 = () => { - return callback(); - }; - } + this.conn.onopen = () => callback(); }; if (!this.gameRef.rid) // Game stored locally or on server this.loadGame(null, () => socketInit(this.roomInit)); - else { + 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. - // A more general approach would be to store it somewhere. socketInit(this.loadGame); - } }, mounted: function() { document @@ -174,15 +179,19 @@ 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) { const player = this.game.players[index]; @@ -191,8 +200,15 @@ export default { return true; // Try to find a match in people: return ( - Object.keys(this.people).some(sid => sid == player.sid) || - Object.values(this.people).some(p => p.id == player.uid) + ( + player.sid && + Object.keys(this.people).some(sid => sid == player.sid) + ) + || + ( + player.uid && + Object.values(this.people).some(p => p.id == player.uid) + ) ); }, resetChatColor: function() { @@ -208,19 +224,35 @@ 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}); - // TODO: this.game.chats = [] could be enough here? - this.$set(this.game, "chats", []); + 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); + const color = ["w","b"][colorIdx]; + const yourTurn = + ( + color == "w" && + this.game.movesCount % 2 == 0 + ) + || + ( + color == "b" && + this.game.movesCount % 2 == 1 + ); + this.send("turnchange", { target: sid, yourTurn: yourTurn }); + }, 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) { this.send("askidentity", { target: sid }); // Ask potentially missed last state, if opponent and I play @@ -236,9 +268,7 @@ export default { }); break; case "connect": - if (!this.people[data.from]) - 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 }); } @@ -246,17 +276,29 @@ export default { 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); + break; case "killed": // I logged in elsewhere: - // TODO: this fails. See https://github.com/websockets/ws/issues/489 - //this.conn.removeEventListener("message", this.socketMessageListener); - //this.conn.removeEventListener("close", this.socketCloseListener); - //this.conn.close(); this.conn = null; 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, @@ -268,28 +310,20 @@ export default { } case "identity": { 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]; } - delete this.newConnect[user.sid]; break; } case "askgame": @@ -352,21 +386,70 @@ export default { // Else: will be processed when game is ready break; case "newmove": { + +console.log(data.data); + 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: "" }); + 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 { + if ( + move.index < this.game.movesCount || + this.gotMoveIdx >= move.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}); + } else { + this.gotMoveIdx = move.index; + const receiveMyMove = ( + !!this.game.mycolor && + move.index == this.game.movesCount + ); + if (!receiveMyMove && !!this.game.mycolor) + // Notify opponent that I got the move: + this.send("gotmove", {data: move.index, target: data.from}); + 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 && + !receiveMyMove + ) { + GameStorage.update(this.gameRef.id, { drawOffer: "" }); + } + } + this.$refs["basegame"].play( + move.move, + "received", + null, + { + addTime: move.addTime, + receiveMyMove: receiveMyMove + } + ); + } } - this.$refs["basegame"].play( - move.move, - "received", - null, - {addTime: move.addTime}); break; } + case "gotmove": { + 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"; @@ -425,7 +508,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; } @@ -482,8 +565,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 @@ -568,6 +653,9 @@ export default { }, game, ); + if (this.fullGameRequested) + // Second (or more) time the full game is asked: + this.fullGameRequested = false; this.re_setClocks(); this.$nextTick(() => { this.game.rendered = true; @@ -576,7 +664,7 @@ export default { }); if (callback) callback(); }; - if (game) { + if (!!game) { afterRetrieval(game); return; } @@ -628,17 +716,18 @@ export default { }, // Post-process a (potentially partial) move (which was just played in BaseGame) 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) { + 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 = (data && this.game.type == "live") ? data.addTime : 0; - if (moveCol == this.game.mycolor) { + let addTime = (this.game.type == "live") ? data.addTime : 0; + if (moveCol == this.game.mycolor && !data.receiveMyMove) { if (this.drawOffer == "received") // I refuse draw this.drawOffer = ""; @@ -650,17 +739,34 @@ export default { } const sendMove = { move: filtered_move, + index: this.game.movesCount, 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) + cancelDrawOffer: this.drawOffer == "" }; - this.send("newmove", { data: sendMove }); + 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): playMove(move, this.vr); this.game.movesCount++; +// TODO: notifyTurn: "changeturn" message // (add)Time indication: useful in case of lastate infos requested this.game.moves.push(this.game.type == "live" ? {move:move, addTime:addTime} @@ -670,19 +776,18 @@ export default { // 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 && + !!this.game.mycolor && + !data.receiveMyMove && (this.game.type == "live" || moveCol == this.game.mycolor) ) { let drawCode = ""; @@ -721,8 +826,14 @@ export default { } } }; - 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:"] +