From f41ce5806b989c06091a403d7e26ff3c457650c9 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 6 Dec 2019 20:47:35 +0100 Subject: [PATCH] Corr games: almost there. Then remote games + abort/resign/draw/message --- client/src/components/BaseGame.vue | 4 +- client/src/store.js | 4 +- client/src/utils/gameStorage.js | 3 +- client/src/views/Game.vue | 124 ++++++++++++----------------- client/src/views/Hall.vue | 15 ++-- server/db/create.sql | 2 +- server/models/Game.js | 59 ++++++++------ server/routes/games.js | 3 +- server/sockets.js | 54 +++++++------ 9 files changed, 130 insertions(+), 138 deletions(-) diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 8e9ac36a..19aaf154 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -223,8 +223,6 @@ export default { this.lastMove = move; if (this.st.settings.sound == 2) new Audio("/sounds/move.mp3").play().catch(err => {}); - if (!this.analyze) - this.$emit("newmove", move); //post-processing (e.g. computer play) if (!navigate) { move.fen = this.vr.getFen(); @@ -237,6 +235,8 @@ export default { this.moves = this.moves.slice(0,this.cursor).concat([move]); } } + if (!this.analyze) + this.$emit("newmove", move); //post-processing (e.g. computer play) // Is opponent in check? this.incheck = this.vr.getCheckSquares(this.vr.turn); const score = this.vr.getCurrentScore(); diff --git a/client/src/store.js b/client/src/store.js index 3b280d94..2728aaf0 100644 --- a/client/src/store.js +++ b/client/src/store.js @@ -36,8 +36,8 @@ export const store = this.state.user.notify = res.notify; }); } - this.state.conn = new WebSocket( - params.socketUrl + "/?sid=" + mysid + "&page=" + page); + this.state.conn = new WebSocket(params.socketUrl + "/?sid=" + mysid + + "&page=" + encodeURIComponent(page)); // Settings initialized with values from localStorage this.state.settings = { bcolor: localStorage["bcolor"] || "lichess", diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index 0284763a..0b3a7da6 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -79,8 +79,7 @@ export const GameStorage = gid: gameId, newObj: { - // TODO: I think stringify isn't requuired here (see ajax() ) - move: JSON.stringify(obj.move), //may be undefined... + move: obj.move, //may be undefined... fen: obj.fen, score: obj.score, } diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index c292252d..b66c6ef8 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -11,7 +11,7 @@ button(@click="abortGame") {{ st.tr["Game is too boring"] }} BaseGame(:game="game" :vr="vr" ref="basegame" @newmove="processMove" @gameover="gameOver") - // TODO: also show players names + div Names: {{ game.players[0].name }} - {{ game.players[1].name }} div Time: {{ virtualClocks[0] }} - {{ virtualClocks[1] }} .button-group(v-if="game.mode!='analyze' && game.score=='*'") button(@click="offerDraw") Draw @@ -38,6 +38,7 @@ import { store } from "@/store"; import { GameStorage } from "@/utils/gameStorage"; import { ppt } from "@/utils/datetime"; import { extractTime } from "@/utils/timeControl"; +import { ArrayFun } from "@/utils/array"; export default { name: 'my-game', @@ -52,7 +53,7 @@ export default { id: "", rid: "" }, - game: {}, //passed to BaseGame + game: {players:[{name:""},{name:""}]}, //passed to BaseGame corrMsg: "", //to send offline messages in corr games virtualClocks: [0, 0], //initialized with true game.clocks vr: null, //"variant rules" object initialized from FEN @@ -109,14 +110,6 @@ export default { } }, 1000); }, - // In case variants array was't loaded when game was retrieved - "st.variants": function(variantArray) { - if (!!this.game.vname && this.game.vname == "") - { - this.game.vname = variantArray.filter(v => - v.id == this.game.vid)[0].name; - } - }, }, // TODO: redundant code with Hall.vue (related to people array) created: function() { @@ -179,67 +172,50 @@ export default { } case "identity": { - const pIdx = this.people.findIndex(p => p.sid == data.user.sid); - this.people[pIdx].id = data.user.id; - this.people[pIdx].name = data.user.name; - break; - } - case "newmove": - // NOTE: next call will trigger processMove() - this.$refs["basegame"].play(data.move, - "receive", this.game.vname!="Dark" ? "animate" : null); - break; - case "pong": //received if we sent a ping (game still alive on our side) - { - if (this.game.type == "live") //corr games are always complete + let player = this.people.find(p => p.sid == data.user.sid); + player.id = data.user.id; + player.name = data.user.name; + // Sending last state only for live games: corr games are complete + if (this.game.type == "live" && this.game.oppsid == player.sid) { - // Send our "last state" informations to opponent(s) + // Send our "last state" informations to opponent const L = this.game.moves.length; this.st.conn.send(JSON.stringify({ code: "lastate", - target: this.getOppSid(), //he's connected for sure - gameId: this.gameRef.id, - lastMove: (L>0 ? this.game.moves[L-1] : undefined), - score: this.game.score, - movesCount: L, - drawOffer: this.drawOffer, - clocks: this.game.clocks, + target: player.sid, + state: + { + lastMove: (L>0 ? this.game.moves[L-1] : undefined), + score: this.game.score, + movesCount: L, + drawOffer: this.drawOffer, + clocks: this.game.clocks, + } })); } break; } + case "newmove": + // NOTE: this call to play() will trigger processMove() + this.$refs["basegame"].play(data.move, + "receive", this.game.vname!="Dark" ? "animate" : null); + break; case "lastate": //got opponent infos about last move { const L = this.game.moves.length; - if (this.gameRef.id != data.gameId) - break; //games IDs don't match: nothing we can do... - // OK, opponent still in game (which might be over) if (data.movesCount > L) { // Just got last move from him - this.$refs["basegame"].play(data.lastMove, "receive"); + this.$refs["basegame"].play(data.lastMove, + "receive", this.game.vname!="Dark" ? "animate" : null); if (data.score != "*" && this.game.score == "*") { // Opponent resigned or aborted game, or accepted draw offer // (this is not a stalemate or checkmate) this.$refs["basegame"].endGame(data.score, "Opponent action"); } - this.game.clocks = data.clocks; - this.drawOffer = data.drawOffer; - } - else if (data.movesCount < L) - { - // We must tell last move to opponent - this.st.conn.send(JSON.stringify({ - code: "lastate", - target: this.getOppSid(), //we know he is connected - gameId: this.gameRef.id, - lastMove: (L>0 ? this.game.moves[L-1] : undefined), - score: this.game.score, - movesCount: L, - drawOffer: this.drawOffer, - clocks: this.game.clocks, - })); + this.game.clocks = data.clocks; //TODO: check this? + this.drawOffer = data.drawOffer; //does opponent offer draw? } break; } @@ -274,7 +250,6 @@ export default { // ==> on "newmove", check "drawOffer" field case "connect": { - // TODO: if opponent connect, trigger lastate chain of events... this.people.push({name:"", id:0, sid:data.sid}); this.st.conn.send(JSON.stringify({code:"askidentity", target:data.sid})); break; @@ -357,21 +332,19 @@ export default { // - from remote peer (one live game I don't play, finished or not) loadGame: function(game) { const afterRetrieval = async (game) => { - // NOTE: variants array might not be available yet, thus the two next lines - const variantCell = this.st.variants.filter(v => v.id == game.vid); - const vname = (variantCell.length > 0 ? variantCell[0].name : ""); - if (!game.fen) - game.fen = game.fenStart; //game wasn't started + const vModule = await import("@/variants/" + game.vname + ".js"); + window.V = vModule.VariantRules; + this.vr = new V(game.fen); const gtype = (game.timeControl.indexOf('d') >= 0 ? "corr" : "live"); - // TODO: this is not really beautiful (uid on corr players...) - if (gtype == "corr" && game.players[0].color == "b") - [ game.players[0], game.players[1] ] = [ game.players[1], game.players[0] ]; - const myIdx = game.players.findIndex(p => { - return p.sid == this.st.user.sid || p.uid == this.st.user.id; - }); const tc = extractTime(game.timeControl); if (gtype == "corr") { + if (game.players[0].color == "b") + { + // Adopt the same convention for live and corr games: [0] = white + [ game.players[0], game.players[1] ] = + [ game.players[1], game.players[0] ]; + } // corr game: needs to compute the clocks + initime game.clocks = [tc.mainTime, tc.mainTime]; game.initime = [0, 0]; @@ -390,6 +363,9 @@ export default { if (L >= 1) game.initime[L%2] = game.moves[L-1].played; } + const myIdx = game.players.findIndex(p => { + return p.sid == this.st.user.sid || p.uid == this.st.user.id; + }); if (gtype == "live" && game.clocks[0] < 0) //game unstarted { game.clocks = [tc.mainTime, tc.mainTime]; @@ -404,16 +380,12 @@ export default { }); } } - const vModule = await import("@/variants/" + vname + ".js"); - window.V = vModule.VariantRules; - this.vr = new V(game.fen); this.game = Object.assign({}, game, // NOTE: assign mycolor here, since BaseGame could also be VS computer { type: gtype, increment: tc.increment, - vname: vname, mycolor: [undefined,"w","b"][myIdx+1], // opponent sid not strictly required (or available), but easier // at least oppsid or oppid is available anyway: @@ -421,23 +393,20 @@ export default { oppid: (myIdx < 0 ? undefined : game.players[1-myIdx].uid), } ); - if (!!this.game.oppid) - { - // Send ping to server (answer pong if players[s] are connected) - this.st.conn.send(JSON.stringify({code:"ping", target:this.game.oppid})); - } }; if (!!game) return afterRetrival(game); if (!!this.gameRef.rid) { + // Remote live game this.st.conn.send(JSON.stringify( {code:"askfullgame", target:this.gameRef.rid})); // (send moves updates + resign/abort/draw actions) } else { - GameStorage.get(this.gameRef.id, async (game) => { + // Local or corr game + GameStorage.get(this.gameRef.id, (game) => { afterRetrieval(game); }); } @@ -477,8 +446,15 @@ export default { const nextIdx = ["w","b"].indexOf(this.vr.turn); GameStorage.update(this.gameRef.id, { - move: filtered_move, fen: move.fen, + move: + { + squares: filtered_move, + message: this.corrMsg, //TODO + played: Date.now(), //TODO: on server? + idx: this.game.moves.length - 1, + color: move.color, + }, clocks: this.game.clocks.map((t,i) => i==colorIdx ? this.game.clocks[i] + addTime : this.game.clocks[i]), diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index a8dfa987..1ad88b4e 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -110,7 +110,7 @@ export default { }); this.games.forEach(g => { if (g.vname == "") - g.vname = this.getVname(g.vid) + g.vname = this.getVname(g.vid); }); }, }, @@ -224,10 +224,10 @@ export default { } this.$router.push(url); }, - // TODO: ...filter(...)[0].name, one-line, just remove this function getVname: function(vid) { - const vIdx = this.st.variants.findIndex(v => v.id == vid); - return vIdx >= 0 ? this.st.variants[vIdx].name : ""; + const variant = this.st.variants.find(v => v.id == vid); + // this.st.variants might be uninitialized (variant == null) + return (!!variant ? variant.name : ""); }, getSid: function(pname) { const pIdx = this.people.findIndex(pl => pl.name == pname); @@ -452,7 +452,8 @@ export default { chall.vname = vname; chall.from = this.people[0]; //avoid sending email this.challenges.push(chall); - localStorage.setItem("challenge", JSON.stringify(chall)); + if (ctype == "live") + localStorage.setItem("challenge", JSON.stringify(chall)); document.getElementById("modalNewgame").checked = false; }; const cIdx = this.challenges.findIndex( @@ -515,7 +516,6 @@ export default { } else //my challenge { - localStorage.removeItem("challenge"); if (c.type == "corr") { ajax( @@ -524,6 +524,8 @@ export default { {id: c.id} ); } + else //live + localStorage.removeItem("challenge"); } // In (almost) all cases, the challenge is consumed: ArrayFun.remove(this.challenges, ch => ch.id == c.id); @@ -541,6 +543,7 @@ export default { 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, }; let target = c.from.sid; //may not be defined if corr + offline opp diff --git a/server/db/create.sql b/server/db/create.sql index 3cfcbae3..0251c176 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -65,7 +65,7 @@ create table Players ( create table Moves ( gid integer, - move varchar, + squares varchar, --description, appear/vanish/from/to message varchar, played datetime, --when was this move played? idx integer, --index of the move in the game diff --git a/server/models/Game.js b/server/models/Game.js index 72ee0dcb..ced56112 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -13,11 +13,10 @@ var db = require("../utils/database"); * gid: ref game id * uid: ref user id * color: character - * rtime: real (remaining time) * * Structure table Moves: * gid: ref game id - * move: varchar (description) + * squares: varchar (description) * message: text * played: datetime * idx: integer @@ -30,8 +29,8 @@ const GameModel = { db.serialize(function() { let query = - "INSERT INTO Games (vid, fenStart, score, timeControl) " + - "VALUES (" + vid + ",'" + fen + "','*','" + timeControl + "')"; + "INSERT INTO Games (vid, fenStart, fen, score, timeControl) VALUES " + + "(" + vid + ",'" + fen + "','" + fen + "','*','" + timeControl + "')"; db.run(query, function(err) { if (!!err) return cb(err); @@ -39,8 +38,7 @@ const GameModel = const color = (idx==0 ? "w" : "b"); query = "INSERT INTO Players VALUES " + - // Remaining time = -1 means "unstarted" - "(" + this.lastID + "," + p.id + ",'" + color + "', -1)"; + "(" + this.lastID + "," + p.id + ",'" + color + "')"; db.run(query); }); cb(null, {gid: this.lastID}); @@ -52,22 +50,28 @@ const GameModel = getOne: function(id, cb) { db.serialize(function() { + // TODO: optimize queries? let query = - "SELECT * " + - "FROM Games " + - "WHERE id = " + id; + "SELECT g.id, g.vid, g.fen, g.fenStart, g.timeControl, g.score, " + + "v.name AS vname " + + "FROM Games g " + + "JOIN Variants v " + + " ON g.vid = v.id " + + "WHERE g.id = " + id; db.get(query, (err,gameInfo) => { if (!!err) return cb(err); query = - "SELECT uid, color, rtime " + - "FROM Players " + - "WHERE gid = " + id; + "SELECT p.uid, p.color, u.name " + + "FROM Players p " + + "JOIN Users u " + + " ON p.uid = u.id " + + "WHERE p.gid = " + id; db.all(query, (err2,players) => { if (!!err2) return cb(err2); query = - "SELECT move, message, played, idx, color " + + "SELECT squares, message, played, idx, color " + "FROM Moves " + "WHERE gid = " + id; db.all(query, (err3,moves) => { @@ -126,26 +130,35 @@ const GameModel = }, // obj can have fields move, fen and/or score - update: function(id, obj, cb) + update: function(id, obj) { - db.serialize(function() { + + + +console.log(id); + console.log(obj); + + + db.parallelize(function() { let query = "UPDATE Games " + "SET "; - if (!!obj.move) - { - move.played = Date.now(); - query += "move = " + obj.move + ","; //TODO: already stringified?! - } if (!!obj.fen) query += "fen = " + obj.fen + ","; if (!!obj.score) query += "score = " + obj.score + ","; query = query.slice(0,-1); //remove last comma query += " WHERE gameId = " + id; - db.run(query, (err) => { - cb(err); - }); + db.run(query); + if (!!obj.move) + { + const m =obj.move; + query = + "INSERT INTO Moves (gid,squares,message,played,idx,color) VALUES " + + "(" + id + ",'" + JSON.stringify(m.squares) + "','" + m.message + + "'" + m.played + "," + m.idx + ",'" + m.color + "')"; + db.run(query); + } }); }, diff --git a/server/routes/games.js b/server/routes/games.js index 3b91e8b7..128d9eeb 100644 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -54,8 +54,7 @@ router.get("/games", access.ajax, (req,res) => { // New move + fen update + score, potentially // TODO: if newmove fail, takeback in GUI router.put("/games", access.logged, access.ajax, (req,res) => { - const gid = req.body.gid; - const oppId = req.body.oppId; + const gid = req.body.gid; const obj = req.body.newObj; GameModel.update(gid, obj, (err) => { if (!!err) diff --git a/server/sockets.js b/server/sockets.js index fe82f6e3..3637c8f4 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -35,9 +35,10 @@ module.exports = function(wss) { switch (obj.code) { case "pollclients": + const curPage = clients[sid].page; socket.send(JSON.stringify({code:"pollclients", sockIds: Object.keys(clients).filter(k => - k != sid && clients[k].page == obj.page)})); + k != sid && clients[k].page == curPage)})); break; case "pagechange": notifyRoom(clients[sid].page, "disconnect"); @@ -45,28 +46,28 @@ module.exports = function(wss) { notifyRoom(obj.page, "connect"); break; case "askidentity": - clients[obj.target].sock.send( - JSON.stringify({code:"askidentity",from:sid})); + clients[obj.target].sock.send(JSON.stringify( + {code:"askidentity",from:sid})); break; case "askchallenge": - clients[obj.target].sock.send( - JSON.stringify({code:"askchallenge",from:sid})); + clients[obj.target].sock.send(JSON.stringify( + {code:"askchallenge",from:sid})); break; case "askgame": - clients[obj.target].sock.send( - JSON.stringify({code:"askgame",from:sid})); + clients[obj.target].sock.send(JSON.stringify( + {code:"askgame",from:sid})); break; case "identity": - clients[obj.target].sock.send( - JSON.stringify({code:"identity",user:obj.user})); + clients[obj.target].sock.send(JSON.stringify( + {code:"identity",user:obj.user})); break; case "refusechallenge": - clients[obj.target].sock.send( - JSON.stringify({code:"refusechallenge", cid:obj.cid, from:sid})); + clients[obj.target].sock.send(JSON.stringify( + {code:"refusechallenge", cid:obj.cid, from:sid})); break; case "deletechallenge": - clients[obj.target].sock.send( - JSON.stringify({code:"deletechallenge", cid:obj.cid, from:sid})); + clients[obj.target].sock.send(JSON.stringify( + {code:"deletechallenge", cid:obj.cid, from:sid})); break; case "newgame": clients[obj.target].sock.send(JSON.stringify( @@ -81,32 +82,33 @@ module.exports = function(wss) { {code:"game", game:obj.game, from:sid})); break; case "newchat": - clients[obj.target].sock.send(JSON.stringify({code:"newchat",msg:obj.msg})); + clients[obj.target].sock.send(JSON.stringify( + {code:"newchat",msg:obj.msg})); break; // TODO: WebRTC instead in this case (most demanding?) case "newmove": - clients[obj.target].sock.send(JSON.stringify({code:"newmove",move:obj.move})); - break; - case "ping": - // If this code is reached, then obj.target is connected - socket.send(JSON.stringify({code:"pong"})); + clients[obj.target].sock.send(JSON.stringify( + {code:"newmove",move:obj.move})); break; case "lastate": - const oppId = obj.target; - obj.oppid = sid; //I'm the opponent of my opponent - clients[oppId].sock.send(JSON.stringify(obj)); + clients[obj.target].sock.send(JSON.stringify( + {code:"lastate", state:obj.state})); break; case "resign": - clients[obj.target].sock.send(JSON.stringify({code:"resign"})); + clients[obj.target].sock.send(JSON.stringify( + {code:"resign"})); break; case "abort": - clients[obj.target].sock.send(JSON.stringify({code:"abort",msg:obj.msg})); + clients[obj.target].sock.send(JSON.stringify( + {code:"abort",msg:obj.msg})); break; case "drawoffer": - clients[obj.target].sock.send(JSON.stringify({code:"drawoffer"})); + clients[obj.target].sock.send(JSON.stringify( + {code:"drawoffer"})); break; case "draw": - clients[obj.target].sock.send(JSON.stringify({code:"draw"})); + clients[obj.target].sock.send(JSON.stringify( + {code:"draw"})); break; } }); -- 2.44.0