'update'
authorBenjamin Auder <benjamin.auder@somewhere>
Thu, 30 Jan 2020 17:53:20 +0000 (18:53 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Thu, 30 Jan 2020 17:53:20 +0000 (18:53 +0100)
14 files changed:
TODO
_tmp/TODO [deleted file]
client/src/components/BaseGame.vue
client/src/components/Chat.vue
client/src/components/ComputerGame.vue
client/src/components/MoveList.vue
client/src/main.js
client/src/translations/en.js
client/src/utils/gameStorage.js
client/src/views/Analyze.vue
client/src/views/Game.vue
hexaboard_test.html [moved from _tmp/hexaboard_test.html with 100% similarity]
server/db/create.sql
server/models/Game.js

diff --git a/TODO b/TODO
index 354e91d..9543164 100644 (file)
--- 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 (file)
index 86d36fe..0000000
--- 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
index e04f508..1ea862f 100644 (file)
@@ -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") &#8645;
         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
index 980cd17..cd3c3c8 100644 (file)
@@ -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}));
index 46b672b..78f6eb4 100644 (file)
@@ -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
     },
   },
index 70756cc..91607c2 100644 (file)
@@ -31,8 +31,8 @@ export default {
         if (rows.length > 0)
         {
           rows[Math.floor(newValue/2)].scrollIntoView({
-            behavior: 'smooth',
-            block: 'center'
+            behavior: "auto",
+            block: "nearest",
           });
         }
       });
index 674852d..6e22936 100644 (file)
@@ -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]);
index ce0bf5c..48e77f2 100644 (file)
@@ -61,6 +61,7 @@ export const translations =
   "Type here": "Type here",
   "Send": "Send",
   "Download PGN": "Download PGN",
+  "Analyze": "Analyze",
   "Cancel": "Cancel",
 
   // Game page:
index b7cf85c..a9ebf33 100644 (file)
@@ -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,
           }
index cd25c25..a1352f7 100644 (file)
@@ -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")
index 1ffcda4..7b14055 100644 (file)
@@ -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;
similarity index 100%
rename from _tmp/hexaboard_test.html
rename to hexaboard_test.html
index 82c8659..ff20c0b 100644 (file)
@@ -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)
index fa4aea0..6755077 100644 (file)
@@ -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);
       }
     });