Fix mycolor when resuming from game vs computer
[vchess.git] / public / javascripts / components / game.js
index 16e1fdb..0189e31 100644 (file)
@@ -11,8 +11,12 @@ Vue.component('my-game', {
                        selectedPiece: null, //moving piece (or clicked piece)
                        conn: null, //socket connection
                        score: "*", //'*' means 'unfinished'
-                       mode: "idle", //human, friend, computer or idle (when not playing)
+                       mode: "idle", //human, chat, friend, problem, computer or idle (if not playing)
+                       myid: "", //our ID, always set
                        oppid: "", //opponent ID in case of HH game
+                       myname: getCookie("username","anonymous"),
+                       oppName: "anonymous", //opponent name, revealed after a game (if provided)
+                       chats: [], //chat messages after human game
                        oppConnected: false,
                        seek: false,
                        fenStart: "",
@@ -22,10 +26,13 @@ Vue.component('my-game', {
                        color: getCookie("color", "lichess"), //lichess, chesscom or chesstempo
                        // sound level: 0 = no sound, 1 = sound only on newgame, 2 = always
                        sound: parseInt(getCookie("sound", "2")),
+                       // Web worker to play computer moves without freezing interface:
+                       compWorker: new Worker('/javascripts/playCompMove.js'),
+                       timeStart: undefined, //time when computer starts thinking
                };
        },
        watch: {
-               problem: function(p, pp) {
+               problem: function(p) {
                        // 'problem' prop changed: update board state
                        this.newGame("problem", p.fen, V.ParseFen(p.fen).turn);
                },
@@ -56,7 +63,7 @@ Vue.component('my-game', {
                        },
                        [h('i', { 'class': { "material-icons": true } }, "accessibility")])
                );
-               if (["idle","computer"].includes(this.mode))
+               if (["idle","chat","computer"].includes(this.mode))
                {
                        actionArray.push(
                                h('button',
@@ -73,7 +80,7 @@ Vue.component('my-game', {
                                [h('i', { 'class': { "material-icons": true } }, "computer")])
                        );
                }
-               if (["idle","friend"].includes(this.mode))
+               if (["idle","chat","friend"].includes(this.mode))
                {
                        actionArray.push(
                                h('button',
@@ -119,6 +126,29 @@ Vue.component('my-game', {
                                );
                                elementArray.push(connectedIndic);
                        }
+                       else if (this.mode == "chat")
+                       {
+                               // Also show connection indication, but also nickname
+                               const nicknameOpponent = h(
+                                       'div',
+                                       {
+                                               "class": {
+                                                       "clickable": true,
+                                                       "topindicator": true,
+                                                       "indic-left": true,
+                                                       "name-connected": this.oppConnected,
+                                                       "name-disconnected": !this.oppConnected,
+                                               },
+                                               domProps: {
+                                                       innerHTML: this.oppName,
+                                               },
+                                               on: {
+                                                       "click": () => { document.getElementById("modal-chat").checked = true; },
+                                               },
+                                       }
+                               );
+                               elementArray.push(nicknameOpponent);
+                       }
                        const turnIndic = h(
                                'div',
                                {
@@ -161,7 +191,10 @@ Vue.component('my-game', {
                                        h('div',
                                                {
                                                        attrs: { id: "instructions-div" },
-                                                       "class": { "section-content": true },
+                                                       "class": {
+                                                               "clearer": true,
+                                                               "section-content": true,
+                                                       },
                                                },
                                                [
                                                        h('p',
@@ -211,13 +244,13 @@ Vue.component('my-game', {
                        // Create board element (+ reserves if needed by variant or mode)
                        const lm = this.vr.lastMove;
                        const showLight = this.hints &&
-                               (this.mode!="idle" || this.cursor==this.vr.moves.length);
+                               (!["idle","chat"].includes(this.mode) || this.cursor==this.vr.moves.length);
                        const gameDiv = h('div',
                                {
                                        'class': { 'game': true },
                                },
                                [_.range(sizeX).map(i => {
-                                       let ci = this.mycolor=='w' ? i : sizeX-i-1;
+                                       let ci = (this.mycolor=='w' ? i : sizeX-i-1);
                                        return h(
                                                'div',
                                                {
@@ -227,7 +260,7 @@ Vue.component('my-game', {
                                                        style: { 'opacity': this.choices.length>0?"0.5":"1" },
                                                },
                                                _.range(sizeY).map(j => {
-                                                       let cj = this.mycolor=='w' ? j : sizeY-j-1;
+                                                       let cj = (this.mycolor=='w' ? j : sizeY-j-1);
                                                        let elems = [];
                                                        if (this.vr.board[ci][cj] != VariantRules.EMPTY)
                                                        {
@@ -286,7 +319,7 @@ Vue.component('my-game', {
                                        );
                                }), choices]
                        );
-                       if (this.mode != "idle")
+                       if (!["idle","chat"].includes(this.mode))
                        {
                                actionArray.push(
                                        h('button',
@@ -326,7 +359,7 @@ Vue.component('my-game', {
                                        ]
                                );
                        }
-                       if (this.mode == "friend")
+                       if (["friend","problem"].includes(this.mode))
                        {
                                actionArray = actionArray.concat(
                                [
@@ -605,6 +638,27 @@ Vue.component('my-game', {
                                                          { },
                                                                [
                                                                        //h('legend', { domProps: { innerHTML: "Legend title" } }),
+                                                                       h('label',
+                                                                               {
+                                                                                       attrs: { for: "nameSetter" },
+                                                                                       domProps: { innerHTML: "My name is..." },
+                                                                               },
+                                                                       ),
+                                                                       h('input',
+                                                                               {
+                                                                                       attrs: {
+                                                                                               "id": "nameSetter",
+                                                                                               type: "text",
+                                                                                               value: this.myname,
+                                                                                       },
+                                                                                       on: { "change": this.setMyname },
+                                                                               }
+                                                                       ),
+                                                               ]
+                                                       ),
+                                                       h('fieldset',
+                                                         { },
+                                                               [
                                                                        h('label',
                                                                                {
                                                                                        attrs: { for: "setHints" },
@@ -721,6 +775,75 @@ Vue.component('my-game', {
                        )
                ];
                elementArray = elementArray.concat(modalSettings);
+               let chatEltsArray =
+               [
+                       h('label',
+                               {
+                                       attrs: { "id": "close-chat", "for": "modal-chat" },
+                                       "class": { "modal-close": true },
+                               }
+                       ),
+                       h('h3',
+                               {
+                                       attrs: { "id": "titleChat" },
+                                       "class": { "section": true },
+                                       domProps: { innerHTML: "Chat with " + this.oppName },
+                               }
+                       )
+               ];
+               for (let chat of this.chats)
+               {
+                       chatEltsArray.push(
+                               h('p',
+                                       {
+                                               "class": {
+                                                       "my-chatmsg": chat.author==this.myid,
+                                                       "opp-chatmsg": chat.author==this.oppid,
+                                               },
+                                               domProps: { innerHTML: chat.msg }
+                                       }
+                               )
+                       );
+               }
+               chatEltsArray = chatEltsArray.concat([
+                       h('input',
+                               {
+                                       attrs: {
+                                               "id": "input-chat",
+                                               type: "text",
+                                               placeholder: "Type here",
+                                       },
+                                       on: { keyup: this.trySendChat }, //if key is 'enter'
+                               }
+                       ),
+                       h('button',
+                               {
+                                       on: { click: this.sendChat },
+                                       domProps: { innerHTML: "Send" },
+                               }
+                       )
+               ]);
+               const modalChat = [
+                       h('input',
+                               {
+                                       attrs: { "id": "modal-chat", type: "checkbox" },
+                                       "class": { "modal": true },
+                               }),
+                       h('div',
+                               {
+                                       attrs: { "role": "dialog", "aria-labelledby": "titleChat" },
+                               },
+                               [
+                                       h('div',
+                                               {
+                                                       "class": { "card": true, "smallpad": true },
+                                               },
+                                               chatEltsArray
+                                       )
+                               ]
+                       )
+               ];
+               elementArray = elementArray.concat(modalChat);
                const actions = h('div',
                        {
                                attrs: { "id": "actions" },
@@ -778,7 +901,7 @@ Vue.component('my-game', {
                                                        h('h3',
                                                                {
                                                                        domProps: { innerHTML: "Show solution" },
-                                                                       on: { click: "toggleShowSolution" }
+                                                                       on: { click: this.toggleShowSolution },
                                                                }
                                                        ),
                                                        h('p',
@@ -879,6 +1002,14 @@ Vue.component('my-game', {
                        const data = JSON.parse(msg.data);
                        switch (data.code)
                        {
+                               case "oppname":
+                                       // Receive opponent's name
+                                       this.oppName = data.name;
+                                       break;
+                               case "newchat":
+                                       // Receive new chat
+                                       this.chats.push({msg:data.msg, author:this.oppid});
+                                       break;
                                case "duplicate":
                                        // We opened another tab on the same game
                                        this.mode = "idle";
@@ -939,8 +1070,14 @@ Vue.component('my-game', {
                                // TODO: also use (dis)connect info to count online players?
                                case "connect":
                                case "disconnect":
-                                       if (this.mode == "human" && this.oppid == data.id)
+                                       if (["human","chat"].includes(this.mode) && this.oppid == data.id)
                                                this.oppConnected = (data.code == "connect");
+                                       if (this.oppConnected)
+                                       {
+                                               // Send our name to the opponent, in case of he hasn't it
+                                               this.conn.send(JSON.stringify({
+                                                       code:"myname", name:this.myname, oppid: this.oppid}));
+                                       }
                                        break;
                        }
                };
@@ -955,8 +1092,8 @@ Vue.component('my-game', {
                this.conn.onclose = socketCloseListener;
                // Listen to keyboard left/right to navigate in game
                document.onkeydown = event => {
-                       if (this.mode == "idle" && !!this.vr && this.vr.moves.length > 0
-                               && [37,39].includes(event.keyCode))
+                       if (["idle","chat"].includes(this.mode) &&
+                               !!this.vr && this.vr.moves.length > 0 && [37,39].includes(event.keyCode))
                        {
                                event.preventDefault();
                                if (event.keyCode == 37) //Back
@@ -965,13 +1102,43 @@ Vue.component('my-game', {
                                        this.play();
                        }
                };
+               // Computer moves web worker logic:
+               this.compWorker.postMessage(["scripts",variant]);
+               const self = this;
+               this.compWorker.onmessage = function(e) {
+                       const compMove = e.data;
+                       // (first move) HACK: small delay to avoid selecting elements
+                       // before they appear on page:
+                       const delay = Math.max(500-(Date.now()-self.timeStart), 0);
+                       setTimeout(() => {
+                               if (self.mode == "computer") //Warning: mode could have changed!
+                                       self.play(compMove, "animate")
+                       }, delay);
+               }
        },
        methods: {
+               setMyname: function(e) {
+                       this.myname = e.target.value;
+                       setCookie("username",this.myname);
+               },
+               trySendChat: function(e) {
+                       if (e.keyCode == 13) //'enter' key
+                               this.sendChat();
+               },
+               sendChat: function() {
+                       let chatInput = document.getElementById("input-chat");
+                       const chatTxt = chatInput.value;
+                       chatInput.value = "";
+                       this.chats.push({msg:chatTxt, author:this.myid});
+                       this.conn.send(JSON.stringify({
+                               code:"newchat", oppid: this.oppid, msg: chatTxt}));
+               },
                toggleShowSolution: function() {
                        let problemSolution = document.getElementById("problem-solution");
-                       problemSolution.style.display = problemSolution.style.display == "none"
-                               ? "block"
-                               : "none";
+                       problemSolution.style.display =
+                               !problemSolution.style.display || problemSolution.style.display == "none"
+                                       ? "block"
+                                       : "none";
                },
                download: function() {
                        let content = document.getElementById("pgn-game").innerHTML;
@@ -995,9 +1162,16 @@ Vue.component('my-game', {
                        this.pgnTxt = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode);
                        if (["human","computer"].includes(this.mode))
                                this.clearStorage();
-                       this.mode = "idle";
+                       if (this.mode == "human" && this.oppConnected)
+                       {
+                               // Send our nickname to opponent
+                               this.conn.send(JSON.stringify({
+                                       code:"myname", name:this.myname, oppid:this.oppid}));
+                       }
+                       this.mode = (this.mode=="human" ? "chat" : "idle");
                        this.cursor = this.vr.moves.length; //to navigate in finished game
-                       this.oppid = "";
+                       if (this.mode == "idle") //keep oppid in case of chat after human game
+                               this.oppid = "";
                },
                setStorage: function() {
                        if (this.mode=="human")
@@ -1066,8 +1240,6 @@ Vue.component('my-game', {
                },
                clickComputerGame: function(e) {
                        this.getRidOfTooltip(e.currentTarget);
-                       if (this.mode == "human")
-                               return; //no newgame while playing
                        this.newGame("computer");
                },
                clickFriendGame: function(e) {
@@ -1118,11 +1290,6 @@ Vue.component('my-game', {
                                        }
                                }
                        }
-                       if (this.mode == "computer" && mode == "human")
-                       {
-                               // Save current computer game to resume it later
-                               this.setStorage();
-                       }
                        this.vr = new VariantRules(fen, moves || []);
                        this.score = "*";
                        this.pgnTxt = ""; //redundant with this.score = "*", but cleaner
@@ -1142,7 +1309,7 @@ Vue.component('my-game', {
                                this.fenStart = localStorage.getItem(prefix+"fenStart");
                        }
                        else
-                               this.fenStart = fen;
+                               this.fenStart = V.ParseFen(fen).position; //this is enough
                        if (mode=="human")
                        {
                                // Opponent found!
@@ -1151,30 +1318,27 @@ Vue.component('my-game', {
                                        if (this.sound >= 1)
                                                new Audio("/sounds/newgame.mp3").play().catch(err => {});
                                        document.getElementById("modal-newgame").checked = false;
+                                       this.setStorage(); //in case of interruptions
                                }
                                this.oppid = oppId;
                                this.oppConnected = !continuation;
                                this.mycolor = color;
                                this.seek = false;
-                               this.setStorage(); //in case of interruptions
                        }
                        else if (mode == "computer")
                        {
-                               this.mycolor = Math.random() < 0.5 ? 'w' : 'b';
-                               if (this.mycolor == 'b')
-                                       setTimeout(this.playComputerMove, 100); //small delay for drawing board
+                               this.compWorker.postMessage(["init",this.vr.getFen()]);
+                               this.mycolor = color || (Math.random() < 0.5 ? 'w' : 'b');
+                               if (!continuation)
+                                       this.setStorage(); //store game state
+                               if (this.mycolor != this.vr.turn)
+                                       this.playComputerMove();
                        }
                        //else: against a (IRL) friend or problem solving: nothing more to do
                },
                playComputerMove: function() {
-                       const timeStart = Date.now();
-                       const compMove = this.vr.getComputerMove();
-                       // (first move) HACK: avoid selecting elements before they appear on page:
-                       const delay = Math.max(250-(Date.now()-timeStart), 0);
-                       setTimeout(() => {
-                               if (this.mode == "computer") //Warning: mode could have changed!
-                                       this.play(compMove, "animate")
-                       }, delay);
+                       this.timeStart = Date.now();
+                       this.compWorker.postMessage(["askmove"]);
                },
                // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
                getSquareId: function(o) {
@@ -1218,7 +1382,7 @@ Vue.component('my-game', {
                                this.selectedPiece.style.zIndex = 3000;
                                const startSquare = this.getSquareFromId(e.target.parentNode.id);
                                this.possibleMoves = [];
-                               if (this.mode != "idle")
+                               if (!["idle","chat"].includes(this.mode))
                                {
                                        const color = ["friend","problem"].includes(this.mode)
                                                ? this.vr.turn
@@ -1332,10 +1496,15 @@ Vue.component('my-game', {
                                this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
                        if (this.sound == 2)
                                new Audio("/sounds/chessmove1.mp3").play().catch(err => {});
-                       if (this.mode != "idle")
+                       if (!["idle","chat"].includes(this.mode))
                        {
                                this.incheck = this.vr.getCheckSquares(move); //is opponent in check?
                                this.vr.play(move, "ingame");
+                               if (this.mode == "computer")
+                               {
+                                       // Send the move to web worker
+                                       this.compWorker.postMessage(["newmove",move]);
+                               }
                        }
                        else
                        {
@@ -1344,7 +1513,7 @@ Vue.component('my-game', {
                        }
                        if (["human","computer"].includes(this.mode))
                                this.updateStorage(); //after our moves and opponent moves
-                       if (this.mode != "idle")
+                       if (!["idle","chat"].includes(this.mode))
                        {
                                const eog = this.vr.checkGameOver();
                                if (eog != "*")
@@ -1360,7 +1529,7 @@ Vue.component('my-game', {
                                }
                        }
                        if (this.mode == "computer" && this.vr.turn != this.mycolor)
-                               setTimeout(this.playComputerMove, 250); //small delay for animation
+                               this.playComputerMove();
                },
                undo: function() {
                        // Navigate after game is over
@@ -1375,7 +1544,18 @@ Vue.component('my-game', {
                undoInGame: function() {
                        const lm = this.vr.lastMove;
                        if (!!lm)
+                       {
                                this.vr.undo(lm);
+                               const lmBefore = this.vr.lastMove;
+                               if (!!lmBefore)
+                               {
+                                       this.vr.undo(lmBefore);
+                                       this.incheck = this.vr.getCheckSquares(lmBefore);
+                                       this.vr.play(lmBefore, "ingame");
+                               }
+                               else
+                                       this.incheck = [];
+                       }
                },
        },
 })