From: Benjamin Auder <benjamin.auder@somewhere> Date: Fri, 8 Feb 2019 15:27:44 +0000 (+0100) Subject: Work on sockets + challenge system X-Git-Url: https://git.auder.net/images/doc/html/current/pieces/%7B%7B?a=commitdiff_plain;h=b4d619d12f3b983c188ca94826e101928016f013;p=vchess.git Work on sockets + challenge system --- diff --git a/_tmp/TODO b/_tmp/TODO index 02787bf3..86d36fe3 100644 --- a/_tmp/TODO +++ b/_tmp/TODO @@ -1,3 +1,11 @@ +--> correspondance: stocker sur serveur lastMove + uid + color + movesCount + gameId + variant + timeleft +fin de partie corr: supprimer partie du serveur au bout de 7 jours (arbitraire) +// TODO: au moins l'échange des coups en P2P ? et game chat ? + +// TODO: surligner "hall" (menu) si nouveau défi perso (reçu) et pas affichage courant +// de même surligner "my games" si c'est à nous de jouer dans une partie (corr) +// ==> myGames componentn + Game component must listen for "new move" events + Hall + problems : similar pages, with "New game[problem]" button with a list of variants. --> but display all challenges (and all problems) diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 674305a2..145d8987 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -1,3 +1,5 @@ +<!-- Main playing hall: online players + current challenges + button "new game" --> + <template lang="pug"> main input#modalNewgame.modal(type="checkbox") @@ -15,10 +17,10 @@ main option(v-show="possibleNbplayers(3)" value="3") 3 option(v-show="possibleNbplayers(4)" value="4") 4 fieldset - label(for="timeControl") Time control (e.g. 3m, 1h+30s, 7d+1d) + label(for="timeControl") {{ st.tr["Time control"] }} input#timeControl(type="text" v-model="newchallenge.timeControl" - placeholder="Time control") - fieldset + placeholder="3m+2s, 1h+30s, 7d+1d ...") + fieldset(v-if="st.user.id > 0") label(for="selectPlayers") {{ st.tr["Play with? (optional)"] }} #selectPlayers input(type="text" v-model="newchallenge.to[0].name") @@ -26,10 +28,10 @@ main v-model="newchallenge.to[1].name") input(v-show="newchallenge.nbPlayers==4" type="text" v-model="newchallenge.to[2].name") - fieldset + fieldset(v-if="st.user.id > 0") label(for="inputFen") {{ st.tr["FEN (optional)"] }} input#inputFen(type="text" v-model="newchallenge.fen") - button(@click="newChallenge") Send challenge + button(@click="newChallenge") {{ st.tr["Send challenge"] }} .row .col-sm-12.col-md-5.col-md-offset-1.col-lg-4.col-lg-offset-2 .button-group @@ -39,8 +41,8 @@ main :challenges="challenges" @click-challenge="clickChallenge") #players(v-show="cpdisplay=='players'") h3 Online players - //TODO: uniquePlayers, show "5 anonymous", and do nothing on click on anonymous - div(v-for="p in uniquePlayers" @click="challenge(p)") {{ p.name }} + div(v-for="p in uniquePlayers" @click="tryChallenge(p)") + | {{ p.name + (!!p.count ? " ("+p.count+")" : "") }} .row .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 button(onClick="doClick('modalNewgame')") New game @@ -56,37 +58,6 @@ main </template> <script> -// TODO: blank time control == untimed -// main playing hall: online players + current challenges + button "new game" -// TODO: si on est en train de jouer une partie, le notifier aux nouveaux connectés -/* -TODO: surligner si nouveau défi perso et pas affichage courant -(cadences base + incrément, corr == incr >= 1jour ou base >= 7j) ---> correspondance: stocker sur serveur lastMove + uid + color + movesCount + gameId + variant + timeleft -fin de partie corr: supprimer partie du serveur au bout de 7 jours (arbitraire) -*/ -// TODO: au moins l'échange des coups en P2P ? et game chat ? -// TODO: objet game, objet challenge ? et player ? -/* - * Possible events: - * - send new challenge (corr or live, cf. time control), with button or click on player - * - accept challenge (corr or live) --> send info to all concerned players - * - cancel challenge (click on sent challenge) --> send info to all concerned players - * - withdraw from challenge (if >= 3 players and previously accepted) - * --> send info to all concerned players - * - prepare and start new game (if challenge is full after acceptation) - * Also send to all connected players (only from me) - * - receive "player connect": send all our current challenges (to him or global) - * Also send all our games (live - max 1 - and corr) [in web worker ?] - * + all our sent challenges. - * - receive "playergames": list of games by some connected player (NO corr) - * - receive "playerchallenges": list of challenges (sent) by some online player (NO corr) - * - receive "player disconnect": remove from players list - * - receive "accept/withdraw/cancel challenge": apply action to challenges list - * - receive "new game": if live, store locally + redirect to game - * If corr: notify "new game has started", give link, but do not redirect - * - receive new challenge: if targeted, replace our name with sender name -*/ import { store } from "@/store"; import { NbPlayers } from "@/data/nbPlayers"; import { checkChallenge } from "@/data/challengeCheck"; @@ -103,12 +74,12 @@ export default { data: function () { return { st: store.state, + cpdisplay: "challenges", gdisplay: "live", liveGames: [], corrGames: [], + challenges: [], players: [], //online players - challenges: [], //live challenges - willPlay: [], //IDs of challenges in which I decide to play (>= 3 players) newchallenge: { fen: "", vid: 0, @@ -124,6 +95,20 @@ export default { }, }; }, + computed: { + uniquePlayers: function() { + // Show e.g. "5 @nonymous", and do nothing on click on anonymous + let playerList = [{id:0, name:"@nonymous", count:0}]; + this.players.forEach(p => { + if (p.id > 0) + playerList.push(p); + else + playerList[0].count++; + }); + return playerList; + }, + }, + // TODO: this looks ugly... (use VueX ?!) watch: { "st.conn": function() { this.st.conn.onmessage = this.socketMessageListener; @@ -132,23 +117,45 @@ export default { }, created: function() { // TODO: ask server for current corr games (all but mines: names, ID, time control) + // also ask for corr challenges + // TODO: add myself to players + // --> when sending something, send to all players but NOT me ! if (!!this.st.conn) { this.st.conn.onmessage = this.socketMessageListener; this.st.conn.onclose = this.socketCloseListener; } + this.players.push(this.st.user); }, methods: { socketMessageListener: function(msg) { const data = JSON.parse(msg.data); switch (data.code) { +// * - receive "new game": if live, store locally + redirect to game +// * If corr: notify "new game has started", give link, but do not redirect case "newgame": // TODO: new game just started: data contain all informations // (id, players, time control, fenStart ...) + // + cid to remove challenge from list + break; +// * - receive "playergame": a live game by some connected player (NO corr) + case "playergame": + // TODO: receive live game summary (update, count moves) + // (just players names, time control, and ID + player ID) + break; +// * - receive "playerchallenges": list of challenges (sent) by some online player (NO corr) + case "playerchallenges": + // TODO: receive challenge + challenge updates break; - // TODO: also receive live games summaries (update) - // (just players names, time control, and ID + player ID) + case "newmove": //live or corr + // TODO: name conflict ? (game "newmove" event) + break; +// * - receive new challenge: if targeted, replace our name with sender name + case "newchallenge": + // receive live or corr challenge + break; +// * - receive "accept/withdraw/cancel challenge": apply action to challenges list case "acceptchallenge": if (true) //TODO: if challenge is full this.newGame(data.challenge, data.user); //user.id et user.name @@ -162,10 +169,18 @@ export default { case "cancelchallenge": ArrayFun.remove(this.challenges, c => c.id == data.cid); break; - case "hallconnect": +// NOTE: finally only one connect / disconnect couple of events +// (because on server side we wouldn't know which to choose) + case "connect": +// * - receive "player connect": send all our current challenges (to him or global) +// * Also send all our games (live - max 1 - and corr) [in web worker ?] +// * + all our sent challenges. this.players.push({name:data.name, id:data.uid}); + // TODO: si on est en train de jouer une partie, le notifier au nouveau connecté + // envoyer aussi nos défis break; - case "halldisconnect": +// * - receive "player disconnect": remove from players list + case "disconnect": ArrayFun.remove(this.players, p => p.id == data.uid); // TODO: also remove all challenges sent by this player, // and all live games where he plays and no other opponent is online @@ -173,20 +188,35 @@ export default { } }, socketCloseListener: function() { - this.st.conn.addEventListener('message', socketMessageListener); - this.st.conn.addEventListener('close', socketCloseListener); - }, - clickPlayer: function() { - //this.newgameInfo.players[0].name = clickPlayer.name; - //show modal; + // connexion is reinitialized in store.js + this.st.conn.addEventListener('message', this.socketMessageListener); + this.st.conn.addEventListener('close', this.socketCloseListener); }, showGame: function(game) { // NOTE: if we are an observer, the game will be found in main games list // (sent by connected remote players) + // TODO: game path ? /vname/gameId seems better this.$router.push("/" + game.id) }, - challenge: function(player) { + tryChallenge: function(player) { + if (player.id == 0) + return; //anonymous players cannot be challenged + this.newchallenge.players[0] = { + name: player.name, + id: player.id, + sid: player.sid, + }; + doClick("modalNewgame"); }, +// * - accept challenge (corr or live) --> send info to all concerned players +// * - cancel challenge (click on sent challenge) --> send info to all concerned players +// * - withdraw from challenge (if >= 3 players and previously accepted) +// * --> send info to all concerned players +// * - refuse challenge (or receive refusal): send to all challenge players (from + to) +// * except us ; graphics: modal again ? (inline ?) +// * - prepare and start new game (if challenge is full after acceptation) +// * --> include challenge ID (so that opponents can delete the challenge too) +// * Also send to all connected players (only from me) clickChallenge: function(challenge) { const index = this.challenges.findIndex(c => c.id == challenge.id); const toIdx = challenge.to.findIndex(p => p.id == user.id); @@ -214,18 +244,19 @@ export default { // si pas le mien et FEN speciale :: (charger code variante et) // montrer diagramme + couleur (orienté) }, - // user: last person to accept the challenge - newGame: function(chall, user) { - const fen = chall.fen || V.GenRandInitFen(); - const game = {}; //TODO: fen, players, time ... - //setStorage(game); //TODO - game.players.forEach(p => { //...even if game is by corr (could be played live, why not...) - this.conn.send( - JSON.stringify({code:"newgame", oppid:p.id, game:game})); - }); - if (this.settings.sound >= 1) - new Audio("/sounds/newgame.mp3").play().catch(err => {}); - }, + // user: last person to accept the challenge (TODO: revoir ça) +// newGame: function(chall, user) { +// const fen = chall.fen || V.GenRandInitFen(); +// const game = {}; //TODO: fen, players, time ... +// //setStorage(game); //TODO +// game.players.forEach(p => { //...even if game is by corr (could be played live, why not...) +// this.conn.send( +// JSON.stringify({code:"newgame", oppid:p.id, game:game})); +// }); +// if (this.settings.sound >= 1) +// new Audio("/sounds/newgame.mp3").play().catch(err => {}); +// }, + // Send new challenge (corr or live, cf. time control), with button or click on player newChallenge: async function() { const idxInVariants = this.st.variants.findIndex(v => v.id == this.newchallenge.vid); @@ -251,6 +282,7 @@ export default { p.sid = this.players[pIdx].sid; } } + // TODO: clarify challenge format (too many fields for now :/ ) const finishAddChallenge = (cid) => { const chall = Object.assign( {}, @@ -263,12 +295,9 @@ export default { } ); this.challenges.push(chall); - document.getElementById("modalNewgame").checked = false; - }; - if (liveGame) - { + // Send challenge to peers const chall = JSON.stringify({ - code: "sendchallenge", + code: "newchallenge", sender: {name:this.st.user.name, id:this.st.user.id, sid:this.st.user.sid}, }); if (this.newchallenge.to[0].id > 0) @@ -284,12 +313,16 @@ export default { // Open challenge: send to all connected players this.players.forEach(p => { this.st.conn.send(chall); }); } + document.getElementById("modalNewgame").checked = false; + }; + if (liveGame) + { // Live challenges have cid = 0 finishAddChallenge(0); } - else //correspondance game: + else { - // Possible (server) error if filled player does not exist + // Correspondance game: send challenge to server ajax( "/challenges/" + this.newchallenge.vid, "POST", diff --git a/server/sockets.js b/server/sockets.js index 80e4c442..1d2f9400 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -29,8 +29,7 @@ function remInArray(arr, item) //TODO: programmatic re-navigation on current game if we receive a move and are not there module.exports = function(wss) { - let clients = {}; //associative array client sid --> {socket, curPath} - let pages = {}; //associative array path --> array of client sid + let clients = {}; //associative array client sid --> socket // No-op function as a callback when sending messages const noop = () => { }; wss.on("connection", (socket, req) => { @@ -39,130 +38,60 @@ module.exports = function(wss) { // Ignore duplicate connections (on the same live game that we play): if (!!clients[sid]) return socket.send(JSON.stringify({code:"duplicate"})); - // We don't know yet on which page the user will be - clients[sid] = {socket: socket, path: ""}; - -// socket.on("message", objtxt => { -// let obj = JSON.parse(objtxt); -// switch (obj.code) -// { -// case "enter": -// if (clients[sid].path.length > 0) -// remInArray(pages[clients[sid].path], sid); -// clients[sid].path = obj.path; -// pages[obj.path].push(sid); -// // TODO also: notify "old" sub-room that I left (if it was not index) -// if (obj.path == "/") -// { -// // Send counting info -// let countings = {}; -// Object.keys(pages).forEach( -// path => { countings[path] = pages[path].length; }); -// socket.send(JSON.stringify({code:"counts",counts:countings})); -// } -// else -// { -// // Send to every client connected on index an update message for counts -// pages["/"].forEach((id) => { -// clients[id].socket.send( -// JSON.stringify({code:"increase",path:obj.path}), noop); -// }); -// // TODO: do not notify anything in rules and problems sections (no socket required) -// // --> in fact only /Atomic (main hall) and inside a game: /Atomic/392f3ju -// // Also notify the (sub-)room (including potential opponents): -// Object.keys(clients[page]).forEach( k => { -// clients[page][k].send(JSON.stringify({code:"connect",id:sid}), noop); -// }); -// // Finally, receive (sub-)room composition -// // TODO. -// } -//// NOTE: no "leave" counterpart (because it's always to enter somewhere else) -//// case "leave": -//// break; -// // Transmit chats and moves to current room -// // TODO: WebRTC instead in this case (most demanding?) -// case "newchat": -// if (!!clients[page][obj.oppid]) -// { -// clients[page][obj.oppid].send( -// JSON.stringify({code:"newchat",msg:obj.msg}), noop); -// } -// break; -// case "newmove": -// if (!!clients[page][obj.oppid]) -// { -// clients[page][obj.oppid].send( -// JSON.stringify({code:"newmove",move:obj.move}), noop); -// } -// break; -// -// -// // TODO: generalize that for several opponents -// case "ping": -// if (!!clients[page][obj.oppid]) -// socket.send(JSON.stringify({code:"pong",gameId:obj.gameId})); -// break; -// case "lastate": -// if (!!clients[page][obj.oppid]) -// { -// const oppId = obj.oppid; -// obj.oppid = sid; //I'm oppid for my opponent -// clients[page][oppId].send(JSON.stringify(obj), noop); -// } -// break; -// // TODO: moreover, here, game info should be sent (through challenge; not stored here) -// case "newgame": -// if (!!games[page]) -// { -// // Start a new game -// const oppId = games[page]["id"]; -// const fen = games[page]["fen"]; -// const gameId = games[page]["gameid"]; -// delete games[page]; -// const mycolor = (Math.random() < 0.5 ? 'w' : 'b'); -// socket.send(JSON.stringify( -// {code:"newgame",fen:fen,oppid:oppId,color:mycolor,gameid:gameId})); -// if (!!clients[page][oppId]) -// { -// clients[page][oppId].send( -// JSON.stringify( -// {code:"newgame",fen:fen,oppid:sid,color:mycolor=="w"?"b":"w",gameid:gameId}), -// noop); -// } -// } -// else -// games[page] = {id:sid, fen:obj.fen, gameid:obj.gameid}; //wait for opponent -// break; -// case "cancelnewgame": //if a user cancel his seek -// // TODO: just transmit event -// //delete games[page]; -// break; -// // TODO: also other challenge events -// case "resign": -// if (!!clients[page][obj.oppid]) -// clients[page][obj.oppid].send(JSON.stringify({code:"resign"}), noop); -// break; -// // TODO: case "challenge" (get ID) --> send to all, "acceptchallenge" (with ID) --> send to all, "cancelchallenge" --> send to all -// // also, "sendgame" (give current game info, if any) --> to new connections, "sendchallenges" (same for challenges) --> to new connections -// } -// }); -// socket.on("close", () => { -// delete clients[sid]; -// // TODO: carefully delete pages[.........] -// // + adapt below: -// if (page != "/") -// { -// // Send to every client connected on index an update message for counts -// Object.keys(clients["index"]).forEach( k => { -// clients["index"][k].send( -// JSON.stringify({code:"decrease",vid:page}), noop); -// }); -// } -// // Also notify potential opponents: -// // hit all clients which check if sid corresponds -// Object.keys(clients[page]).forEach( k => { -// clients[page][k].send(JSON.stringify({code:"disconnect",id:sid}), noop); -// }); -// }); + clients[sid] = socket; + socket.on("message", objtxt => { + let obj = JSON.parse(objtxt); + if (!!obj.oppid && !clients[oppid]) + return; //receiver not connected, nothing we can do + switch (obj.code) + { + // Transmit chats and moves to current room + // TODO: WebRTC instead in this case (most demanding?) + case "newchat": + clients[obj.oppid].send(JSON.stringify({code:"newchat",msg:obj.msg}), noop); + break; + case "newmove": + clients[obj.oppid].send(JSON.stringify({code:"newmove",move:obj.move}), noop); + break; + // TODO: generalize that for several opponents + case "ping": + socket.send(JSON.stringify({code:"pong",gameId:obj.gameId})); + break; + case "lastate": + const oppId = obj.oppid; + obj.oppid = sid; //I'm oppid for my opponent + clients[oppId].send(JSON.stringify(obj), noop); + break; + // TODO: moreover, here, game info should be sent (through challenge; not stored here) + case "newgame": + clients[oppId].send( + JSON.stringify( + {code:"newgame",fen:fen,oppid:sid,color:"w",gameid:"TODO"}), + noop); + break; + case "cancelnewgame": //if a user cancel his seek + // TODO: just transmit event + //delete games[page]; + break; + // TODO: also other challenge events + case "resign": + clients[obj.oppid].send(JSON.stringify({code:"resign"}), noop); + break; + // TODO: case "challenge" (get ID) --> send to all, "acceptchallenge" (with ID) --> send to all, "cancelchallenge" --> send to all + // also, "sendgame" (give current game info, if any) --> to new connections, "sendchallenges" (same for challenges) --> to new connections + case "newchallenge": + console.log("challenge received"); + console.log(obj.sender); + console.log(obj); + break; + } + }); + socket.on("close", () => { + delete clients[sid]; + // Notify every other connected client + Object.keys(clients).forEach( k => { + clients[k].send(JSON.stringify({code:"disconnect",sid:sid}), noop); + }); + }); }); }