From c292ebb2a014646005b01e27253c162f1d639387 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Mon, 9 Mar 2020 10:37:30 +0100 Subject: [PATCH] Draft rematch (not working yet) + fix Crazyhouse getPromotedFen() --- client/src/utils/gameStorage.js | 5 +- client/src/variants/Crazyhouse.js | 8 +- client/src/views/Game.vue | 176 +++++++++++++++++++++++++++--- client/src/views/Hall.vue | 66 ++++++----- server/models/Game.js | 12 +- server/routes/games.js | 5 +- server/sockets.js | 21 +++- 7 files changed, 237 insertions(+), 56 deletions(-) diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index 92db2991..59efb43a 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -43,7 +43,7 @@ export const GameStorage = { // Optional callback to get error status add: function(game, callback) { dbOperation((err,db) => { - if (err) { + if (!!err) { callback("error"); return; } @@ -51,6 +51,9 @@ export const GameStorage = { transaction.oncomplete = function() { callback(); //everything's fine }; + transaction.onerror = function(err) { + callback(err); //duplicate key error (most likely) + }; let objectStore = transaction.objectStore("games"); objectStore.add(game); }); diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js index 984c902f..d677823d 100644 --- a/client/src/variants/Crazyhouse.js +++ b/client/src/variants/Crazyhouse.js @@ -66,11 +66,11 @@ export const VariantRules = class CrazyhouseRules extends ChessRules { let res = ""; for (let i = 0; i < V.size.x; i++) { for (let j = 0; j < V.size.y; j++) { - if (this.promoted[i][j]) res += V.CoordsToSquare({ x: i, y: j }); + if (this.promoted[i][j]) res += V.CoordsToSquare({ x: i, y: j }) + ","; } } + // Remove last comma: if (res.length > 0) res = res.slice(0, -1); - //remove last comma else res = "-"; return res; } @@ -98,8 +98,8 @@ export const VariantRules = class CrazyhouseRules extends ChessRules { this.promoted = ArrayFun.init(V.size.x, V.size.y, false); if (fenParsed.promoted != "-") { for (let square of fenParsed.promoted.split(",")) { - const [x, y] = V.SquareToCoords(square); - this.promoted[x][y] = true; + const coords = V.SquareToCoords(square); + this.promoted[coords.x][coords.y] = true; } } } diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index d920053a..a7cb6a90 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -1,5 +1,13 @@ <template lang="pug"> main + input#modalInfo.modal(type="checkbox") + div#infoDiv( + role="dialog" + data-checkbox="modalInfo" + ) + .card.text-center + label.modal-close(for="modalInfo") + p(v-html="infoMessage") input#modalChat.modal( type="checkbox" @click="resetChatColor()" @@ -82,7 +90,8 @@ main img(src="/images/icons/resign.svg") button.tooltip( v-else-if="!!game.mycolor" - @click="rematch()" + @click="clickRematch()" + :class="{['rematch-' + rematchOffer]: true}" :aria-label="st.tr['Rematch']" ) img(src="/images/icons/rematch.svg") @@ -152,6 +161,7 @@ export default { virtualClocks: [], vr: null, //"variant rules" object initialized from FEN drawOffer: "", + rematchOffer: "", people: {}, //players + observers onMygames: [], //opponents (or me) on "MyGames" page lastate: undefined, //used if opponent send lastate before game is ready @@ -257,6 +267,7 @@ export default { this.virtualClocks = [[0,0], [0,0]]; this.vr = null; this.drawOffer = ""; + this.rematchOffer = ""; this.onMygames = []; this.lastate = undefined; this.newChat = ""; @@ -350,6 +361,17 @@ export default { ) ); }, + getOppsid: function() { + let oppsid = this.game.oppsid; + if (!oppsid) { + oppsid = Object.keys(this.people).find( + sid => this.people[sid].id == this.game.oppid + ); + } + // oppsid is useful only if opponent is online: + if (!!oppsid && !!this.people[oppsid]) return oppsid; + return null; + }, resetChatColor: function() { // TODO: this is called twice, once on opening an once on closing document.getElementById("chatBtn").classList.remove("somethingnew"); @@ -393,10 +415,6 @@ export default { this.$router.push( "/game/" + nextGid + "/?next=" + JSON.stringify(this.nextIds)); }, - rematch: function() { - alert("Unimplemented yet (soon :) )"); - // TODO: same logic as for draw, but re-click remove rematch offer (toggle) - }, askGameAgain: function() { this.gameIsLoading = true; const currentUrl = document.location.href; @@ -560,7 +578,7 @@ export default { .filter(k => [ "id","fen","players","vid","cadence","fenStart","vname", - "moves","clocks","initime","score","drawOffer" + "moves","clocks","initime","score","drawOffer","rematchOffer" ].includes(k)) .reduce( (obj, k) => { @@ -580,7 +598,8 @@ export default { if ( (this.game.moves.length > 0 && this.vr.turn != this.game.mycolor) || this.game.score != "*" || - this.drawOffer == "sent" + this.drawOffer == "sent" || + this.rematchOffer == "sent" ) { // Send our "last state" informations to opponent const L = this.game.moves.length; @@ -591,6 +610,7 @@ export default { // Since we played a move (or abort or resign), // only drawOffer=="sent" is possible drawSent: this.drawOffer == "sent", + rematchSent: this.rematchOffer == "sent", score: this.game.score, score: this.game.scoreMsg, movesCount: L, @@ -683,6 +703,41 @@ export default { // NOTE: observers don't know who offered draw this.drawOffer = "received"; break; + case "rematchoffer": + // NOTE: observers don't know who offered rematch + this.rematchOffer = data.data ? "received" : ""; + break; + case "newgame": { + // A game started, redirect if I'm playing in + const gameInfo = data.data; + if ( + gameInfo.players.some(p => + p.sid == this.st.user.sid || p.uid == this.st.user.id) + ) { + this.$router.push("/game/" + gameInfo.id); + } else { + let urlRid = ""; + if (gameInfo.cadence.indexOf('d') === -1) { + urlRid = "/?rid="; + // Select sid of any of the online players: + let onlineSid = []; + gameInfo.players.forEach(p => { + if (!!this.people[p.sid]) onlineSid.push(p.sid); + }); + urlRid += onlineSid[Math.floor(Math.random() * onlineSid.length)]; + } + this.infoMessage = + this.st.tr["Rematch in progress:"] + + " <a href='#/game/" + + gameInfo.id + urlRid + + "'>" + + "#/game/" + + gameInfo.id + urlRid + + "</a>"; + document.getElementById("modalInfo").checked = true; + } + break; + } case "newchat": this.newChat = data.data; if (!document.getElementById("modalChat").checked) @@ -721,6 +776,7 @@ export default { this.processMove(data.lastMove, { clock: data.clock }); } if (data.drawSent) this.drawOffer = "received"; + if (data.rematchSent) this.rematchOffer = "received"; if (data.score != "*") { this.drawOffer = ""; if (this.game.score == "*") @@ -754,6 +810,80 @@ export default { } else this.updateCorrGame({ drawOffer: this.game.mycolor }); } }, + clickRematch: function() { + if (!this.game.mycolor) return; //I'm just spectator + if (this.rematchOffer == "received") { + // Start a new game! + let gameInfo = { + id: getRandString(), //ignored if corr + fen: V.GenRandInitFen(this.game.randomness), + players: this.game.players.reverse(), + vid: this.game.vid, + cadence: this.game.cadence + }; + let oppsid = this.getOppsid(); //may be null + this.send("rnewgame", { data: gameInfo, oppsid: oppsid }); + if (this.game.type == "live") { + const game = Object.assign( + {}, + gameInfo, + { + // (other) Game infos: constant + fenStart: gameInfo.fen, + vname: this.game.vname, + created: Date.now(), + // Game state (including FEN): will be updated + moves: [], + clocks: [-1, -1], //-1 = unstarted + initime: [0, 0], //initialized later + score: "*" + } + ); + GameStorage.add(game, (err) => { + // No error expected. + if (!err) { + if (this.st.settings.sound) + new Audio("/sounds/newgame.flac").play().catch(() => {}); + this.$router.push("/game/" + gameInfo.id); + } + }); + } + else { + // corr game + ajax( + "/games", + "POST", + { + // cid is useful to delete the challenge: + data: { gameInfo: gameInfo }, + success: (response) => { + gameInfo.id = response.gameId; + this.$router.push("/game/" + response.gameId); + } + } + ); + } + } else if (this.rematchOffer == "") { + this.rematchOffer = "sent"; + this.send("rematchoffer", { data: true }); + if (this.game.type == "live") { + GameStorage.update( + this.gameRef.id, + { rematchOffer: this.game.mycolor } + ); + } else this.updateCorrGame({ rematchOffer: this.game.mycolor }); + } else if (this.rematchOffer == "sent") { + // Toggle rematch offer (on --> off) + this.rematchOffer = ""; + this.send("rematchoffer", { data: false }); + if (this.game.type == "live") { + GameStorage.update( + this.gameRef.id, + { rematchOffer: '' } + ); + } else this.updateCorrGame({ rematchOffer: 'n' }); + } + }, abortGame: function() { if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) return; this.gameOver("?", "Stop"); @@ -845,6 +975,7 @@ export default { } } } + // TODO: merge next 2 "if" conditions if (!!game.drawOffer) { if (game.drawOffer == "t") // Three repetitions @@ -863,6 +994,18 @@ export default { } } } + if (!!game.rematchOffer) { + if (myIdx < 0) this.rematchOffer = "received"; + else { + // I play in this game: + if ( + (game.rematchOffer == "w" && myIdx == 0) || + (game.rematchOffer == "b" && myIdx == 1) + ) + this.rematchOffer = "sent"; + else this.rematchOffer = "received"; + } + } this.repeat = {}; //reset: scan past moves' FEN: let repIdx = 0; let vr_tmp = new V(game.fenStart); @@ -1123,13 +1266,8 @@ export default { clearInterval(this.retrySendmove); return; } - let oppsid = this.game.players[nextIdx].sid; - if (!oppsid) { - oppsid = Object.keys(this.people).find( - sid => this.people[sid].id == this.game.players[nextIdx].uid - ); - } - if (!oppsid || !this.people[oppsid]) + const oppsid = this.getOppsid(); + if (!oppsid) // Opponent is disconnected: he'll ask last state clearInterval(this.retrySendmove); else { @@ -1237,6 +1375,10 @@ export default { </script> <style lang="sass" scoped> +#infoDiv > .card + padding: 15px 0 + max-width: 430px + .connected background-color: lightgreen @@ -1337,6 +1479,12 @@ span.yourturn .draw-threerep, .draw-threerep:hover background-color: #e4d1fc +.rematch-sent, .rematch-sent:hover + background-color: lightyellow + +.rematch-received, .rematch-received:hover + background-color: lightgreen + .somethingnew background-color: #c5fefe diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 9656c967..4cb46ec9 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -682,9 +682,8 @@ export default { } case "game": //individual request case "newgame": { - // NOTE: it may be live or correspondance const game = data.data; - // Ignore games where I play (corr games) + // Ignore games where I play (will go in MyGames page) if (game.players.every(p => p.sid != this.st.user.sid || p.id != this.st.user.id)) { @@ -720,7 +719,7 @@ export default { break; } case "startgame": { - // New game just started: data contain all information + // New game just started, I'm involved const gameInfo = data.data; if (this.classifyObject(gameInfo) == "live") this.startNewGame(gameInfo); @@ -733,8 +732,7 @@ export default { "#/game/" + gameInfo.id + "</a>"; - let modalBox = document.getElementById("modalInfo"); - modalBox.checked = true; + document.getElementById("modalInfo").checked = true; } break; } @@ -964,16 +962,17 @@ export default { const notifyNewgame = () => { const oppsid = this.getOppsid(c); if (!!oppsid) - //opponent is online + // Opponent is online 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 }); + // Send game info (only if live) to everyone except me and opponent + // TODO: this double message send could be avoided. + this.send("newgame", { data: gameInfo, oppsid: oppsid }); }; if (c.type == "live") { notifyNewgame(); this.startNewGame(gameInfo); - } //corr: game only on server - else { + } else { + // corr: game only on server ajax( "/games", "POST", @@ -991,25 +990,36 @@ export default { }, // 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, - vname: this.getVname(gameInfo.vid), - created: Date.now(), - // Game state (including FEN): will be updated - moves: [], - clocks: [-1, -1], //-1 = unstarted - initime: [0, 0], //initialized later - score: "*" - }); - GameStorage.add(game, (err) => { - // If an error occurred, game is not added: abort - if (!err) { - if (this.st.settings.sound) - new Audio("/sounds/newgame.flac").play().catch(() => {}); - this.$router.push("/game/" + gameInfo.id); + const game = Object.assign( + {}, + gameInfo, + { + // (other) Game infos: constant + fenStart: gameInfo.fen, + vname: this.getVname(gameInfo.vid), + created: Date.now(), + // Game state (including FEN): will be updated + moves: [], + clocks: [-1, -1], //-1 = unstarted + initime: [0, 0], //initialized later + score: "*" } - }); + ); + setTimeout( + () => { + GameStorage.add(game, (err) => { + // If an error occurred, game is not added: a tab already + // added the game and (if focused) is redirected toward it. + // If no error and the tab is hidden: do not show anything. + if (!err && !document.hidden) { + if (this.st.settings.sound) + new Audio("/sounds/newgame.flac").play().catch(() => {}); + this.$router.push("/game/" + gameInfo.id); + } + }); + }, + document.hidden ? 500 + 1000 * Math.random() : 0 + ); } } }; diff --git a/server/models/Game.js b/server/models/Game.js index fc485910..e45c1827 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -212,6 +212,8 @@ const GameModel = ) ) && ( !obj.drawOffer || !!(obj.drawOffer.match(/^[wbtn]$/)) + ) && ( + !obj.rematchOffer || !!(obj.rematchOffer.match(/^[wbn]$/)) ) && ( !obj.fen || !!(obj.fen.match(/^[a-zA-Z0-9, /-]*$/)) ) && ( @@ -234,12 +236,18 @@ const GameModel = 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) { - if (obj.drawOffer == "n") //Special "None" update + if (obj.drawOffer == "n") //special "None" update obj.drawOffer = ""; modifs += "drawOffer = '" + obj.drawOffer + "',"; } + 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) diff --git a/server/routes/games.js b/server/routes/games.js index 57656f41..21aee475 100644 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -8,14 +8,15 @@ const params = require("../config/parameters"); // From main hall, start game between players 0 and 1 router.post("/games", access.logged, access.ajax, (req,res) => { const gameInfo = req.body.gameInfo; + // Challenge ID is provided if game start from Hall: const cid = req.body.cid; if ( Array.isArray(gameInfo.players) && gameInfo.players.some(p => p.id == req.userId) && - cid.toString().match(/^[0-9]+$/) && + (!cid || cid.toString().match(/^[0-9]+$/)) && GameModel.checkGameInfo(gameInfo) ) { - ChallengeModel.remove(cid); + if (!!cid) ChallengeModel.remove(cid); GameModel.create( gameInfo.vid, gameInfo.fen, gameInfo.cadence, gameInfo.players, (err,ret) => { diff --git a/server/sockets.js b/server/sockets.js index 39cf260b..f3224f1f 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -190,13 +190,24 @@ module.exports = function(wss) { // Notify all room: mostly game events case "newchat": case "newchallenge": - case "newgame": case "deletechallenge": + case "newgame": case "resign": case "abort": case "drawoffer": + case "rematchoffer": case "draw": - notifyRoom(page, obj.code, {data: obj.data}); + if (!!obj.oppsid) + // "newgame" message from Hall: do not target players + notifyAllBut(page, "newgame", {data: obj.data}, [sid, obj.oppsid]); + else notifyRoom(page, obj.code, {data: obj.data}); + break; + + case "rnewgame": + // A rematch game started: players are already informed + notifyAllBut(page, "newgame", {data: obj.data}, [sid, obj.oppsid]); + notifyAllBut("/", "newgame", {data: obj.data}, [sid, obj.oppsid]); + notifyRoom("/mygames", "newgame", {data: obj.data}); break; case "newmove": { @@ -267,11 +278,11 @@ module.exports = function(wss) { case "getfocus": case "losefocus": - if (page == "/") notifyAllButMe("/", obj.code, { page: "/" }); + if (page == "/") notifyAllBut("/", obj.code, { page: "/" }, [sid]); else { // Notify game room + Hall: - notifyAllButMe(page, obj.code); - notifyAllButMe("/", obj.code, { page: page }); + notifyAllBut(page, obj.code, {}, [sid]); + notifyAllBut("/", obj.code, { page: page }, [sid]); } break; -- 2.44.0