From: Benjamin Auder Date: Wed, 19 Jun 2019 16:26:53 +0000 (+0200) Subject: Some fixes. TODO: check clocks update, and continue work on resign/abort/draw X-Git-Url: https://git.auder.net/?p=vchess.git;a=commitdiff_plain;h=967a2686ea801d4b33129d78087651451ef1904b Some fixes. TODO: check clocks update, and continue work on resign/abort/draw --- diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 4a40a3a5..254741d5 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -22,17 +22,8 @@ a#download(href="#") .button-group button#downloadBtn(@click="download") {{ st.tr["Download PGN"] }} - // TODO: Import game button copy game locally in IndexedDB //button Import game - - -// TODO: do not use localStorage for current game, but directly indexedDB -// update function is similar -// ==> retrieval functions must filter on score, and potential "imported" tag -// ==> this should allow several simultaneous games - - //MoveList(v-if="showMoves" :moves="moves" :cursor="cursor" @goto-move="gotoMove") diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js new file mode 100644 index 00000000..94c1c30c --- /dev/null +++ b/client/src/utils/gameStorage.js @@ -0,0 +1,136 @@ +// Game object: { +// // Static informations: +// gameId: string +// vname: string, +// fenStart: string, +// players: array of sid+id+name, +// timeControl: string, +// increment: integer (seconds), +// mode: string ("live" or "corr") +// imported: boolean (optional, default false) +// // Game (dynamic) state: +// fen: string, +// moves: array of Move objects, +// clocks: array of integers, +// initime: integer (when clock start running), +// score: string (several options; '*' == running), +// } + +function dbOperation(callback) +{ + let db = null; + let DBOpenRequest = window.indexedDB.open("vchess", 4); + + DBOpenRequest.onerror = function(event) { + alert("Database error: " + event.target.errorCode); + }; + + DBOpenRequest.onsuccess = function(event) { + db = DBOpenRequest.result; + callback(db); + db.close(); + }; + + DBOpenRequest.onupgradeneeded = function(event) { + let db = event.target.result; + db.onerror = function(event) { + alert("Error while loading database: " + event.target.errorCode); + }; + // Create objectStore for vchess->games + db.createObjectStore("games", { keyPath: "gameId" }); + } +} + +export const GameStorage = +{ + // Optional callback to get error status + add: function(game, callback) + { + dbOperation((db) => { + let transaction = db.transaction("games", "readwrite"); + if (callback) + { + transaction.oncomplete = function() { + callback({}); //everything's fine + } + transaction.onerror = function() { + callback({errmsg: "addGame failed: " + transaction.error}); + }; + } + let objectStore = transaction.objectStore("games"); + objectStore.add(game); + }); + }, + + // TODO: also option to takeback a move ? + // NOTE: for live games only (all on server for corr) + update: function(gameId, obj) //colorIdx, move, fen, addTime, initime, score + { + dbOperation((db) => { + let objectStore = db.transaction("games", "readwrite").objectStore("games"); + objectStore.get(gameId).onsuccess = function(event) { + const game = event.target.result; + if (!!obj.move) + { + game.moves.push(obj.move); + game.fen = obj.fen; + if (!!obj.addTime) //NaN if first move in game + game.clocks[obj.colorIdx] += obj.addTime; + } + if (!!obj.initime) //just a flag (true) + game.initime = Date.now(); + if (!!obj.score) + game.score = obj.score; + objectStore.put(game); //save updated data + } + }); + }, + + // Retrieve any live game from its identifiers (locally, running or not) + // NOTE: need callback because result is obtained asynchronously + get: function(gameId, callback) + { + dbOperation((db) => { + let objectStore = db.transaction('games').objectStore('games'); + if (!gameId) //retrieve all + { + let games = []; + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + // if there is still another cursor to go, keep running this code + if (cursor) + { + games.push(cursor.value); + cursor.continue(); + } + else + callback(games); + } + } + else //just one game + { + objectStore.get(gameId).onsuccess = function(event) { + callback(event.target.result); + } + } + }); + }, + + // Delete a game in indexedDB + remove: function(gameId, callback) + { + dbOperation((db) => { + let transaction = db.transaction(["games"], "readwrite"); + if (callback) + { + transaction.oncomplete = function() { + callback({}); //everything's fine + } + transaction.onerror = function() { + callback({errmsg: "removeGame failed: " + transaction.error}); + }; + } + transaction.objectStore("games").delete(gameId); + }); + }, +}; diff --git a/client/src/utils/storage.js b/client/src/utils/storage.js deleted file mode 100644 index 2e7a84fb..00000000 --- a/client/src/utils/storage.js +++ /dev/null @@ -1,214 +0,0 @@ -import { extractTime } from "@/utils/timeControl"; - -// TODO: show game structure -//const newItem = [ -// { gameId: "", players: [], timeControl: "", clocks: [] } -//]; - -function dbOperation(callback) -{ - let db = null; - let DBOpenRequest = window.indexedDB.open("vchess", 4); - - DBOpenRequest.onerror = function(event) { - alert("Database error: " + event.target.errorCode); - }; - - DBOpenRequest.onsuccess = function(event) { - db = DBOpenRequest.result; - callback(db); - db.close(); - }; - - DBOpenRequest.onupgradeneeded = function(event) { - let db = event.target.result; - db.onerror = function(event) { - alert("Error while loading database: " + event.target.errorCode); - }; - // Create objectStore for vchess->games - db.createObjectStore("games", { keyPath: "gameId" }); - } -} - -// Optional callback to get error status -function addGame(game, callback) -{ - dbOperation((db) => { - let transaction = db.transaction(["games"], "readwrite"); - if (callback) - { - transaction.oncomplete = function() { - callback({}); //everything's fine - } - transaction.onerror = function() { - callback({errmsg: "addGame failed: " + transaction.error}); - }; - } - let objectStore = transaction.objectStore("games"); - objectStore.add(game); - }); -} - -// Clear current live game from localStorage -function clear() { - localStorage.removeItem("gameInfo"); - localStorage.removeItem("gameState"); -} - -// Current live game: -function getCurrent() -{ - return Object.assign({}, - JSON.parse(localStorage.getItem("gameInfo")), - JSON.parse(localStorage.getItem("gameState"))); -} - -// Only called internally after a score update -function transferToDb() -{ - addGame(getCurrent(), (err) => { - if (!!err.errmsg) - return err; - clear(); - }); -} - -export const GameStorage = -{ - // localStorage: - init: function(o) - { - // Extract times (in [milli]seconds), set clocks, store in localStorage - const tc = extractTime(o.timeControl); - - // game infos: constant - const gameInfo = - { - gameId: o.gameId, - vname: o.vname, - fenStart: o.fenStart, - players: o.players, - timeControl: o.timeControl, - increment: tc.increment, - mode: "live", //function for live games only - }; - - // game state: will be updated - const gameState = - { - fen: o.fenStart, - moves: [], - clocks: [...Array(o.players.length)].fill(tc.mainTime), - initime: (o.initime ? Date.now() : undefined), - score: "*", - }; - - localStorage.setItem("gameInfo", JSON.stringify(gameInfo)); - localStorage.setItem("gameState", JSON.stringify(gameState)); - }, - - getInitime: function() - { - const gameState = JSON.parse(localStorage.getItem("gameState")); - return gameState.initime; - }, - - // localStorage: - // TODO: also option to takeback a move ? - // NOTE: for live games only (all on server for corr) - update: function(o) //colorIdx, move, fen, addTime, initime, score - { - let gameState = JSON.parse(localStorage.getItem("gameState")); - if (!!o.move) - { - gameState.moves.push(o.move); - gameState.fen = o.fen; - if (!!o.addTime) //NaN if first move in game - gameState.clocks[o.colorIdx] += o.addTime; - } - if (!!o.initime) //just a flag (true) - gameState.initime = Date.now(); - if (!!o.score) - gameState.score = o.score; - localStorage.setItem("gameState", JSON.stringify(gameState)); - if (!!o.score && o.score != "*") - transferToDb(); //game is over - }, - - // indexedDB: - // Since DB requests are asynchronous, require a callback using the result - // TODO: option for remote retrieval (third arg, or just "gameRef") - getLocal: function(gameId, callback) - { - dbOperation((db) => { - let objectStore = db.transaction('games').objectStore('games'); - if (!gameId) //retrieve all - { - let games = []; - objectStore.openCursor().onsuccess = function(event) { - let cursor = event.target.result; - // if there is still another cursor to go, keep running this code - if (cursor) - { - games.push(cursor.value); - cursor.continue(); - } - else - callback(games); - } - } - else //just one game - { - objectStore.get(gameId).onsuccess = function(event) { - callback(event.target.result); - } - } - }); - }, - - // Delete a game in indexedDB - remove: function(gameId, callback) - { - dbOperation((db) => { - let transaction = db.transaction(["games"], "readwrite"); - if (callback) - { - transaction.oncomplete = function() { - callback({}); //everything's fine - } - transaction.onerror = function() { - callback({errmsg: "game removal failed: " + transaction.error}); - }; - } - transaction.objectStore("games").delete(gameId); - }); - }, - - // Retrieve any live game from its identifiers (remote or not, running or not) - // NOTE: need callback because result might be obtained asynchronously - get: function(gameRef, callback) - { - const gid = gameRef.id; - const rid = gameRef.rid; //may be blank - if (!!rid) - { - // TODO: send request to server which forward to user sid == rid, - // need to listen to "remote game" event in main hall ? - return callback({}); //means "the game will arrive later" (TODO...) - } - - const gameInfoStr = localStorage.getItem("gameInfo"); - if (gameInfoStr) - { - const gameInfo = JSON.parse(gameInfoStr); - if (gameInfo.gameId == gid) - { - const gameState = JSON.parse(localStorage.getItem("gameState")); - return callback(Object.assign({}, gameInfo, gameState)); - } - } - - // Game is local and not running - GameStorage.getLocal(gid, callback); - }, -}; diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 9be88069..ab6ae7a8 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -25,7 +25,7 @@ import BaseGame from "@/components/BaseGame.vue"; //import Chat from "@/components/Chat.vue"; //import MoveList from "@/components/MoveList.vue"; import { store } from "@/store"; -import { GameStorage } from "@/utils/storage"; +import { GameStorage } from "@/utils/gameStorage"; export default { name: 'my-game', @@ -250,13 +250,12 @@ export default { // Next line will trigger a "gameover" event, bubbling up till here this.$refs["basegame"].endGame(this.game.mycolor=="w" ? "0-1" : "1-0"); }, - // 4 cases for loading a game: - // - from localStorage (one running game I play) - // - from indexedDB (one completed live game) + // 3 cases for loading a game: + // - from indexedDB (running or completed live game I play) // - from server (one correspondance game I play[ed] or not) // - from remote peer (one live game I don't play, finished or not) - loadGame: function() { - GameStorage.get(this.gameRef, async (game) => { + loadGame: function(game) { + const afterRetrieval = async (game) => { const vModule = await import("@/variants/" + game.vname + ".js"); window.V = vModule.VariantRules; this.vr = new V(game.fenStart); @@ -266,7 +265,23 @@ export default { {mycolor: [undefined,"w","b"][1 + game.players.findIndex( p => p.sid == this.st.user.sid)]}, ); - }); + }; + if (!!game) + return afterRetrival(game); + if (!!this.gameRef.rid) + { + // TODO: just send a game request message to the remote player, + // and when receiving answer just call loadGame(received_game) + // + remote peer should have registered us as an observer + // (send moves updates + resign/abort/draw actions) + return; + } + else + { + GameStorage.get(this.gameRef.id, async (game) => { + afterRetrieval(game); + }); + } // // Poll all players except me (if I'm playing) to know online status. // // --> Send ping to server (answer pong if players[s] are connected) // if (this.gameInfo.players.some(p => p.sid == this.st.user.sid)) @@ -300,7 +315,7 @@ export default { let addTime = undefined; if (move.color == this.game.mycolor) { - const elapsed = Date.now() - GameStorage.getInitime(); + const elapsed = Date.now() - this.game.initime; this.game.players.forEach(p => { if (p.sid != this.st.user.sid) { @@ -315,13 +330,20 @@ export default { // elapsed time is measured in milliseconds addTime = this.game.increment - elapsed/1000; } - GameStorage.update({ + const myTurnNow = (this.vr.turn == this.game.mycolor); + GameStorage.update(this.gameRef.id, + { colorIdx: colorIdx, move: filtered_move, fen: move.fen, addTime: addTime, - initime: (this.vr.turn == this.game.mycolor), //my turn now? + initime: myTurnNow, }); + // Also update current game object: + this.game.moves.push(move); + this.game.fen = move.fen; + this.game.clocks[colorIdx] += (!!addTime ? addTime : 0); + this.game.initime = (myTurnNow ? Date.now() : undefined); }, // TODO: this update function should also work for corr games gameOver: function(score) { diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index dd4621b3..22aa53e0 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -81,7 +81,8 @@ import { ajax } from "@/utils/ajax"; import { getRandString, shuffle } from "@/utils/alea"; import GameList from "@/components/GameList.vue"; import ChallengeList from "@/components/ChallengeList.vue"; -import { GameStorage } from "@/utils/storage"; +import { GameStorage } from "@/utils/gameStorage"; +import { extractTime } from "@/utils/timeControl"; export default { name: "my-hall", components: { @@ -292,20 +293,21 @@ export default { } case "askgame": { - // Send my current live game (if any) - if (!!localStorage["gid"]) - { - const myGame = - { - // Minimal game informations: (fen+clock not required) - id: localStorage["gid"], - players: JSON.parse(localStorage["players"]), //array sid+id+name - vname: localStorage["vname"], - timeControl: localStorage["timeControl"], - }; - this.st.conn.send(JSON.stringify({code:"game", - game:myGame, target:data.from})); - } + // Send my current live games (if any) + // TODO: from indexedDB, through GameStorage. +// if (!!localStorage["gid"]) +// { +// const myGame = +// { +// // Minimal game informations: (fen+clock not required) +// id: localStorage["gid"], +// players: JSON.parse(localStorage["players"]), //array sid+id+name +// vname: localStorage["vname"], +// timeControl: localStorage["timeControl"], +// }; +// this.st.conn.send(JSON.stringify({code:"game", +// game:myGame, target:data.from})); +// } break; } case "identity": @@ -331,6 +333,7 @@ export default { { // Receive game from some player (+sid) // NOTE: it may be correspondance (if newgame while we are connected) + // TODO: ambiguous naming "newGame" ==> rename function ? let newGame = data.game; newGame.type = this.classifyObject(data.game); newGame.vname = newGame.vname; @@ -586,14 +589,27 @@ export default { }, // NOTE: for live games only (corr games are launched on server) newGame: function(gameInfo) { - GameStorage.init({ + // Extract times (in [milli]seconds), set clocks + const tc = extractTime(gameInfo.timeControl); + const IPlayFirst = (gameInfo.players[0].sid == this.st.user.sid); + const game = + { + // Game infos: constant gameId: gameInfo.gameId, vname: this.getVname(gameInfo.vid), fenStart: gameInfo.fen, players: gameInfo.players, timeControl: gameInfo.timeControl, - initime: (gameInfo.players[0].sid == this.st.user.sid), - }); + increment: tc.increment, + mode: "live", //function for live games only + // Game state: will be updated + fen: gameInfo.fen, + moves: [], + clocks: [...Array(gameInfo.players.length)].fill(tc.mainTime), + initime: (IPlayFirst ? Date.now() : undefined), + score: "*", + }; + GameStorage.add(game); if (this.st.settings.sound >= 1) new Audio("/sounds/newgame.mp3").play().catch(err => {}); // TODO: redirect to game @@ -605,3 +621,9 @@ export default { + +