From: Benjamin Auder Date: Mon, 16 Mar 2020 18:52:22 +0000 (+0100) Subject: Refactor Games structure on server: no longer use an extra 'Players' table X-Git-Url: https://git.auder.net/?p=vchess.git;a=commitdiff_plain;h=f14572c4a22425033735253eabbaa2d8dbb53d05 Refactor Games structure on server: no longer use an extra 'Players' table --- diff --git a/client/public/images/pieces/SOURCE b/client/public/images/pieces/SOURCE index 26cb11c2..ec69ae2e 100644 --- a/client/public/images/pieces/SOURCE +++ b/client/public/images/pieces/SOURCE @@ -2,3 +2,4 @@ SVG standard images found on chesstempo website: https://www4.chesstempo.com/images/pieces/svg/merida/whitepawn.vers1.svg (...) + Adaptation for checkered pieces Some fairy pieces found on the web and icon scout: https://iconscout.com/ +PNG images for Eightpieces from https://greenchess.net/index.php and Jeff Kubach design. diff --git a/client/src/App.vue b/client/src/App.vue index 6a998080..6590fcfb 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -274,7 +274,9 @@ footer @media screen and (max-width: 420px) footer + height: 55px display: block + padding: 5px 0 .menuitem.somenews color: red diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 471c139c..612ded48 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -309,6 +309,7 @@ export default { const playSubmove = (smove) => { if (!navigate) smove.notation = this.vr.getNotation(smove); this.vr.play(smove); + this.lastMove = smove; if (!navigate) { if (!this.inMultimove) { if (this.cursor < this.moves.length - 1) @@ -356,7 +357,6 @@ export default { smove.fen = this.vr.getFen(); // Is opponent in check? this.incheck = this.vr.getCheckSquares(this.vr.turn); - this.lastMove = smove; this.emitFenIfAnalyze(); this.inMultimove = false; if (!noemit) { diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue index 549e2a1d..a83eb85a 100644 --- a/client/src/components/GameList.vue +++ b/client/src/components/GameList.vue @@ -85,7 +85,9 @@ export default { const deltaCreated = maxCreated - minCreated; return remGames.sort((g1, g2) => { return ( - g2.priority - g1.priority + (g2.created - g1.created) / deltaCreated + g2.priority - g1.priority + + // Modulate with creation time (value in ]0,1[) + (g2.created - g1.created) / (deltaCreated + 1) ); }); }, diff --git a/client/src/translations/en.js b/client/src/translations/en.js index a37a75c1..5a14d3f7 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -109,7 +109,7 @@ export const translations = { Register: "Register", "Registration complete! Please check your emails now": "Registration complete! Please check your emails now", Rematch: "Rematch", - "Rematch in progress:": "Rematch in progress", + "Rematch in progress": "Rematch in progress", "Remove game?": "Remove game?", Resign: "Resign", "Resign the game?": "Resign the game?", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index e1048b7d..34444758 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -109,7 +109,7 @@ export const translations = { Register: "Registrarse", "Registration complete! Please check your emails now": "¡Registro completo! Revise sus correos electrónicos ahora", Rematch: "Revancha", - "Rematch in progress:": "Revancha en progreso:", + "Rematch in progress": "Revancha en progreso", "Remove game?": "¿Eliminar la partida?", Resign: "Abandonar", "Resign the game?": "¿Abandonar la partida?", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 20b7b587..8c1f7ae1 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -109,7 +109,7 @@ export const translations = { Register: "S'enregistrer", "Registration complete! Please check your emails now": "Enregistrement terminé ! Allez voir vos emails maintenant", Rematch: "Rejouer", - "Rematch in progress:": "Revanche en cours :", + "Rematch in progress": "Revanche en cours", "Remove game?": "Supprimer la partie ?", Resign: "Abandonner", "Resign the game?": "Abandonner la partie ?", diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js index f7d93d84..76f72b0f 100644 --- a/client/src/variants/Eightpieces.js +++ b/client/src/variants/Eightpieces.js @@ -933,7 +933,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return false; } let sq = [ x1 + step[0], y1 + step[1] ]; - while (sq[0] != x2 && sq[1] != y2) { + while (sq[0] != x2 || sq[1] != y2) { if ( // NOTE: no need to check OnBoard in this special case (!lancer && this.board[sq[0]][sq[1]] != V.EMPTY) || @@ -1080,9 +1080,24 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return (!choice.second ? choice : [choice, choice.second]); } + // For moves notation: + static get LANCER_DIRNAMES() { + return { + 'c': "N", + 'd': "NE", + 'e': "E", + 'f': "SE", + 'g': "S", + 'h': "SW", + 'm': "W", + 'o': "NW" + }; + } + getNotation(move) { // Special case "king takes jailer" is a pass move if (move.appear.length == 0 && move.vanish.length == 0) return "pass"; + let notation = undefined; if (this.subTurn == 2) { // Do not consider appear[1] (sentry) for sentry pushes const simpleMove = { @@ -1091,8 +1106,11 @@ export const VariantRules = class EightpiecesRules extends ChessRules { start: move.start, end: move.end }; - return super.getNotation(simpleMove); - } - return super.getNotation(move); + notation = super.getNotation(simpleMove); + } else notation = super.getNotation(move); + if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p)) + // Lancer: add direction info + notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p]; + return notation; } }; diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index a401e464..0182352a 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -7,13 +7,11 @@ main ) .card.text-center label.modal-close(for="modalInfo") - p - span {{ st.tr["Rematch in progress:"] }} - a( - :href="'#/game/' + rematchId" - onClick="document.getElementById('modalInfo').checked=false" - ) - | {{ "#/game/" + rematchId }} + a( + :href="'#/game/' + rematchId" + onClick="document.getElementById('modalInfo').checked=false" + ) + | {{ st.tr["Rematch in progress"] }} input#modalChat.modal( type="checkbox" @click="resetChatColor()" @@ -365,7 +363,7 @@ export default { ) || ( - player.id && + !!player.id && Object.values(this.people).some(p => p.id == player.id && p.focus) ) @@ -849,8 +847,11 @@ export default { cadence: this.game.cadence }; const notifyNewGame = () => { - let oppsid = this.getOppsid(); //may be null + const oppsid = this.getOppsid(); //may be null this.send("rnewgame", { data: gameInfo, oppsid: oppsid }); + // To main Hall if corr game: + if (this.game.type == "corr") + this.send("newgame", { data: gameInfo }); // Also to MyGames page: this.notifyMyGames("newgame", gameInfo); }; @@ -923,20 +924,13 @@ export default { const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers if (!game.chats) game.chats = []; //live games don't have chat history 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] - ]; - } // 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.initime = [0, 0]; + game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; if (L >= 1) game.initime[L % 2] = game.moves[L-1].played; // NOTE: game.clocks shouldn't be computed right now: // job will be done in re_setClocks() called soon below. @@ -966,17 +960,15 @@ export default { game.moves = game.moves.map(m => m.squares); } if (gtype == "live" && game.clocks[0] < 0) { - // Game is unstarted + // Game is unstarted. clocks and initime are ignored until move 2 game.clocks = [tc.mainTime, tc.mainTime]; - if (game.score == "*") { - game.initime[0] = Date.now(); - if (myIdx >= 0) { - // I play in this live game; corr games don't have clocks+initime - GameStorage.update(game.id, { - clocks: game.clocks, - initime: game.initime - }); - } + game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; + if (myIdx >= 0) { + // I play in this live game + GameStorage.update(game.id, { + clocks: game.clocks, + initime: game.initime + }); } } // TODO: merge next 2 "if" conditions @@ -1085,9 +1077,6 @@ export default { g.moves.forEach(m => { m.squares = JSON.parse(m.squares); }); - g.players = [{ id: g.white }, { id: g.black }]; - delete g["white"]; - delete g["black"]; afterRetrieval(g); } } diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index b9262d12..37558a93 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -338,14 +338,14 @@ export default { cursor: this.cursor }, success: (response) => { - if ( - response.games.length > 0 && - this.games.length == 0 && - this.gdisplay == "live" - ) { - document - .getElementById("btnGcorr") - .classList.add("somethingnew"); + const L = response.games.length; + if (L > 0) { + this.cursor = response.games[L - 1].created; + if (this.games.length == 0 && this.gdisplay == "live") { + document + .getElementById("btnGcorr") + .classList.add("somethingnew"); + } } this.games = this.games.concat( response.games.map(g => { @@ -605,8 +605,9 @@ export default { if (!s.page) // Peer is in Hall this.send("askchallenges", { target: s.sid }); - // Peer is in Game - else this.send("askgame", { target: s.sid, page: page }); + // Peer is in Game: ask only if live game + else if (!page.match(/\/[0-9]+$/)) + this.send("askgame", { target: s.sid, page: page }); }); break; } @@ -618,7 +619,9 @@ export default { this.people[data.from] = { pages: [{ path: page, focus: true }] }; if (data.code == "connect") this.send("askchallenges", { target: data.from }); - else this.send("askgame", { target: data.from, page: page }); + // Ask game only if live: + else if (!page.match(/\/[0-9]+$/)) + this.send("askgame", { target: data.from, page: page }); } else { // Append page if not already in list if (!(this.people[data.from].pages.find(p => p.path == page))) @@ -772,8 +775,8 @@ export default { } break; } - case "game": { - // Individual request + case "game": // Individual request + case "newgame": { const game = data.data; // Ignore games where I play (will go in MyGames page) if (game.players.every(p => @@ -1015,7 +1018,6 @@ export default { }); // Add new challenge: chall.from = { - // Decompose to avoid revealing email sid: this.st.user.sid, id: this.st.user.id, name: this.st.user.name @@ -1049,7 +1051,7 @@ export default { { data: { chall: chall }, success: (response) => { - finishAddChallenge(response.cid); + finishAddChallenge(response.id); } } ); @@ -1065,7 +1067,6 @@ export default { finishProcessingChallenge: function(c) { if (c.accepted) { c.seat = { - // Again, avoid c.seat = st.user to not reveal email sid: this.st.user.sid, id: this.st.user.id, name: this.st.user.name @@ -1135,7 +1136,8 @@ export default { }, // NOTE: when launching game, the challenge is already being deleted launchGame: function(c) { - let players = + // White player index 0, black player index 1: + const players = !!c.mycolor ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat]) : shuffle([c.from, c.seat]); @@ -1144,10 +1146,7 @@ export default { id: getRandString(), fen: c.fen || V.GenRandInitFen(c.randomness), randomness: c.randomness, //for rematch - // White player index 0, black player index 1: - players: c.mycolor - ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat]) - : shuffle([c.from, c.seat]), + players: players, vid: c.vid, cadence: c.cadence }; @@ -1156,14 +1155,22 @@ export default { if (!!oppsid) // Opponent is online this.send("startgame", { data: gameInfo, target: oppsid }); + // If new corr game, notify Hall (except opponent and me) + if (c.type == "corr") { + this.send( + "newgame", + { + data: gameInfo, + excluded: [this.st.user.sid, oppsid] + } + ); + } // Notify MyGames page: this.send( "notifynewgame", { data: gameInfo, - targets: gameInfo.players.map(p => { - return { sid: p.sid, id: p.id }; - }) + targets: gameInfo.players } ); // NOTE: no need to send the game to the room, since I'll connect @@ -1179,11 +1186,14 @@ export default { "POST", { // cid is useful to delete the challenge: - data: { gameInfo: gameInfo, cid: c.id }, + data: { + gameInfo: gameInfo, + cid: c.id + }, success: (response) => { - gameInfo.id = response.gameId; + gameInfo.id = response.id; notifyNewgame(); - this.$router.push("/game/" + response.gameId); + this.$router.push("/game/" + response.id); } } ); @@ -1328,8 +1338,8 @@ tr > td margin: 5px 0 button#loadMoreBtn - margin-top: 0 - margin-bottom: 0 + display: block + margin: 0 auto td.remove-preset background-color: lightgrey diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index 628d72eb..1f1da59e 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -103,16 +103,21 @@ export default { "/runninggames", "GET", { + credentials: true, success: (res) => { // These games are garanteed to not be deleted this.corrGames = res.games; - this.corrGames.forEach(g => g.type = "corr"); + this.corrGames.forEach(g => { + g.type = "corr"; + g.score = "*"; + }); this.decorate(this.corrGames); // Now ask completed games (partial list) ajax( "/completedgames", "GET", { + credentials: true, data: { cursor: this.cursor }, success: (res2) => { if (res2.games.length > 0) { @@ -191,7 +196,7 @@ export default { if (thing == "turn") { game.myTurn = !game.myTurn; if (game.myTurn) this.tryShowNewsIndicator(type); - } + } else game.myTurn = false; // TODO: forcing refresh like that is ugly and wrong. // How to do it cleanly? this.$refs[type + "games"].$forceUpdate(); @@ -287,6 +292,7 @@ export default { "/completedgames", "GET", { + credentials: true, data: { cursor: this.cursor }, success: (res) => { if (res.games.length > 0) { @@ -316,8 +322,8 @@ table.game-list max-height: 100% button#loadMoreBtn - margin-top: 0 - margin-bottom: 0 + display: block + margin: 0 auto .somethingnew background-color: #c5fefe !important diff --git a/server/models/Game.js b/server/models/Game.js index 65aedfde..080cab3c 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -38,10 +38,10 @@ const GameModel = return ( g.vid.toString().match(/^[0-9]+$/) && g.cadence.match(/^[0-9dhms +]+$/) && - g.randomness.match(/^[0-2]$/) && + g.randomness.toString().match(/^[0-2]$/) && g.fen.match(/^[a-zA-Z0-9, /-]*$/) && g.players.length == 2 && - g.players.every(p => p.toString().match(/^[0-9]+$/)) + g.players.every(p => p.id.toString().match(/^[0-9]+$/)) ); }, @@ -57,11 +57,11 @@ const GameModel = "VALUES " + "(" + vid + ",'" + fen + "','" + fen + "'," + randomness + "," + - "'" + players[0] + "','" + players[1] + "," + + players[0].id + "," + players[1].id + "," + "'" + cadence + "'," + Date.now() + ")"; db.run(query, function(err) { - cb(err, { id: this.lastId }); + cb(err, { id: this.lastID }); }); }); }, @@ -72,7 +72,7 @@ const GameModel = db.serialize(function() { let query = "SELECT " + - "g.id, g.vid, g.fen, g.fenStart, g.cadence, g.created, " + + "g.id, g.fen, g.fenStart, g.cadence, g.created, " + "g.white, g.black, g.score, g.scoreMsg, " + "g.drawOffer, g.rematchOffer, v.name AS vname " + "FROM Games g " + @@ -80,25 +80,40 @@ const GameModel = " ON g.vid = v.id " + "WHERE g.id = " + id; db.get(query, (err, gameInfo) => { + if (!gameInfo) { + cb(err || { errmsg: "Game not found" }, undefined); + return; + } query = - "SELECT squares, played, idx " + - "FROM Moves " + - "WHERE gid = " + id; - db.all(query, (err3, moves) => { + "SELECT id, name " + + "FROM Users " + + "WHERE id IN (" + gameInfo.white + "," + gameInfo.black + ")"; + db.all(query, (err2, players) => { + if (players[0].id == gameInfo.black) players = players.reverse(); + // The original players' IDs info isn't required anymore + delete gameInfo["white"]; + delete gameInfo["black"]; query = - "SELECT msg, name, added " + - "FROM Chats " + + "SELECT squares, played, idx " + + "FROM Moves " + "WHERE gid = " + id; - db.all(query, (err4, chats) => { - const game = Object.assign( - {}, - gameInfo, - { - moves: moves, - chats: chats - } - ); - cb(null, game); + db.all(query, (err3, moves) => { + query = + "SELECT msg, name, added " + + "FROM Chats " + + "WHERE gid = " + id; + db.all(query, (err4, chats) => { + const game = Object.assign( + {}, + gameInfo, + { + players: players, + moves: moves, + chats: chats + } + ); + cb(null, game); + }); }); }); }); @@ -109,9 +124,8 @@ const GameModel = getObserved: function(uid, cursor, cb) { db.serialize(function() { let query = - "SELECT g.id, g.vid, g.cadence, g.created, " + - " g.score, g.white, g.black " + - "FROM Games g "; + "SELECT id, vid, cadence, created, score, white, black " + + "FROM Games "; if (uid > 0) query += "WHERE " + " created < " + cursor + " AND " + @@ -131,16 +145,19 @@ const GameModel = let names = {}; users.forEach(u => { names[u.id] = u.name; }); cb( + err, games.map( g => { return { id: g.id, + vid: g.vid, cadence: g.cadence, - vname: g.vname, created: g.created, score: g.score, - white: names[g.white], - black: names[g.black] + players: [ + { id: g.white, name: names[g.white] }, + { id: g.black, name: names[g.black] } + ] }; } ) @@ -154,12 +171,12 @@ const GameModel = getRunning: function(uid, cb) { db.serialize(function() { let query = - "SELECT g.id, g.cadence, g.created, g.score, " + + "SELECT g.id, g.cadence, g.created, " + "g.white, g.black, v.name AS vname " + "FROM Games g " + "JOIN Variants v " + " ON g.vid = v.id " + - "WHERE white = " + uid + " OR black = " + uid; + "WHERE score = '*' AND (white = " + uid + " OR black = " + uid + ")"; db.all(query, (err, games) => { // Get movesCount (could be done in // with next query) query = @@ -176,21 +193,24 @@ const GameModel = if (!pids[g.white]) pids[g.white] = true; if (!pids[g.black]) pids[g.black] = true; }); - UserModel.getByIds(pids, (err2, users) => { + UserModel.getByIds(Object.keys(pids), (err2, users) => { let names = {}; users.forEach(u => { names[u.id] = u.name; }); cb( + null, games.map( g => { return { id: g.id, - cadence: g.cadence, vname: g.vname, + cadence: g.cadence, created: g.created, score: g.score, - movesCount: movesCounts[g.id], - white: names[g.white], - black: names[g.black] + movesCount: movesCounts[g.id] || 0, + players: [ + { id: g.white, name: names[g.white] }, + { id: g.black, name: names[g.black] } + ] }; } ) @@ -212,6 +232,7 @@ const GameModel = "JOIN Variants v " + " ON g.vid = v.id " + "WHERE " + + " score <> '*' AND " + " created < " + cursor + " AND " + " (" + " (" + uid + " = white AND NOT deletedByWhite) OR " + @@ -227,21 +248,24 @@ const GameModel = if (!pids[g.white]) pids[g.white] = true; if (!pids[g.black]) pids[g.black] = true; }); - UserModel.getByIds(pids, (err2, users) => { + UserModel.getByIds(Object.keys(pids), (err2, users) => { let names = {}; users.forEach(u => { names[u.id] = u.name; }); cb( + null, games.map( g => { return { id: g.id, - cadence: g.cadence, vname: g.vname, + cadence: g.cadence, created: g.created, score: g.score, scoreMsg: g.scoreMsg, - white: names[g.white], - black: names[g.black], + players: [ + { id: g.white, name: names[g.white] }, + { id: g.black, name: names[g.black] } + ], deletedByWhite: g.deletedByWhite, deletedByBlack: g.deletedByBlack }; @@ -258,8 +282,8 @@ const GameModel = const query = "SELECT white, black " + "FROM Games " + - "WHERE gid = " + id; - db.all(query, (err, players) => { + "WHERE id = " + id; + db.get(query, (err, players) => { return cb(err, players); }); }); @@ -292,71 +316,83 @@ const GameModel = // obj can have fields move, chat, fen, drawOffer and/or score + message update: function(id, obj, cb) { db.parallelize(function() { - let query = + let updateQuery = "UPDATE Games " + "SET "; let modifs = ""; // NOTE: if drawOffer is set, we should check that it's player's turn // A bit overcomplicated. Let's trust the client on that for now... - if (!!obj.drawOffer) - { - if (obj.drawOffer == "n") //special "None" update + if (!!obj.drawOffer) { + if (obj.drawOffer == "n") + // Special "None" update obj.drawOffer = ""; modifs += "drawOffer = '" + obj.drawOffer + "',"; } - if (!!obj.rematchOffer) - { - if (obj.rematchOffer == "n") //special "None" update + if (!!obj.rematchOffer) { + if (obj.rematchOffer == "n") + // Special "None" update obj.rematchOffer = ""; modifs += "rematchOffer = '" + obj.rematchOffer + "',"; } - if (!!obj.fen) - modifs += "fen = '" + obj.fen + "',"; - if (!!obj.score) - modifs += "score = '" + obj.score + "',"; - if (!!obj.scoreMsg) - modifs += "scoreMsg = '" + obj.scoreMsg + "',"; + if (!!obj.fen) modifs += "fen = '" + obj.fen + "',"; if (!!obj.deletedBy) { const myColor = obj.deletedBy == 'w' ? "White" : "Black"; modifs += "deletedBy" + myColor + " = true,"; } - modifs = modifs.slice(0,-1); //remove last comma - if (modifs.length > 0) - { - query += modifs + " WHERE id = " + id; - db.run(query); + if (!!obj.score) { + modifs += "score = '" + obj.score + "'," + + "scoreMsg = '" + obj.scoreMsg + "',"; } - // NOTE: move, chat and delchat are mutually exclusive - if (!!obj.move) - { - // Security: only update moves if index is right - query = - "SELECT MAX(idx) AS maxIdx " + + const finishAndSendQuery = () => { + modifs = modifs.slice(0, -1); //remove last comma + if (modifs.length > 0) { + updateQuery += modifs + " WHERE id = " + id; + db.run(updateQuery); + } + cb(null); + }; + if (!!obj.move || (!!obj.score && obj.scoreMsg == "Time")) { + // Security: only update moves if index is right, + // and score with scoreMsg "Time" if really lost on time. + let query = + "SELECT MAX(idx) AS maxIdx, MAX(played) AS lastPlayed " + "FROM Moves " + "WHERE gid = " + id; - db.get(query, (err,ret) => { - const m = obj.move; - if (!ret.maxIdx || ret.maxIdx + 1 == m.idx) { - query = - "INSERT INTO Moves (gid, squares, played, idx) VALUES " + - "(" + id + ",?," + m.played + "," + m.idx + ")"; - db.run(query, JSON.stringify(m.squares)); - cb(null); + db.get(query, (err, ret) => { + if (!!obj.move ) { + if (!ret.maxIdx || ret.maxIdx + 1 == obj.move.idx) { + query = + "INSERT INTO Moves (gid, squares, played, idx) VALUES " + + "(" + id + ",?," + Date.now() + "," + obj.move.idx + ")"; + db.run(query, JSON.stringify(obj.move.squares)); + finishAndSendQuery(); + } else cb({ errmsg: "Wrong move index" }); + } else { + if (ret.maxIdx < 2) cb({ errmsg: "Time not over" }); + else { + // We also need the game cadence + query = + "SELECT cadence " + + "FROM Games " + + "WHERE id = " + id; + db.get(query, (err2, ret2) => { + const daysTc = parseInt(ret2.cadence.match(/\(^[0-9]+\)/)[0]); + if (Date.now() - ret.lastPlayed > daysTc * 24 * 3600 * 1000) + finishAndSendQuery(); + else cb({ errmsg: "Time not over" }); + }); + } } - else cb({errmsg:"Wrong move index"}); }); - } - else cb(null); - if (!!obj.chat) - { - query = + } else finishAndSendQuery(); + // NOTE: chat and delchat are mutually exclusive + if (!!obj.chat) { + const query = "INSERT INTO Chats (gid, msg, name, added) VALUES (" + id + ",?,'" + obj.chat.name + "'," + Date.now() + ")"; db.run(query, obj.chat.msg); - } - else if (obj.delchat) - { - query = + } else if (obj.delchat) { + const query = "DELETE " + "FROM Chats " + "WHERE gid = " + id; @@ -368,7 +404,7 @@ const GameModel = "deletedBy" + (obj.deletedBy == 'w' ? "Black" : "White") + " AS deletedByOpp"; - query = + const query = "SELECT " + selection + " " + "FROM Games " + "WHERE id = " + id; diff --git a/server/routes/challenges.js b/server/routes/challenges.js index d27f81ee..4c2d5b40 100644 --- a/server/routes/challenges.js +++ b/server/routes/challenges.js @@ -29,7 +29,7 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => { if (user.notify) UserModel.notify( user, - "New challenge: " + params.siteURL + "/#/?disp=corr"); + "New challenge : " + params.siteURL + "/#/?disp=corr"); } }); } else insertChallenge(); diff --git a/server/routes/games.js b/server/routes/games.js index 77877a2e..42c15c99 100644 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -13,12 +13,13 @@ router.post("/games", access.logged, access.ajax, (req,res) => { if ( Array.isArray(gameInfo.players) && gameInfo.players.some(p => p.id == req.userId) && - (!cid || cid.toString().match(/^[0-9]+$/)) && + (!cid || !!cid.toString().match(/^[0-9]+$/)) && GameModel.checkGameInfo(gameInfo) ) { if (!!cid) ChallengeModel.remove(cid); GameModel.create( - gameInfo.vid, gameInfo.fen, gameInfo.cadence, gameInfo.players, + gameInfo.vid, gameInfo.fen, gameInfo.randomness, + gameInfo.cadence, gameInfo.players, (err, ret) => { const oppIdx = (gameInfo.players[0].id == req.userId ? 1 : 0); const oppId = gameInfo.players[oppIdx].id; @@ -35,7 +36,7 @@ router.get("/games", access.ajax, (req,res) => { const gameId = req.query["gid"]; if (!!gameId && gameId.match(/^[0-9]+$/)) { GameModel.getOne(gameId, (err, game) => { - res.json({ game: game }); + res.json(err || { game: game }); }); } }); @@ -45,20 +46,20 @@ router.get("/observedgames", access.ajax, (req,res) => { const userId = req.query["uid"]; const cursor = req.query["cursor"]; if (!!userId.match(/^[0-9]+$/) && !!cursor.match(/^[0-9]+$/)) { - GameModel.getObserved(userId, (err, games) => { + GameModel.getObserved(userId, cursor, (err, games) => { res.json({ games: games }); }); } }); // Get by user ID, for MyGames page -router.get("/runninggames", access.ajax, access.logged, (req,res) => { +router.get("/runninggames", access.logged, access.ajax, (req,res) => { GameModel.getRunning(req.userId, (err, games) => { res.json({ games: games }); }); }); -router.get("/completedgames", access.ajax, access.logged, (req,res) => { +router.get("/completedgames", access.logged, access.ajax, (req,res) => { const cursor = req.query["cursor"]; if (!!cursor.match(/^[0-9]+$/)) { GameModel.getCompleted(req.userId, cursor, (err, games) => { @@ -72,7 +73,7 @@ router.put("/games", access.logged, access.ajax, (req,res) => { const gid = req.body.gid; let obj = req.body.newObj; if (gid.toString().match(/^[0-9]+$/) && GameModel.checkGameUpdate(obj)) { - GameModel.getPlayers(gid, (err,players) => { + GameModel.getPlayers(gid, (err, players) => { let myColor = ''; if (players.white == req.userId) myColor = 'w'; else if (players.black == req.userId) myColor = 'b'; @@ -88,8 +89,8 @@ router.put("/games", access.logged, access.ajax, (req,res) => { const oppid = (myColor == 'w' ? players.black : players.white); const messagePrefix = !!obj.move - ? "New move in game: " - : "Game ended: "; + ? "New move in game : " + : "Game ended : "; UserModel.tryNotify( oppid, messagePrefix + params.siteURL + "/#/game/" + gid diff --git a/server/sockets.js b/server/sockets.js index 7de7abb9..4907965d 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -32,24 +32,13 @@ module.exports = function(wss) { const id = query["id"]; const tmpId = query["tmpId"]; const page = query["page"]; - const notifyRoom = (page,code,obj={}) => { - if (!clients[page]) return; - Object.keys(clients[page]).forEach(k => { - Object.keys(clients[page][k]).forEach(x => { - if (k == sid && x == tmpId) return; - send( - clients[page][k][x].socket, - Object.assign({ code: code, from: sid }, obj) - ); - }); - }); - }; - // For focus events: no need to target self - const notifyAllBut = (page,code,obj={},except) => { + const notifyRoom = (page, code, obj={}, except) => { if (!clients[page]) return; + except = except || []; Object.keys(clients[page]).forEach(k => { if (except.includes(k)) return; Object.keys(clients[page][k]).forEach(x => { + if (k == sid && x == tmpId) return; send( clients[page][k][x].socket, Object.assign({ code: code, from: sid }, obj) @@ -208,14 +197,16 @@ module.exports = function(wss) { case "drawoffer": case "rematchoffer": case "draw": - notifyRoom(page, obj.code, {data: obj.data}); + notifyRoom(page, obj.code, {data: obj.data}, obj.excluded); break; case "rnewgame": // A rematch game started: - // NOTE: no need to explicitely notify Hall: the game will be sent - notifyAllBut(page, "newgame", {data: obj.data}, [sid]); - notifyRoom("/mygames", "newgame", {data: obj.data}); + notifyRoom(page, "newgame", {data: obj.data}); + // Explicitely notify Hall if gametype == corr. + // Live games will be polled from Hall after gconnect event. + if (obj.data.cadence.indexOf('d') >= 0) + notifyRoom("/", "newgame", {data: obj.data}); break; case "newmove": { @@ -284,11 +275,11 @@ module.exports = function(wss) { case "getfocus": case "losefocus": - if (page == "/") notifyAllBut("/", obj.code, { page: "/" }, [sid]); + if (page == "/") notifyRoom("/", obj.code, { page: "/" }, [sid]); else { // Notify game room + Hall: - notifyAllBut(page, obj.code, {}, [sid]); - notifyAllBut("/", obj.code, { page: page }, [sid]); + notifyRoom(page, obj.code, {}, [sid]); + notifyRoom("/", obj.code, { page: page }, [sid]); } break;