X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FHall.vue;h=9c06c4bd8883dfcedf52f5766fecd2f52aad19d3;hb=51d87b528ca16905a834cd5bcfad83ab07fbc99f;hp=39d7c167a5582d59602a5e15bb7aef48597a64c0;hpb=26f3a8879fb4b410ed8840c5a37397011c13bc1c;p=vchess.git diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 39d7c167..9c06c4bd 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -18,12 +18,12 @@ main :selected="newchallenge.vid==v.id") | {{ v.name }} fieldset - label(for="timeControl") {{ st.tr["Time control"] }} * - div#predefinedTimeControls + label(for="cadence") {{ st.tr["Cadence"] }} * + div#predefinedCadences button 3+2 button 5+3 button 15+5 - input#timeControl(type="text" v-model="newchallenge.timeControl" + input#cadence(type="text" v-model="newchallenge.cadence" placeholder="5+0, 1h+30s, 7d+1d ...") fieldset(v-if="st.user.id > 0") label(for="selectPlayers") {{ st.tr["Play with?"] }} @@ -52,11 +52,12 @@ main #players p(v-for="sid in Object.keys(people)" v-if="!!people[sid].name") span {{ people[sid].name }} + // Check: anonymous players cannot send individual challenges or be challenged individually button.player-action( - v-if="people[sid].name != st.user.name" - @click="challOrWatch(sid, $event)" + v-if="sid != st.user.sid && !!st.user.name && people[sid].id > 0" + @click="challOrWatch(sid)" ) - | {{ st.tr[!!people[sid].gamer ? 'Playing' : 'Available'] }} + | {{ getActionLabel(sid) }} p.anonymous @nonymous ({{ anonymousCount }}) #chat Chat(:newChat="newChat" @mychat="processChat") @@ -78,13 +79,13 @@ import { store } from "@/store"; import { checkChallenge } from "@/data/challengeCheck"; import { ArrayFun } from "@/utils/array"; import { ajax } from "@/utils/ajax"; +import params from "@/parameters"; import { getRandString, shuffle } from "@/utils/alea"; import Chat from "@/components/Chat.vue"; import GameList from "@/components/GameList.vue"; import ChallengeList from "@/components/ChallengeList.vue"; import { GameStorage } from "@/utils/gameStorage"; import { processModalClick } from "@/utils/modalClick"; - export default { name: "my-hall", components: { @@ -96,32 +97,32 @@ export default { return { st: store.state, cdisplay: "live", //or corr - pdisplay: "players", //or chat gdisplay: "live", games: [], challenges: [], - people: {}, //people in main hall + people: {}, infoMessage: "", newchallenge: { fen: "", vid: localStorage.getItem("vid") || "", to: "", //name of challenged player (if any) - timeControl: localStorage.getItem("timeControl") || "", + cadence: localStorage.getItem("cadence") || "", }, newChat: "", + conn: null, + connexionString: "", + // Related to (killing of) self multi-connects: + newConnect: {}, + killed: {}, }; }, watch: { // st.variants changes only once, at loading from [] to [...] "st.variants": function(variantArray) { // Set potential challenges and games variant names: - this.challenges.forEach(c => { - if (c.vname == "") - c.vname = this.getVname(c.vid); - }); - this.games.forEach(g => { - if (g.vname == "") - g.vname = this.getVname(g.vid); + this.challenges.concat(this.games).forEach(o => { + if (o.vname == "") + o.vname = this.getVname(o.vid); }); }, }, @@ -133,23 +134,8 @@ export default { }, }, 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}); - // Retrieve live challenge (not older than 30 minute) if any: - const chall = JSON.parse(localStorage.getItem("challenge") || "false"); - if (!!chall) - { - // NOTE: a challenge survives 3 minutes, for potential connection issues - if ((Date.now() - chall.added)/1000 <= 3*60) - { - chall.added = Date.now(); //update added time, for next disconnect... - this.challenges.push(chall); - localStorage.setItem("challenge", JSON.stringify(chall)); - } - else - localStorage.removeItem("challenge"); - } + this.$set(this.people, my.sid, {id:my.id, name:my.name, pages:["/"]}); // Ask server for current corr games (all but mines) ajax( "/games", @@ -163,7 +149,7 @@ export default { })); } ); - // Also ask for corr challenges (open + sent to me) + // Also ask for corr challenges (open + sent by/to me) ajax( "/challenges", "GET", @@ -171,57 +157,90 @@ export default { response => { // Gather all senders names, and then retrieve full identity: // (TODO [perf]: some might be online...) - const uids = response.challenges.map(c => { return c.uid }); - ajax("/users", - "GET", - { ids: uids.join(",") }, - response2 => { - let names = {}; - response2.users.forEach(u => {names[u.id] = u.name}); - this.challenges = this.challenges.concat( - response.challenges.map(c => { - // (just players names in fact) - const from = {name: names[c.uid], id: c.uid}; - const type = this.classifyObject(c); - const vname = this.getVname(c.vid); - return Object.assign({}, c, {type: type, vname: vname, from: from}); - }) - ) - } - ); + let names = {}; + response.challenges.forEach(c => { + if (c.uid != this.st.user.id) + names[c.uid] = ""; //unknwon for now + else if (!!c.target && c.target != this.st.user.id) + names[c.target] = ""; + }); + const addChallenges = (newChalls) => { + names[this.st.user.id] = this.st.user.name; //in case of + this.challenges = this.challenges.concat( + response.challenges.map(c => { + const from = {name: names[c.uid], id: c.uid}; //or just name + const type = this.classifyObject(c); + const vname = this.getVname(c.vid); + return Object.assign({}, + { + type: type, + vname: vname, + from: from, + to: (!!c.target ? names[c.target] : ""), + }, + c); + }) + ); + }; + if (names !== {}) + { + ajax("/users", + "GET", + { ids: Object.keys(names).join(",") }, + response2 => { + response2.users.forEach(u => {names[u.id] = u.name}); + addChallenges(); + } + ); + } + else + addChallenges(); } ); - // 0.1] Ask server for room composition: - const funcPollClients = () => { - // Same strategy as in Game.vue: send connection - // after we're sure WebSocket is initialized - this.st.conn.send(JSON.stringify({code:"connect"})); - this.st.conn.send(JSON.stringify({code:"pollclients"})); - this.st.conn.send(JSON.stringify({code:"pollgamers"})); - }; - if (!!this.st.conn && this.st.conn.readyState == 1) //1 == OPEN state - funcPollClients(); - else //socket not ready yet (initial loading) - this.st.conn.onopen = funcPollClients; - this.st.conn.onmessage = this.socketMessageListener; - const socketCloseListener = () => { - store.socketCloseListener(); //reinitialize connexion (in store.js) - this.st.conn.addEventListener('message', this.socketMessageListener); - this.st.conn.addEventListener('close', socketCloseListener); + const connectAndPoll = () => { + this.send("connect"); + this.send("pollclientsandgamers"); }; - this.st.conn.onclose = socketCloseListener; + // 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.onopen = connectAndPoll; + this.conn.onmessage = this.socketMessageListener; + this.conn.onclose = this.socketCloseListener; }, mounted: function() { [document.getElementById("infoDiv"),document.getElementById("newgameDiv")] .forEach(elt => elt.addEventListener("click", processModalClick)); - document.querySelectorAll("#predefinedTimeControls > button").forEach( + document.querySelectorAll("#predefinedCadences > button").forEach( (b) => { b.addEventListener("click", - () => { this.newchallenge.timeControl = b.innerHTML; } + () => { this.newchallenge.cadence = b.innerHTML; } )} ); }, + beforeDestroy: function() { + this.send("disconnect"); + }, methods: { // Helpers: + send: function(code, obj) { + if (!!this.conn) + { + this.conn.send(JSON.stringify( + Object.assign( + {code: code}, + obj, + ) + )); + } + }, + getVname: function(vid) { + const variant = this.st.variants.find(v => v.id == vid); + // this.st.variants might be uninitialized (variant == null) + return (!!variant ? variant.name : ""); + }, filterChallenges: function(type) { return this.challenges.filter(c => c.type == type); }, @@ -229,16 +248,7 @@ export default { return this.games.filter(g => g.type == type); }, classifyObject: function(o) { //challenge or game - // Heuristic: should work for most cases... (TODO) - return (o.timeControl.indexOf('d') === -1 ? "live" : "corr"); - }, - showGame: function(g) { - // NOTE: we are an observer, since only games I don't play are shown here - // ==> Moves sent by connected remote player(s) if live game - let url = "/game/" + g.id; - if (g.type == "live") - url += "?rid=" + g.rid; - this.$router.push(url); + return (o.cadence.indexOf('d') === -1 ? "live" : "corr"); }, setDisplay: function(letter, type, e) { this[letter + "display"] = type; @@ -248,105 +258,165 @@ export default { else e.target.nextElementSibling.classList.remove("active"); }, - getVname: function(vid) { - const variant = this.st.variants.find(v => v.id == vid); - // this.st.variants might be uninitialized (variant == null) - return (!!variant ? variant.name : ""); - }, - processChat: function(chat) { - // When received on server, this will trigger a "notifyRoom" - this.st.conn.send(JSON.stringify({code:"newchat", chat: chat})); + getActionLabel: function(sid) { + return this.people[sid].pages.some(p => p == "/") + ? "Challenge" + : "Observe"; }, - sendSomethingTo: function(to, code, obj, warnDisconnected) { - const doSend = (code, obj, sid) => { - this.st.conn.send(JSON.stringify(Object.assign( - {code: code}, - obj, - {target: sid} - ))); - }; - if (!to || (!to.sid && !to.name)) + challOrWatch: function(sid) { + if (this.people[sid].pages.some(p => p == "/")) { - // Open challenge: send to all connected players (me excepted) - Object.keys(this.people).forEach(sid => { - if (sid != this.st.user.sid) - doSend(code, obj, sid); - }); + // Available, in Hall + this.newchallenge.to = this.people[sid].name; + doClick("modalNewgame"); } else { - let targetSid = ""; - if (!!to.sid) - targetSid = to.sid; - else - { - if (to.name == this.st.user.name) - return alert(this.st.tr["Cannot challenge self"]); - // Challenge with targeted players - targetSid = - Object.keys(this.people).find(sid => this.people[sid].name == to.name); - if (!targetSid) - { - if (!!warnDisconnected) - alert(this.st.tr["Warning: target is not connected"]); - return false; - } - } - doSend(code, obj, targetSid); + // In some game, maybe playing maybe not + const gid = this.people[sid].page.match(/[a-zA-Z0-9]+$/)[0]; + this.showGame(this.games.find(g => g.id == gid)); } - return true; + }, + showGame: function(g) { + // NOTE: we are an observer, since only games I don't play are shown here + // ==> Moves sent by connected remote player(s) if live game + let url = "/game/" + g.id; + if (g.type == "live") + url += "?rid=" + g.rids[Math.floor(Math.random() * g.rids.length)]; + this.$router.push(url); + }, + processChat: function(chat) { + this.send("newchat", {data:chat}); }, // Messaging center: socketMessageListener: function(msg) { + if (!this.conn) + return; const data = JSON.parse(msg.data); switch (data.code) { - case "duplicate": - alert(this.st.tr["Warning: multi-tabs not supported"]); - break; - // 0.2] Receive clients list (just socket IDs) - case "pollclients": - data.sockIds.forEach(sid => { - this.$set(this.people, sid, {id:0, name:""}); - // Ask identity and challenges - this.st.conn.send(JSON.stringify({code:"askidentity", target:sid})); - this.st.conn.send(JSON.stringify({code:"askchallenge", target:sid})); + case "pollclientsandgamers": + { + // Since people can be both in Hall and Game, + // need to track "askIdentity" requests: + let identityAsked = {}; + data.sockIds.forEach(s => { + if (s.sid != this.st.user.sid && !identityAsked[s.sid]) + { + identityAsked[s.sid] = true; + this.send("askidentity", {target:s.sid}); + } + if (!this.people[s.sid]) + this.$set(this.people, s.sid, {id:0, name:"", pages:[s.page || "/"]}); + else if (!!s.page && this.people[s.sid].pages.indexOf(s.page) < 0) + this.people[s.sid].pages.push(s.page); + if (!s.page) //peer is in Hall + this.send("askchallenge", {target:s.sid}); + else //peer is in Game + this.send("askgame", {target:s.sid}); }); break; - case "pollgamers": - // NOTE: we could make a difference between people in hall - // and gamers, but is it necessary? - data.sockIds.forEach(sid => { - this.$set(this.people, sid, {id:0, name:"", gamer:true}); - this.st.conn.send(JSON.stringify({code:"askidentity", target:sid})); - }); - // Also ask current games to all playing peers (TODO: some design issue) - this.st.conn.send(JSON.stringify({code:"askgames"})); + } + case "connect": + case "gconnect": + // NOTE: player could have been polled earlier, but might have logged in then + // So it's a good idea to ask identity if he was anonymous. + // But only ask game / challenge if currently disconnected. + if (!this.people[data.from]) + { + this.$set(this.people, data.from, {name:"", id:0, pages:[data.page]}); + if (data.code == "connect") + this.send("askchallenge", {target:data.from}); + else + this.send("askgame", {target:data.from}); + } + else + { + // append page if not already in list + if (this.people[data.from].pages.indexOf(data.page) < 0) + this.people[data.from].pages.push(data.page); + } + if (this.people[data.from].id == 0) + { + this.newConnect[data.from] = true; //for self multi-connects tests + this.send("askidentity", {target:data.from}); + } break; - case "askidentity": - { - // Request for identification: reply if I'm not anonymous - if (this.st.user.id > 0) + case "disconnect": + case "gdisconnect": + // Disconnect means no more tmpIds: + if (data.code == "disconnect") { - this.st.conn.send(JSON.stringify({code:"identity", - user: { - // NOTE: decompose to avoid revealing email - name: this.st.user.name, - sid: this.st.user.sid, - id: this.st.user.id, - }, - target:data.from})); + // Remove the live challenge sent by this player: + ArrayFun.remove(this.challenges, c => c.from.sid == data.from); } + else + { + // Remove the matching live game if now unreachable + const gid = data.page.match(/[a-zA-Z0-9]+$/)[0]; + const gidx = this.games.findIndex(g => g.id == gid); + if (gidx >= 0) + { + const game = this.games[gidx]; + if (game.type == "live" && + game.rids.length == 1 && game.rids[0] == data.from) + { + this.games.splice(gidx, 1); + } + } + } + const page = data.page || "/"; + ArrayFun.remove(this.people[data.from].pages, p => p == page); + if (this.people[data.from].pages.length == 0) + this.$delete(this.people, data.from); + break; + case "killed": + // I logged in elsewhere: + alert(this.st.tr["New connexion detected: tab now offline"]); + // 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; + break; + case "askidentity": + { + // Request for identification (TODO: anonymous shouldn't need to reply) + const me = { + // Decompose to avoid revealing email + name: this.st.user.name, + sid: this.st.user.sid, + id: this.st.user.id, + }; + this.send("identity", {data:me, target:data.from}); break; } case "identity": { - this.$set(this.people, data.user.sid, + const user = data.data; + if (!!user.name) //otherwise anonymous + { + // If I multi-connect, kill current connexion if no mark (I'm older) + if (this.newConnect[user.sid] && user.id > 0 + && user.id == this.st.user.id && user.sid != 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... { - id: data.user.id, - name: data.user.name, - gamer: this.people[data.user.sid].gamer, - }); + this.$set(this.people, user.sid, + { + id: user.id, + name: user.name, + pages: this.people[user.sid].pages, + }); + } + } + delete this.newConnect[user.sid]; break; } case "askchallenge": @@ -357,148 +427,119 @@ export default { if (cIdx >= 0) { const c = this.challenges[cIdx]; - if (!!c.to) - { - // Only share targeted challenges to the targets: - const toSid = Object.keys(this.people).find(k => - this.people[k].name == c.to); - if (toSid != data.from) - return; - } + // NOTE: in principle, should only send targeted challenge to the target. + // But we may not know yet the identity of the target (just name), + // so cannot decide if data.from is the target or not. const myChallenge = { - // Minimal challenge informations: (from not required) id: c.id, + from: this.st.user.sid, to: c.to, fen: c.fen, vid: c.vid, - timeControl: c.timeControl, + cadence: c.cadence, + added: c.added, }; - this.st.conn.send(JSON.stringify({code:"challenge", - chall:myChallenge, target:data.from})); + this.send("challenge", {data:myChallenge, target:data.from}); } break; } - case "challenge": + case "challenge": //after "askchallenge" + case "newchallenge": { - // Receive challenge from some player (+sid) - let newChall = data.chall; - newChall.type = this.classifyObject(data.chall); - newChall.from = - Object.assign({sid:data.from}, this.people[data.from]); - newChall.added = Date.now(); //TODO: this is reception timestamp, not creation - newChall.vname = this.getVname(newChall.vid); - this.challenges.push(newChall); - break; - } - case "game": - { - // Receive game from some player (+sid) - // NOTE: it may be correspondance (if newgame while we are connected) - // If duplicate found: select rid (remote ID) at random - let game = this.games.find(g => g.id == data.game.id); - if (!!game && Math.random() < 0.5) - game.rid = data.from; - else + // NOTE about next condition: see "askchallenge" case. + const chall = data.data; + if (!chall.to || (this.people[chall.from].id > 0 && + (chall.from == this.st.user.sid || chall.to == this.st.user.name))) { - let newGame = data.game; - newGame.type = this.classifyObject(data.game); - newGame.vname = this.getVname(data.game.vid); - newGame.rid = data.from; - this.games.push(newGame); + let newChall = Object.assign({}, chall); + newChall.type = this.classifyObject(chall); + newChall.added = Date.now(); + let fromValues = Object.assign({}, this.people[chall.from]); + delete fromValues["pages"]; //irrelevant in this context + newChall.from = Object.assign({sid:chall.from}, fromValues); + newChall.vname = this.getVname(newChall.vid); + this.challenges.push(newChall); } break; } - case "newgame": - { - // TODO: next line required ?! - //ArrayFun.remove(this.challenges, c => c.id == data.cid); - // New game just started: data contain all information - if (this.classifyObject(data.gameInfo) == "live") - this.startNewGame(data.gameInfo); - else - { - this.infoMessage = "New game started: " + - "" + - "#/game/" + data.gameInfo.id + ""; - let modalBox = document.getElementById("modalInfo"); - modalBox.checked = true; - setTimeout(() => { modalBox.checked = false; }, 3000); - } - break; - } - case "newchat": - this.newChat = data.chat; - break; case "refusechallenge": { - ArrayFun.remove(this.challenges, c => c.id == data.cid); - localStorage.removeItem("challenge"); + const cid = data.data; + ArrayFun.remove(this.challenges, c => c.id == cid); alert(this.st.tr["Challenge declined"]); break; } case "deletechallenge": { // NOTE: the challenge may be already removed - ArrayFun.remove(this.challenges, c => c.id == data.cid); - localStorage.removeItem("challenge"); //in case of + const cid = data.data; + ArrayFun.remove(this.challenges, c => c.id == cid); break; } - case "connect": - case "gconnect": - this.$set(this.people, data.from, {name:"", id:0, gamer:data.code[0]=='g'}); - this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from})); - if (data.code == "connect") - this.st.conn.send(JSON.stringify({code:"askchallenge", target:data.from})); + case "game": //individual request + case "newgame": + { + // NOTE: it may be live or correspondance + const game = data.data; + let locGame = this.games.find(g => g.id == game.id); + if (!locGame) + { + let newGame = game; + newGame.type = this.classifyObject(game); + newGame.vname = this.getVname(game.vid); + if (!game.score) //if new game from Hall + newGame.score = "*"; + this.games.push(newGame); + } else - this.st.conn.send(JSON.stringify({code:"askgame", target:data.from})); - break; - case "disconnect": - case "gdisconnect": - this.$delete(this.people, data.from); - if (data.code == "disconnect") { - // Also remove all challenges sent by this player: - ArrayFun.remove(this.challenges, c => c.from.sid == data.from); + // Append rid (if not already in list) + if (!locGame.rids.includes(game.rid)) + locGame.rids.push(game.rid); } + break; + } + case "startgame": + { + // New game just started: data contain all information + const gameInfo = data.data; + if (this.classifyObject(gameInfo) == "live") + this.startNewGame(gameInfo); else { - // And all live games where he plays and no other opponent is online - ArrayFun.remove(this.games, g => - g.type == "live" && (g.players.every(p => p.sid == data.from - || !this.people[p.sid])), "all"); + this.infoMessage = this.st.tr["New correspondance game:"] + + " " + + "#/game/" + gameInfo.id + ""; + let modalBox = document.getElementById("modalInfo"); + modalBox.checked = true; + setTimeout(() => { modalBox.checked = false; }, 3000); } break; + } + case "newchat": + this.newChat = data.data; + break; } }, - // Challenge lifecycle: - tryChallenge: function(sid) { - if (this.people[sid].id == 0) - return; //anonymous players cannot be challenged - // TODO: SID is available, so we could use it instead of searching from name - this.newchallenge.to = this.people[sid].name; - doClick("modalNewgame"); - }, - challOrWatch: function(sid, e) { - switch (e.target.innerHTML) - { - case "Available": - this.tryChallenge(sid); - break; - case "Playing": - this.showGame(this.games.find( - g => g.players.some(pl => pl.sid == sid || pl.uid == this.people[sid].id))); - break; - }; + socketCloseListener: function() { + if (!this.conn) + return; + this.conn = new WebSocket(this.connexionString); + this.conn.addEventListener("message", this.socketMessageListener); + this.conn.addEventListener("close", this.socketCloseListener); }, + // Challenge lifecycle: newChallenge: async function() { if (this.newchallenge.vid == "") return alert(this.st.tr["Please select a variant"]); + if (!!this.newchallenge.to && this.newchallenge.to == this.st.user.name) + return alert(this.st.tr["Self-challenge is forbidden"]); const vname = this.getVname(this.newchallenge.vid); const vModule = await import("@/variants/" + vname + ".js"); window.V = vModule.VariantRules; - if (!!this.newchallenge.timeControl.match(/^[0-9]+$/)) - this.newchallenge.timeControl += "+0"; //assume minutes, no increment + if (!!this.newchallenge.cadence.match(/^[0-9]+$/)) + this.newchallenge.cadence += "+0"; //assume minutes, no increment const error = checkChallenge(this.newchallenge); if (!!error) return alert(error); @@ -507,21 +548,15 @@ export default { return alert(this.st.tr["Please log in to play correspondance games"]); // NOTE: "from" information is not required here let chall = Object.assign({}, this.newchallenge); - const finishAddChallenge = (cid,warnDisconnected) => { + const finishAddChallenge = (cid) => { chall.id = cid || "c" + getRandString(); - // Send challenge to peers (if connected) - const isSent = this.sendSomethingTo({name:chall.to}, "challenge", - {chall:chall}, !!warnDisconnected); - if (!isSent) - return; - // Remove old challenge if any (only one at a time): + // Remove old challenge if any (only one at a time of a given type): const cIdx = this.challenges.findIndex(c => - c.from.sid == this.st.user.sid && c.type == ctype); + (c.from.sid == this.st.user.sid || c.from.id == this.st.user.id) && c.type == ctype); if (cIdx >= 0) { // Delete current challenge (will be replaced now) - this.sendSomethingTo({name:this.challenges[cIdx].to}, - "deletechallenge", {cid:this.challenges[cIdx].id}); + this.send("deletechallenge", {data:this.challenges[cIdx].id}); if (ctype == "corr") { ajax( @@ -532,28 +567,27 @@ export default { } this.challenges.splice(cIdx, 1); } + this.send("newchallenge", {data:Object.assign({from:this.st.user.sid}, chall)}); // Add new challenge: - chall.added = Date.now(); - // NOTE: vname and type are redundant (can be deduced from timeControl + vid) - chall.type = ctype; - chall.vname = vname; chall.from = { //decompose to avoid revealing email sid: this.st.user.sid, id: this.st.user.id, name: this.st.user.name, }; + chall.added = Date.now(); + // NOTE: vname and type are redundant (can be deduced from cadence + vid) + chall.type = ctype; + chall.vname = vname; this.challenges.push(chall); - if (ctype == "live") - localStorage.setItem("challenge", JSON.stringify(chall)); - // Also remember timeControl + vid for quicker further challenges: - localStorage.setItem("timeControl", chall.timeControl); + // Remember cadence + vid for quicker further challenges: + localStorage.setItem("cadence", chall.cadence); localStorage.setItem("vid", chall.vid); document.getElementById("modalNewgame").checked = false; }; if (ctype == "live") { // Live challenges have a random ID - finishAddChallenge(null, "warnDisconnected"); + finishAddChallenge(null); } else { @@ -590,11 +624,9 @@ export default { } else { - this.st.conn.send(JSON.stringify({ - code: "refusechallenge", - cid: c.id, target: c.from.sid})); + this.send("refusechallenge", {data:c.id, target:c.from.sid}); } - this.sendSomethingTo(!!c.to ? {sid:c.from.sid} : null, "deletechallenge", {cid:c.id}); + this.send("deletechallenge", {data:c.id}); } else //my challenge { @@ -606,26 +638,23 @@ export default { {id: c.id} ); } - else //live - localStorage.removeItem("challenge"); - this.sendSomethingTo({name:c.to}, "deletechallenge", {cid:c.id}); + this.send("deletechallenge", {data:c.id}); } // In all cases, the challenge is consumed: ArrayFun.remove(this.challenges, ch => ch.id == c.id); }, - // NOTE: when launching game, the challenge is already deleted + // NOTE: when launching game, the challenge is already being deleted launchGame: async function(c) { const vModule = await import("@/variants/" + c.vname + ".js"); window.V = vModule.VariantRules; - // These game informations will be sent to other players - const gameInfo = + // These game informations will be shared + let gameInfo = { id: getRandString(), fen: c.fen || V.GenRandInitFen(), players: shuffle([c.from, c.seat]), //white then black vid: c.vid, - vname: c.vname, //theoretically vid is enough, but much easier with vname - timeControl: c.timeControl, + cadence: c.cadence, }; let oppsid = c.from.sid; //may not be defined if corr + offline opp if (!oppsid) @@ -633,17 +662,15 @@ export default { oppsid = Object.keys(this.people).find(sid => this.people[sid].id == c.from.id); } - const tryNotifyOpponent = () => { + const notifyNewgame = () => { if (!!oppsid) //opponent is online - { - this.st.conn.send(JSON.stringify({code:"newgame", - gameInfo:gameInfo, target:oppsid, cid:c.id})); - } + this.send("startgame", {data:gameInfo, target:oppsid}); + // Send game info (only if live) to everyone except me in this tab + this.send("newgame", {data:gameInfo}); }; if (c.type == "live") { - // NOTE: in this case we are sure opponent is online - tryNotifyOpponent(); + notifyNewgame(); this.startNewGame(gameInfo); } else //corr: game only on server @@ -654,32 +681,19 @@ export default { {gameInfo: gameInfo, cid: c.id}, //cid useful to delete challenge response => { gameInfo.id = response.gameId; - tryNotifyOpponent(); + notifyNewgame(); this.$router.push("/game/" + response.gameId); } ); } - // Send game info to everyone except opponent (and me) - Object.keys(this.people).forEach(sid => { - if (![this.st.user.sid,oppsid].includes(sid)) - { - this.st.conn.send(JSON.stringify({code:"game", - game: { //minimal game info: - id: gameInfo.id, - players: gameInfo.players, - vid: gameInfo.vid, - timeControl: gameInfo.timeControl, - }, - target: sid})); - } - }); }, // NOTE: for live games only (corr games start on the server) startNewGame: function(gameInfo) { const game = Object.assign({}, gameInfo, { // (other) Game infos: constant fenStart: gameInfo.fen, - added: Date.now(), + vname: this.getVname(gameInfo.vid), + created: Date.now(), // Game state (including FEN): will be updated moves: [], clocks: [-1, -1], //-1 = unstarted