From: Benjamin Auder <benjamin.auder@somewhere> Date: Thu, 30 Jan 2020 17:53:20 +0000 (+0100) Subject: 'update' X-Git-Url: https://git.auder.net/doc/html/css/scripts/pieces/assets/rpsls.css?a=commitdiff_plain;h=63ca2b89cfe577efd168c6b2e26750cb01b66d64;p=vchess.git 'update' --- diff --git a/TODO b/TODO index 354e91d6..95431640 100644 --- a/TODO +++ b/TODO @@ -1,2 +1,45 @@ -styling: connection indicator, names, put movesList + chat in better positions -complete translations, stylesheets, variants rules ... +au moins l'échange des coups en P2P ? et game chat ? +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) + +Click elsewhere make modal disappear (for now: Esc key works...) + +Use better-sqlite3 instead of node-sqlite3: +https://www.npmjs.com/package/better-sqlite3 + +Canvas for hexagonal board Vue reactivity : +https://stackoverflow.com/questions/40177493/drawing-onto-a-canvas-with-vue-js +custom directives ? + +Desktop notifications: +https://developer.mozilla.org/fr/docs/Web/API/notification + +Think about this: +https://alligator.io/vuejs/component-communication/ +https://alligator.io/vuejs/global-event-bus/ + +Dans variant page, "mes parties" peut toujours contenir corr + importées (deux onglets) +En fin de partie (observée ou non), bouton "import game" en + de "download game" ==> directement dans indexedDB +les parties par correspondance survivent 7 jours après la fin de partie + +mat en 2 échiqueté : brnkr3/pppp1p1p/4ps2/8/2P2P2/P1qP4/2c1s1PP/R1K5 +(Bb3+ Kb1 Ba2#) + +Importer des parties : nécessite de parser le PGN produit (possible, un peu pénible) + +espagnol : jugada ou movimiento ? +fin de la partida au lieu de final de partida ? + +Mode new game contre un ami comme sur lichess ? + +Coordonnées sur échiquier: sur cases, à gauche (verticale) ou en bas (horizontale) + +Import game : en local dans indexedDb, affichage dans "Games --> Imported" + +Hexachess: McCooey et Shafran (deux tailles, randomisation OK) +http://www.math.bas.bg/~iad/tyalie/shegra/shegrax.html +http://www.quadibloc.com/chess/ch0401.htm + +Inspiration for refactor: +https://github.com/triestpa/Vue-Chess/blob/master/src/components/chessboard/chessboard.js +https://github.com/gustaYo/vue-chess diff --git a/_tmp/TODO b/_tmp/TODO deleted file mode 100644 index 86d36fe3..00000000 --- a/_tmp/TODO +++ /dev/null @@ -1,104 +0,0 @@ ---> 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) -Possible filter: write a few variant names, to keep only these. ---> In settings ! - -Use better-sqlite3 instead of node-sqlite3: -https://www.npmjs.com/package/better-sqlite3 - -Canvas for hexagonal board Vue reactivity : -https://stackoverflow.com/questions/40177493/drawing-onto-a-canvas-with-vue-js -custom directives ? - -Desktop notifications: -https://developer.mozilla.org/fr/docs/Web/API/notification - -Think about this: -https://alligator.io/vuejs/component-communication/ -https://alligator.io/vuejs/global-event-bus/ - -CRON task remove unlogged users, finished corr games after 7 days, individual challenges older than 7 days - -tell opponent that I got the move, for him to start timer (and lose...) - --> no, not needed and impossible if everybody is offline - ==> just store this time locally (cheating possible but...) -board2, board3, board4 -VariantRules2, 3 et 4 aussi -fetch challenges and corr games from server at startup (room) -but forbid anonymous to start corr games or accept challenges - -Dans variant page, "mes parties" peut toujours contenir corr + importées (deux onglets) -En fin de partie (observée ou non), bouton "import game" en + de "download game" ==> directement dans indexedDB ---> sursis de 7 jours pour les parties par correspondance, qui sont encore chargées depuis le serveur - -mat en 2 échiqueté : brnkr3/pppp1p1p/4ps2/8/2P2P2/P1qP4/2c1s1PP/R1K5 -(Bb3+ Kb1 Ba2#) - -// TODO: decodeURIComponent() for GET/DELETE parameters - -2) Integrate computer play into rules tab -3) Allow correspondance play (no need for P2P: online moves through the server (which also store them)) -4) Write my-games tab (included current/finished/imported) - Use Dexie.js, or anything to store games locally -5) Write room tab - Use this: https://github.com/feross/simple-peer for online games+challenges+chat -6) Test... and publish - -Finish rules translation in Spanish + improve existing ones -Design: final touch (gain extra space on top, using space on the right) -Crazyhouse: center reserves, grey if zero available, numbers superimposed -Promotions: increase pieces sizes, better background. -Code: use two spaces instead of tabs, everywhere. -Increase code line length to 100 or more? -(http://katafrakt.me/2017/09/16/80-characters-line-length-limit/) -Chat button should be more apparent after game ends (color ?) -Reinforce security for problems upload (how ?) - -Later: -Let choice of time control, allow correspondance play, several corr games at the same time -==> need to use indexedDB instead of localStorage. Maybe with Dexie https://dexie.org/ -Each user would have a unique identifier stored in the client DB. -Allow to cancel games (if opponent doesn't connect again) -Live games storage would be browser-based: different games on smartphone, home computer, work computer... (why not ?) -==> (at most 1) running, and finished (which can be deleted from local memory) -Allow challenging a specific player (by his chosen name) -But keep the random pairings as main playing way + always playing in ZEN mode - -style menu : surligner onglet courant - -Interface : - - newGame: une modalBox à paramètres, timeControl, type d'adversaire ==> "new Game") - -Importer des parties : nécessite de parser le PGN produit (possible, un peu pénible) -mais permettrait mode analyse (avec bouton "analyse", comme sur ancien site). - -espagnol : jugada ou movimiento ? -fin de la partida au lieu de final de partida ? - -Bouton new game ==> human only. Indiquer adversaire (éventuellement), cadence (ou "infini") -Mode analyse : accessible à tout moment d'une partie (HH, ou computer) terminée + bouton "analyze from here" (sur parties observées) - -Coordonnées sur échiquier: sur cases, à gauche (verticale) ou en bas (horizontale) - -Import game : en local dans indexedDb, affichage dans "Games --> Imported" - -Checkered : si intervention d'un 3eme joueur, initialiser son temps à la moyenne des temps restants des deux autres... ? - -Mode contre ordinateur : seulement accessible depuis onglet "Rules" (son principal intérêt) - -Hexachess: McCooey et Shafran (deux tailles, randomisation OK) -http://www.math.bas.bg/~iad/tyalie/shegra/shegrax.html -http://www.quadibloc.com/chess/ch0401.htm - -Inspiration for refactor: -https://github.com/triestpa/Vue-Chess/blob/master/src/components/chessboard/chessboard.js -https://github.com/gustaYo/vue-chess diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index e04f5082..1ea862fb 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -7,7 +7,7 @@ div#baseGame(tabindex=-1 @click="() => focusBg()" @keydown="handleKeys") h3#eogMessage.section {{ endgameMessage }} .row #boardContainer.col-sm-12.col-md-9 - Board(:vr="vr" :last-move="lastMove" :analyze="analyze" + Board(:vr="vr" :last-move="lastMove" :analyze="game.mode=='analyze'" :user-color="game.mycolor" :orientation="orientation" :vname="game.vname" @play-move="play") #controls @@ -16,11 +16,11 @@ div#baseGame(tabindex=-1 @click="() => focusBg()" @keydown="handleKeys") button(@click="flip") ⇅ button(@click="() => play()") > button(@click="gotoEnd") >> - #fenDiv(v-if="showFen && !!vr") - p(@click="gotoFenContent") {{ vr.getFen() }} #pgnDiv a#download(href="#") button(@click="download") {{ st.tr["Download PGN"] }} + button(v-if="game.mode!='analyze'" @click="analyzePosition") + | {{ st.tr["Analyze"] }} .col-sm-12.col-md-3 MoveList(v-if="showMoves" :score="game.score" :message="game.scoreMsg" :moves="moves" :cursor="cursor" @goto-move="gotoMove") @@ -59,20 +59,14 @@ export default { this.re_setVariables(); }, // Received a new move to play: - "game.moveToPlay": function() { - this.play(this.game.moveToPlay, "receive", this.game.vname=="Dark"); + "game.moveToPlay": function(newMove) { + if (!!newMove) //if stop + launch new game, get undefined move + this.play(newMove, "receive"); }, }, computed: { showMoves: function() { - return true; - //return window.innerWidth >= 768; - }, - showFen: function() { - return this.game.vname != "Dark" || this.game.score != "*"; - }, - analyze: function() { - return this.game.mode == "analyze" || this.game.score != "*"; + return this.game.vname != "Dark" || this.game.mode=="analyze"; }, }, created: function() { @@ -131,10 +125,11 @@ export default { this.cursor = L-1; this.lastMove = (L > 0 ? this.moves[L-1] : null); }, - gotoFenContent: function(event) { - const newUrl = "#/analyze/" + this.game.vname + - "/?fen=" + event.target.innerText.replace(/ /g, "_"); - window.open(newUrl); //to open in a new tab + analyzePosition: function() { + const newUrl = "/analyze/" + this.game.vname + + "/?fen=" + this.vr.getFen().replace(/ /g, "_"); + //window.open("#" + newUrl); //to open in a new tab + this.$router.push(newUrl); //better }, download: function() { const content = this.getPgn(); @@ -194,7 +189,7 @@ export default { modalBox.checked = true; setTimeout(() => { modalBox.checked = false; }, 2000); }, - animateMove: function(move) { + animateMove: function(move, callback) { let startSquare = document.getElementById(getSquareId(move.start)); let endSquare = document.getElementById(getSquareId(move.end)); let rectStart = startSquare.getBoundingClientRect(); @@ -219,66 +214,65 @@ export default { for (let i=0; i<squares.length; i++) squares.item(i).style.zIndex = "auto"; movingPiece.style = {}; //required e.g. for 0-0 with KR swap - this.play(move); + callback(); }, 250); }, - play: function(move, receive, noanimate) { + play: function(move, receive) { + // NOTE: navigate and receive are mutually exclusive const navigate = !move; - // Forbid playing outside analyze mode when cursor isn't at moves.length-1 - // (except if we receive opponent's move, human or computer) - if (!navigate && !this.analyze && !receive + // Forbid playing outside analyze mode, except if move is received. + // Sufficient condition because Board already knows which turn it is. + if (!navigate && this.game.mode!="analyze" && !receive && this.cursor < this.moves.length-1) { return; } - if (navigate) - { - if (this.cursor == this.moves.length-1) - return; //no more moves - move = this.moves[this.cursor+1]; - } - if (!!receive && !noanimate) //opponent move, variant != "Dark" - { - if (this.cursor < this.moves.length-1) + const doPlayMove = () => { + if (!!receive && this.cursor < this.moves.length-1) this.gotoEnd(); //required to play the move - return this.animateMove(move); - } - if (!navigate) - { - move.color = this.vr.turn; - move.notation = this.vr.getNotation(move); - } - // Not programmatic, or animation is over - this.vr.play(move); - this.cursor++; - this.lastMove = move; - if (this.st.settings.sound == 2) - new Audio("/sounds/move.mp3").play().catch(err => {}); - if (!navigate) - { - move.fen = this.vr.getFen(); - if (this.game.score == "*" || this.analyze) + if (navigate) + { + if (this.cursor == this.moves.length-1) + return; //no more moves + move = this.moves[this.cursor+1]; + } + else { + move.color = this.vr.turn; + move.notation = this.vr.getNotation(move); + } + this.vr.play(move); + this.cursor++; + this.lastMove = move; + if (this.st.settings.sound == 2) + new Audio("/sounds/move.mp3").play().catch(err => {}); + if (!navigate) + { + move.fen = this.vr.getFen(); // Stack move on movesList at current cursor if (this.cursor == this.moves.length) this.moves.push(move); else 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(); - if (score != "*") - { - const message = this.getScoreMessage(score); - if (!this.analyze) - this.$emit("gameover", score, message); - else //just show score on screen (allow undo) - this.showEndgameMsg(score + " . " + message); - } + if (this.game.mode != "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(); + if (score != "*") + { + const message = this.getScoreMessage(score); + if (this.game.mode != "analyze") + this.$emit("gameover", score, message); + else //just show score on screen (allow undo) + this.showEndgameMsg(score + " . " + message); + } + }; + if (!!receive && this.game.vname != "Dark") + this.animateMove(move, doPlayMove); + else + doPlayMove(); }, undo: function(move) { const navigate = !move; @@ -328,7 +322,7 @@ export default { <style lang="sass"> #modal-eog+div .card overflow: hidden -#pgnDiv, #fenDiv +#pgnDiv text-align: center margin-left: auto margin-right: auto diff --git a/client/src/components/Chat.vue b/client/src/components/Chat.vue index 980cd177..cd3c3c89 100644 --- a/client/src/components/Chat.vue +++ b/client/src/components/Chat.vue @@ -51,6 +51,7 @@ export default { chatInput.value = ""; const chat = {msg:chatTxt, name: this.st.user.name || "@nonymous", sid:this.st.user.sid}; + this.$emit("newchat", chat); //useful for corr games this.chats.unshift(chat); this.st.conn.send(JSON.stringify({ code:"newchat", msg:chatTxt, name:chat.name})); diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue index 46b672bb..78f6eb49 100644 --- a/client/src/components/ComputerGame.vue +++ b/client/src/components/ComputerGame.vue @@ -14,7 +14,7 @@ export default { BaseGame, }, // gameInfo: fen + mode + vname - // mode: "auto" (game comp vs comp), "versus" (normal) or "analyze" + // mode: "auto" (game comp vs comp) or "versus" (normal) props: ["gameInfo"], data: function() { return { @@ -35,7 +35,6 @@ export default { if (newScore != "*") { this.game.score = newScore; //user action - this.game.mode = "analyze"; if (!this.compThink) this.$emit("game-stopped"); //otherwise wait for comp } @@ -66,7 +65,7 @@ export default { let moveIdx = 0; let self = this; (function executeMove() { - self.$refs.basegame.play(compMove[moveIdx++], animate); + self.$set(self.game, "moveToPlay", compMove[moveIdx++]); if (moveIdx >= compMove.length) { self.compThink = false; @@ -126,7 +125,6 @@ export default { gameOver: function(score, scoreMsg) { this.game.score = score; this.game.scoreMsg = scoreMsg; - this.game.mode = "analyze"; this.$emit("game-over", score); //bubble up to Rules.vue }, }, diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue index 70756cc3..91607c25 100644 --- a/client/src/components/MoveList.vue +++ b/client/src/components/MoveList.vue @@ -31,8 +31,8 @@ export default { if (rows.length > 0) { rows[Math.floor(newValue/2)].scrollIntoView({ - behavior: 'smooth', - block: 'center' + behavior: "auto", + block: "nearest", }); } }); diff --git a/client/src/main.js b/client/src/main.js index 674852db..6e229362 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -12,6 +12,16 @@ new Vue({ }, created: function() { window.doClick = (elemId) => { document.getElementById(elemId).click() }; + document.addEventListener("keydown", (e) => { + if (e.code === "Escape") + { + let modalBoxes = document.querySelectorAll("[id^='modal']"); + modalBoxes.forEach(m => { + if (m.checked) + m.checked = false; + }); + } + }); // TODO: why is this wrong? //store.initialize(this.$route.path); store.initialize(window.location.href.split("#")[1]); diff --git a/client/src/translations/en.js b/client/src/translations/en.js index ce0bf5c6..48e77f27 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -61,6 +61,7 @@ export const translations = "Type here": "Type here", "Send": "Send", "Download PGN": "Download PGN", + "Analyze": "Analyze", "Cancel": "Cancel", // Game page: diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index b7cf85c1..a9ebf33a 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -67,7 +67,7 @@ export const GameStorage = }, // TODO: also option to takeback a move ? - update: function(gameId, obj) //move, fen, clocks, score, initime, ... + update: function(gameId, obj) //chat, move, fen, clocks, score, initime, ... { if (Number.isInteger(gameId) || !isNaN(parseInt(gameId))) { @@ -79,9 +79,9 @@ export const GameStorage = gid: gameId, newObj: { + chat: obj.chat, move: obj.move, //may be undefined... fen: obj.fen, - message: obj.message, score: obj.score, drawOffer: obj.drawOffer, } diff --git a/client/src/views/Analyze.vue b/client/src/views/Analyze.vue index cd25c254..a1352f7c 100644 --- a/client/src/views/Analyze.vue +++ b/client/src/views/Analyze.vue @@ -1,5 +1,8 @@ <template lang="pug"> main + .row + .col-sm-12 + #fenDiv(v-if="!!vr") {{ vr.getFen() }} .row .col-sm-12.col-md-10.col-md-offset-1 BaseGame(:game="game" :vr="vr" ref="basegame") diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 1ffcda41..7b14055f 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -2,7 +2,7 @@ main .row #chat.col-sm-12.col-md-4.col-md-offset-4 - Chat(:players="game.players") + Chat(:players="game.players" @newchat="processChat") .row .col-sm-12 #actions(v-if="game.mode!='analyze' && game.score=='*'") @@ -11,8 +11,6 @@ main button(@click="resign") Resign div Names: {{ game.players[0].name }} - {{ game.players[1].name }} div(v-if="game.score=='*'") Time: {{ virtualClocks[0] }} - {{ virtualClocks[1] }} - div(v-if="game.type=='corr'") {{ game.corrMsg }} - textarea(v-if="game.score=='*'" v-model="corrMsg") BaseGame(:game="game" :vr="vr" ref="basegame" @newmove="processMove" @gameover="gameOver") </template> @@ -41,7 +39,6 @@ export default { rid: "" }, 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 drawOffer: "", //TODO: use for button style @@ -198,7 +195,6 @@ export default { game:myGame, target:data.from})); break; case "newmove": - this.corrMsg = data.move.message; //may be empty this.$set(this.game, "moveToPlay", data.move); //TODO: Vue3... break; case "lastate": //got opponent infos about last move @@ -366,7 +362,6 @@ export default { vanish: s.vanish, start: s.start, end: s.end, - message: m.message, }; }); } @@ -447,8 +442,6 @@ export default { addTime = this.game.increment - elapsed/1000; } let sendMove = Object.assign({}, filtered_move, {addTime: addTime}); - if (this.game.type == "corr") - sendMove.message = this.corrMsg; this.people.forEach(p => { if (p.sid != this.st.user.sid) { @@ -472,7 +465,6 @@ export default { GameStorage.update(this.gameRef.id, { fen: move.fen, - message: this.corrMsg, move: { squares: filtered_move, @@ -513,6 +505,10 @@ export default { if (this.repeat[repIdx] >= 3) this.drawOffer = "received"; //TODO: will print "mutual agreement"... }, + processChat: function(chat) { + if (this.game.type == "corr") + GameStorage.update(this.gameRef.id, {chat: chat}); + }, gameOver: function(score, scoreMsg) { this.game.mode = "analyze"; this.game.score = score; diff --git a/_tmp/hexaboard_test.html b/hexaboard_test.html similarity index 100% rename from _tmp/hexaboard_test.html rename to hexaboard_test.html diff --git a/server/db/create.sql b/server/db/create.sql index 82c8659d..ff20c0b7 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -45,6 +45,11 @@ create table Games ( foreign key (vid) references Variants(id) ); +create table Chats ( + gid integer, + uid integer, +); + -- Store informations about players in a corr game create table Players ( gid integer, @@ -57,7 +62,6 @@ create table Players ( create table Moves ( gid integer, 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 foreign key (gid) references Games(id) diff --git a/server/models/Game.js b/server/models/Game.js index fa4aea02..67550779 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -134,13 +134,15 @@ const GameModel = }); }, - // obj can have fields move, fen, drawOffer and/or score + // obj can have fields move, message, fen, drawOffer and/or score update: function(id, obj) { db.parallelize(function() { let query = "UPDATE Games " + "SET "; + if (!!obj.message) + query += "message = message || ' ' || '" + obj.message + "',"; if (!!obj.drawOffer) query += "drawOffer = " + obj.drawOffer + ","; if (!!obj.fen) @@ -154,9 +156,9 @@ const GameModel = { const m = obj.move; query = - "INSERT INTO Moves (gid, squares, message, played, idx) VALUES " + - "(" + id + ",'" + JSON.stringify(m.squares) + "','" + m.message + - "'," + m.played + "," + m.idx + ")"; + "INSERT INTO Moves (gid, squares, played, idx) VALUES " + + "(" + id + ",'" + JSON.stringify(m.squares) + "'," + + m.played + "," + m.idx + ")"; db.run(query); } });