Save current state (unmerged, broken, not working...)
[vchess.git] / public / javascripts / components / game.js
index 34b3633..297ccc9 100644 (file)
-// TODO: use indexedDB instead of localStorage: more flexible.
-
+// Game logic on a variant page
 Vue.component('my-game', {
+       props: ["gameId"], //to find the game in storage (assumption: it exists)
        data: function() {
                return {
-                       vr: null, //object to check moves, store them, FEN..
-                       mycolor: "w",
-                       possibleMoves: [], //filled after each valid click/dragstart
-                       choices: [], //promotion pieces, or checkered captures... (contain possible pieces)
-                       start: {}, //pixels coordinates + id of starting square (click or drag)
-                       selectedPiece: null, //moving piece (or clicked piece)
-                       conn: null, //socket messages
-                       endofgame: "", //end of game message
-                       mode: "idle", //human, computer or idle (when not playing)
+                       
+                       // TODO: merge next variables into "game"
+                       // if oppid == "computer" then mode = "computer" (otherwise human)
+                       myid: "", //our ID, always set
+               //this.myid = localStorage.getItem("myid")
                        oppid: "", //opponent ID in case of HH game
+                       score: "*", //'*' means 'unfinished'
+                       mycolor: "w",
+                       fromChallenge: false, //if true, show chat during game
+                       
+                       conn: null, //socket connection
                        oppConnected: false,
+                       seek: false,
+                       fenStart: "",
+                       pgnTxt: "",
+                       // sound level: 0 = no sound, 1 = sound only on newgame, 2 = always
+                       sound: parseInt(localStorage["sound"] || "2"),
+                       // Web worker to play computer moves without freezing interface:
+                       compWorker: new Worker('/javascripts/playCompMove.js'),
+                       timeStart: undefined, //time when computer starts thinking
                };
        },
-       render(h) {
-               let [sizeX,sizeY] = VariantRules.size;
-               // Precompute hints squares to facilitate rendering
-               let hintSquares = doubleArray(sizeX, sizeY, false);
-               this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
-               let elementArray = [];
-               let square00 = document.getElementById("sq-0-0");
-               let squareWidth = !!square00
-                       ? parseFloat(window.getComputedStyle(square00).width.slice(0,-2))
-                       : 0;
-               let actionArray = [
-                       h('button',
-                               {
-                                       on: {
-                                               click: () => {
-                                                       if (localStorage.getItem("newgame") === variant)
-                                                               delete localStorage["newgame"]; //cancel game seek
-                                                       else
-                                                       {
-                                                               localStorage["newgame"] = variant;
-                                                               this.newGame("human");
-                                                       }
-                                               }
-                                       },
-                                       attrs: { "aria-label": 'New game VS human' },
-                                       'class': { "tooltip":true },
-                               },
-                               [h('i', { 'class': { "material-icons": true } }, "accessibility")]),
-                       h('button',
-                               {
-                                       on: { click: () => this.newGame("computer") },
-                                       attrs: { "aria-label": 'New game VS computer' },
-                                       'class': { "tooltip":true },
-                               },
-                               [h('i', { 'class': { "material-icons": true } }, "computer")])
-               ];
-               if (!!this.vr)
-               {
-                       if (this.mode == "human")
+       computed: {
+               mode: function() {
+                       return (this.game.oppid == "computer" ? "computer" ? "human");
+               },
+               showChat: function() {
+                       return this.mode=='human' &&
+                               (this.game.score != '*' || this.game.fromChallenge);
+               },
+               showMoves: function() {
+                       return window.innerWidth >= 768;
+               },
+       },
+       // Modal end of game, and then sub-components
+       template: `
+               <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
+                       <input id="modal-eog" type="checkbox" class="modal"/>
+                       <div role="dialog" aria-labelledby="eogMessage">
+                               <div class="card smallpad small-modal text-center">
+                                       <label for="modal-eog" class="modal-close"></label>
+                                       <h3 id="eogMessage" class="section">{{ endgameMessage }}</h3>
+
+                       <my-chat v-if="showChat"></my-chat>
+                       //TODO: connection + turn indicators en haut à droite (superposé au menu)
+                       <my-board></my-board>
+                       // TODO: controls: abort, clear, resign, draw (avec confirm box)
+                       // et si partie terminée : (mode analyse) just clear, back / play
+                       // + flip button toujours disponible
+                       
+                       <div id="pgn-div" class="section-content">
+                               <a id="download" href: "#"></a>
+                               <button id="downloadBtn" @click="download">
+                                       {{ translations["Download PGN"] }}
+                               </button>
+                       
+                       <my-move-list v-if="showMoves"></my-move-list>
+               </div>
+       `,
+       computed: {
+               endgameMessage: function() {
+                       let eogMessage = "Unfinished";
+                       switch (this.game.score)
                        {
-                               let connectedIndic = h(
-                                       'div',
-                                       {
-                                               "class": {
-                                                       "connected": this.oppConnected,
-                                                       "disconnected": !this.oppConnected,
-                                               },
-                                       }
-                               );
-                               elementArray.push(connectedIndic);
+                               case "1-0":
+                                       eogMessage = translations["White win"];
+                                       break;
+                               case "0-1":
+                                       eogMessage = translations["Black win"];
+                                       break;
+                               case "1/2":
+                                       eogMessage = translations["Draw"];
+                                       break;
                        }
-                       let choices = h('div',
-                               {
-                                       attrs: { "id": "choices" },
-                                       'class': { 'row': true },
-                                       style: {
-                                               //"position": "relative",
-                                               "display": this.choices.length>0?"block":"none",
-                                               "top": "-" + ((sizeY/2)*squareWidth+squareWidth/2) + "px",
-                                               "width": (this.choices.length * squareWidth) + "px",
-                                               "height": squareWidth + "px",
-                                       },
-                               },
-                               this.choices.map( m => { //a "choice" is a move
-                                       return h('div',
-                                               {
-                                                       'class': { 'board': true },
-                                                       style: {
-                                                               'width': (100/this.choices.length) + "%",
-                                                               'padding-bottom': (100/this.choices.length) + "%",
-                                                       },
-                                               },
-                                               [h('img',
-                                                       {
-                                                               attrs: { "src": '/images/pieces/' + VariantRules.getPpath(m.appear[0].c+m.appear[0].p) + '.svg' },
-                                                               'class': { 'choice-piece': true, 'board': true },
-                                                               on: { "click": e => { this.play(m); this.choices=[]; } },
-                                                       })
-                                               ]
-                                       );
-                               })
-                       );
-                       // Create board element (+ reserves if needed by variant or mode)
-                       let gameDiv = h('div',
-                               {
-                                       'class': { 'game': true },
-                               },
-                               [_.range(sizeX).map(i => {
-                                       let ci = this.mycolor=='w' ? i : sizeX-i-1;
-                                       return h(
-                                               'div',
-                                               {
-                                                       'class': {
-                                                               'row': true,
-                                                       },
-                                                       style: { 'opacity': this.choices.length>0?"0.5":"1" },
-                                               },
-                                               _.range(sizeY).map(j => {
-                                                       let cj = this.mycolor=='w' ? j : sizeY-j-1;
-                                                       let elems = [];
-                                                       if (this.vr.board[ci][cj] != VariantRules.EMPTY)
-                                                       {
-                                                               elems.push(
-                                                                       h(
-                                                                               'img',
-                                                                               {
-                                                                                       'class': {
-                                                                                               'piece': true,
-                                                                                               'ghost': !!this.selectedPiece && this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
-                                                                                       },
-                                                                                       attrs: {
-                                                                                               src: "/images/pieces/" + VariantRules.getPpath(this.vr.board[ci][cj]) + ".svg",
-                                                                                       },
-                                                                               }
-                                                                       )
-                                                               );
-                                                       }
-                                                       if (hintSquares[ci][cj])
-                                                       {
-                                                               elems.push(
-                                                                       h(
-                                                                               'img',
-                                                                               {
-                                                                                       'class': {
-                                                                                               'mark-square': true,
-                                                                                       },
-                                                                                       attrs: {
-                                                                                               src: "/images/mark.svg",
-                                                                                       },
-                                                                               }
-                                                                       )
-                                                               );
-                                                       }
-                                                       const lm = this.vr.lastMove;
-                                                       const highlight = !!lm && _.isMatch(lm.end, {x:ci,y:cj}); //&& _.isMatch(lm.start, {x:ci,y:cj})
-                                                       return h(
-                                                               'div',
-                                                               {
-                                                                       'class': {
-                                                                               'board': true,
-                                                                               'light-square': !highlight && (i+j)%2==0,
-                                                                               'dark-square': !highlight && (i+j)%2==1,
-                                                                               'highlight': highlight,
-                                                                       },
-                                                                       attrs: {
-                                                                               id: this.getSquareId({x:ci,y:cj}),
-                                                                       },
-                                                               },
-                                                               elems
-                                                       );
-                                               })
-                                       );
-                               }), choices]
-                       );
-                       actionArray.push(
-                               h('button',
-                                       {
-                                               on: { click: this.resign },
-                                               attrs: { "aria-label": 'Resign' },
-                                               'class': { "tooltip":true },
-                                       },
-                                       [h('i', { 'class': { "material-icons": true } }, "flag")])
-                       );
-                       elementArray.push(gameDiv);
-       //                      if (!!vr.reserve)
-       //                      {
-       //                              let reserve = h('div',
-       //                                      {'class':{'game':true}}, [
-       //                                              h('div',
-       //                                                      { 'class': { 'row': true }},
-       //                                                      [
-       //                                                              h('div',
-       //                                                                      {'class':{'board':true}},
-       //                                                                      [h('img',{'class':{"piece":true},attrs:{"src":"/images/pieces/wb.svg"}})]
-       //                                                              )
-       //                                                      ]
-       //                                              )
-       //                                      ],
-       //                              );
-       //                              elementArray.push(reserve);
-       //                      }
-                       const modalEog = [
-                               h('input',
-                                       {
-                                               attrs: { "id": "modal-control", type: "checkbox" },
-                                               "class": { "modal": true },
-                                       }),
-                               h('div',
-                                       {
-                                               attrs: { "role": "dialog", "aria-labelledby": "dialog-title" },
-                                       },
-                                       [
-                                               h('div',
-                                                       {
-                                                               "class": { "card": true, "smallpad": true },
-                                                       },
-                                                       [
-                                                               h('label',
-                                                                       {
-                                                                               attrs: { "for": "modal-control" },
-                                                                               "class": { "modal-close": true },
-                                                                       }
-                                                               ),
-                                                               h('h3',
-                                                                       {
-                                                                               "class": { "section": true },
-                                                                               domProps: { innerHTML: "End of game" },
-                                                                       }
-                                                               ),
-                                                               h('p',
-                                                                       {
-                                                                               "class": { "section": true },
-                                                                               domProps: { innerHTML: this.endofgame },
-                                                                       }
-                                                               )
-                                                       ]
-                                               )
-                                       ]
-                               )
-                       ];
-                       elementArray = elementArray.concat(modalEog);
-               }
-               const modalNewgame = [
-                       h('input',
-                               {
-                                       attrs: { "id": "modal-control2", type: "checkbox" },
-                                       "class": { "modal": true },
-                               }),
-                       h('div',
-                               {
-                                       attrs: { "role": "dialog", "aria-labelledby": "dialog-title" },
-                               },
-                               [
-                                       h('div',
-                                               {
-                                                       "class": { "card": true, "smallpad": true },
-                                               },
-                                               [
-                                                       h('label',
-                                                               {
-                                                                       attrs: { "id": "close-newgame", "for": "modal-control2" },
-                                                                       "class": { "modal-close": true },
-                                                               }
-                                                       ),
-                                                       h('h3',
-                                                               {
-                                                                       "class": { "section": true },
-                                                                       domProps: { innerHTML: "New game" },
-                                                               }
-                                                       ),
-                                                       h('p',
-                                                               {
-                                                                       "class": { "section": true },
-                                                                       domProps: { innerHTML: "Waiting for opponent..." },
-                                                               }
-                                                       )
-                                               ]
-                                       )
-                               ]
-                       )
-               ];
-               elementArray = elementArray.concat(modalNewgame);
-               const actions = h('div',
-                       {
-                               attrs: { "id": "actions" },
-                               'class': { 'text-center': true },
-                       },
-                       actionArray
-               );
-               elementArray.push(actions);
-               return h(
-                       'div',
-                       {
-                               'class': {
-                                       "col-sm-12":true,
-                                       "col-md-8":true,
-                                       "col-md-offset-2":true,
-                                       "col-lg-6":true,
-                                       "col-lg-offset-3":true,
-                               },
-                               // NOTE: click = mousedown + mouseup --> what about smartphone?!
-                               on: {
-                                       mousedown: this.mousedown,
-                                       mousemove: this.mousemove,
-                                       mouseup: this.mouseup,
-                                       touchdown: this.mousedown,
-                                       touchmove: this.mousemove,
-                                       touchup: this.mouseup,
-                               },
-                       },
-                       elementArray
-               );
+                       return eogMessage;
+               },
        },
        created: function() {
                const url = socketUrl;
-               const continuation = (localStorage.getItem("variant") === variant);
-               this.myid = continuation
-                       ? localStorage.getItem("myid")
-                       // random enough (TODO: function)
-                       : (Date.now().toString(36) + Math.random().toString(36).substr(2, 7)).toUpperCase();
-               this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
-               const socketOpenListener = () => {
-                       if (continuation)
-                       {
-                               // TODO: check FEN integrity with opponent
-                               this.newGame("human", localStorage.getItem("fen"),
-                                       localStorage.getItem("mycolor"), localStorage.getItem("oppid"), true);
-                               // Send ping to server, which answers pong if opponent is connected
-                               this.conn.send(JSON.stringify({code:"ping", oppid:this.oppId}));
-                       }
-                       else if (localStorage.getItem("newgame") === variant)
-                       {
-                               // New game request has been cancelled on disconnect
-                               this.newGame("human");
-                       }
-               };
+               this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant._id);
+//             const socketOpenListener = () => {
+//             };
+
+// TODO: after game, archive in indexedDB
+
+               // TODO: this events listener is central. Refactor ? How ?
                const socketMessageListener = msg => {
                        const data = JSON.parse(msg.data);
+                       let L = undefined;
                        switch (data.code)
                        {
-                               case "newgame": //opponent found
-                                       this.newGame("human", data.fen, data.color, data.oppid); //oppid: opponent socket ID
-                                       break;
                                case "newmove": //..he played!
-                                       this.play(data.move, "animate");
+                                       this.play(data.move, (variant.name!="Dark" ? "animate" : null));
                                        break;
-                               case "pong": //sent when opponent stayed online after we disconnected
+                               case "pong": //received if we sent a ping (game still alive on our side)
+                                       if (this.gameId != data.gameId)
+                                               break; //games IDs don't match: definitely over...
                                        this.oppConnected = true;
+                                       // Send our "last state" informations to opponent
+                                       L = this.vr.moves.length;
+                                       this.conn.send(JSON.stringify({
+                                               code: "lastate",
+                                               oppid: this.oppid,
+                                               gameId: this.gameId,
+                                               lastMove: (L>0?this.vr.moves[L-1]:undefined),
+                                               movesCount: L,
+                                       }));
+                                       break;
+                               case "lastate": //got opponent infos about last move
+                                       L = this.vr.moves.length;
+                                       if (this.gameId != data.gameId)
+                                               break; //games IDs don't match: nothing we can do...
+                                       // OK, opponent still in game (which might be over)
+                                       if (this.score != "*")
+                                       {
+                                               // We finished the game (any result possible)
+                                               this.conn.send(JSON.stringify({
+                                                       code: "lastate",
+                                                       oppid: data.oppid,
+                                                       gameId: this.gameId,
+                                                       score: this.score,
+                                               }));
+                                       }
+                                       else if (!!data.score) //opponent finished the game
+                                               this.endGame(data.score);
+                                       else if (data.movesCount < L)
+                                       {
+                                               // We must tell last move to opponent
+                                               this.conn.send(JSON.stringify({
+                                                       code: "lastate",
+                                                       oppid: this.oppid,
+                                                       gameId: this.gameId,
+                                                       lastMove: this.vr.moves[L-1],
+                                                       movesCount: L,
+                                               }));
+                                       }
+                                       else if (data.movesCount > L) //just got last move from him
+                                               this.play(data.lastMove, "animate");
                                        break;
                                case "resign": //..you won!
-                                       this.endGame("Victory!");
+                                       this.endGame(this.mycolor=="w"?"1-0":"0-1");
                                        break;
-                               // TODO: also use (dis)connect info to count online players
+                               // TODO: also use (dis)connect info to count online players?
                                case "connect":
                                case "disconnect":
-                                       if (this.mode == "human" && this.oppid == data.id)
+                                       if (this.mode=="human" && this.oppid == data.id)
                                                this.oppConnected = (data.code == "connect");
+                                       if (this.oppConnected && this.score != "*")
+                                       {
+                                               // 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;
                        }
                };
+
                const socketCloseListener = () => {
-                       console.log("Lost connection -- reconnect");
-                       this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
-                       this.conn.addEventListener('open', socketOpenListener);
+                       this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant._id);
+                       //this.conn.addEventListener('open', socketOpenListener);
                        this.conn.addEventListener('message', socketMessageListener);
                        this.conn.addEventListener('close', socketCloseListener);
                };
-               this.conn.onopen = socketOpenListener;
+               //this.conn.onopen = socketOpenListener;
                this.conn.onmessage = socketMessageListener;
                this.conn.onclose = socketCloseListener;
+               
+               
+               // Listen to keyboard left/right to navigate in game
+               // TODO: also mouse wheel !
+               document.onkeydown = event => {
+                       if (["human","computer"].includes(this.mode) &&
+                               !!this.vr && this.vr.moves.length > 0 && [37,39].includes(event.keyCode))
+                       {
+                               event.preventDefault();
+                               if (event.keyCode == 37) //Back
+                                       this.undo();
+                               else //Forward (39)
+                                       this.play();
+                       }
+               };
+
+
+               // Computer moves web worker logic: (TODO: also for observers in HH games)
+               this.compWorker.postMessage(["scripts",variant.name]);
+               const self = this;
+               this.compWorker.onmessage = function(e) {
+                       let compMove = e.data;
+                       if (!compMove)
+                               return; //may happen if MarseilleRules and subTurn==2 (TODO: a bit ugly...)
+                       if (!Array.isArray(compMove))
+                               compMove = [compMove]; //to deal with MarseilleRules
+                       // TODO: imperfect attempt to avoid ghost move:
+                       compMove.forEach(m => { m.computer = true; });
+                       // (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(() => {
+                               const animate = (variant.name!="Dark" ? "animate" : null);
+                               if (self.mode == "computer") //warning: mode could have changed!
+                                       self.play(compMove[0], animate);
+                               if (compMove.length == 2)
+                                       setTimeout( () => {
+                                               if (self.mode == "computer")
+                                                       self.play(compMove[1], animate);
+                                       }, 750);
+                       }, delay);
+               }
        },
+
+
        methods: {
-               endGame: function(message) {
-                       this.endofgame = message;
-                       document.getElementById("modal-control").checked = true;
-                       if (this.mode == "human")
-                               this.clearStorage();
-                       this.mode = "idle";
-                       this.oppid = "";
+               download: function() {
+                       // Variants may have special PGN structure (so next function isn't defined here)
+                       const content = V.GetPGN(this.moves, this.mycolor, this.score, this.fenStart, this.mode);
+                       // Prepare and trigger download link
+                       let downloadAnchor = document.getElementById("download");
+                       downloadAnchor.setAttribute("download", "game.pgn");
+                       downloadAnchor.href = "data:text/plain;charset=utf-8," + encodeURIComponent(content);
+                       downloadAnchor.click();
                },
-               resign: function() {
-                       if (this.mode == "human" && this.oppConnected)
+               showScoreMsg: function() {
+                       let modalBox = document.getElementById("modal-eog");
+                       modalBox.checked = true;
+                       setTimeout(() => { modalBox.checked = false; }, 2000);
+               },
+               endGame: function(score) {
+                       this.score = score;
+                       if (["human","computer"].includes(this.mode))
                        {
-                               try {
-                                       this.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
-                               } catch (INVALID_STATE_ERR) {
-                                       return; //resign failed for some reason...
-                               }
+                               const prefix = (this.mode=="computer" ? "comp-" : "");
+                               localStorage.setItem(prefix+"score", score);
                        }
-                       this.endGame("Try again!");
-               },
-               updateStorage: function() {
-                       if (!localStorage.getItem("myid"))
+                       this.showScoreMsg();
+                       if (this.mode == "human" && this.oppConnected)
                        {
-                               localStorage.setItem("myid", this.myid);
-                               localStorage.setItem("variant", variant);
-                               localStorage.setItem("mycolor", this.mycolor);
-                               localStorage.setItem("oppid", this.oppid);
+                               // Send our nickname to opponent
+                               this.conn.send(JSON.stringify({
+                                       code:"myname", name:this.myname, oppid:this.oppid}));
                        }
-                       localStorage.setItem("fen", this.vr.getFen());
-               },
-               clearStorage: function() {
-                       delete localStorage["variant"];
-                       delete localStorage["myid"];
-                       delete localStorage["mycolor"];
-                       delete localStorage["oppid"];
-                       delete localStorage["fen"];
+                       this.cursor = this.vr.moves.length; //to navigate in finished game
                },
-               newGame: function(mode, fenInit, color, oppId, continuation) {
-                       const fen = fenInit || VariantRules.GenRandInitFen();
-                       console.log(fen); //DEBUG
-                       if (mode=="human" && !oppId)
+               resign: function(e) {
+                       this.getRidOfTooltip(e.currentTarget);
+                       if (this.mode == "human" && this.oppConnected)
                        {
-                               // Send game request and wait..
-                               this.clearStorage(); //in case of
                                try {
-                                       this.conn.send(JSON.stringify({code:"newgame", fen:fen}));
+                                       this.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
                                } catch (INVALID_STATE_ERR) {
-                                       return; //nothing achieved
-                               }
-                               document.getElementById("modal-control2").checked = true;
-                               return;
-                       }
-                       this.vr = new VariantRules(fen);
-                       this.mode = mode;
-                       if (mode=="human")
-                       {
-                               // Opponent found!
-                               if (!continuation)
-                               {
-                                       // Playing sound fails on game continuation:
-                                       new Audio("/sounds/newgame.mp3").play();
-                                       document.getElementById("modal-control2").checked = false;
+                                       return; //socket is not ready (and not yet reconnected)
                                }
-                               this.oppid = oppId;
-                               this.oppConnected = true;
-                               this.mycolor = color;
-                               delete localStorage["newgame"]; //in case of
-                       }
-                       else //against computer
-                       {
-                               this.mycolor = Math.random() < 0.5 ? 'w' : 'b';
-                               if (this.mycolor == 'b')
-                                       setTimeout(this.playComputerMove, 500);
                        }
+                       this.endGame(this.mycolor=="w"?"0-1":"1-0");
                },
                playComputerMove: function() {
-                       const compColor = this.mycolor=='w' ? 'b' : 'w';
-                       const compMove = this.vr.getComputerMove(compColor);
-                       // HACK: avoid selecting elements before they appear on page:
-                       setTimeout(() => this.play(compMove, "animate"), 500);
-               },
-               // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
-               getSquareId: function(o) {
-                       // NOTE: a separator is required to allow any size of board
-                       return  "sq-" + o.x + "-" + o.y;
-               },
-               // Inverse function
-               getSquareFromId: function(id) {
-                       let idParts = id.split('-');
-                       return [parseInt(idParts[1]), parseInt(idParts[2])];
-               },
-               mousedown: function(e) {
-                       e = e || window.event;
-                       e.preventDefault(); //disable native drag & drop
-                       if (!this.selectedPiece && e.target.classList.contains("piece"))
-                       {
-                               // Next few lines to center the piece on mouse cursor
-                               let rect = e.target.parentNode.getBoundingClientRect();
-                               this.start = {
-                                       x: rect.x + rect.width/2,
-                                       y: rect.y + rect.width/2,
-                                       id: e.target.parentNode.id
-                               };
-                               this.selectedPiece = e.target.cloneNode();
-                               this.selectedPiece.style.position = "absolute";
-                               this.selectedPiece.style.top = 0;
-                               this.selectedPiece.style.display = "inline-block";
-                               this.selectedPiece.style.zIndex = 3000;
-                               let startSquare = this.getSquareFromId(e.target.parentNode.id);
-                               this.possibleMoves = this.vr.canIplay(this.mycolor,startSquare)
-                                       ? this.vr.getPossibleMovesFrom(startSquare)
-                                       : [];
-                               e.target.parentNode.appendChild(this.selectedPiece);
-                       }
-               },
-               mousemove: function(e) {
-                       if (!this.selectedPiece)
-                               return;
-                       e = e || window.event;
-                       // If there is an active element, move it around
-                       if (!!this.selectedPiece)
-                       {
-                               this.selectedPiece.style.left = (e.clientX-this.start.x) + "px";
-                               this.selectedPiece.style.top = (e.clientY-this.start.y) + "px";
-                       }
-               },
-               mouseup: function(e) {
-                       if (!this.selectedPiece)
-                               return;
-                       e = e || window.event;
-                       // Read drop target (or parentElement, parentNode... if type == "img")
-                       this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coordinates
-                       let landing = document.elementFromPoint(e.clientX, e.clientY);
-                       this.selectedPiece.style.zIndex = 3000;
-                       while (landing.tagName == "IMG") //classList.contains(piece) fails because of mark/highlight
-                               landing = landing.parentNode;
-                       if (this.start.id == landing.id) //a click: selectedPiece and possibleMoves already filled
-                               return;
-                       // OK: process move attempt
-                       let endSquare = this.getSquareFromId(landing.id);
-                       let moves = this.findMatchingMoves(endSquare);
-                       this.possibleMoves = [];
-                       if (moves.length > 1)
-                               this.choices = moves;
-                       else if (moves.length==1)
-                               this.play(moves[0]);
-                       // Else: impossible move
-                       this.selectedPiece.parentNode.removeChild(this.selectedPiece);
-                       delete this.selectedPiece;
-                       this.selectedPiece = null;
-               },
-               findMatchingMoves: function(endSquare) {
-                       // Run through moves list and return the matching set (if promotions...)
-                       let moves = [];
-                       this.possibleMoves.forEach(function(m) {
-                               if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
-                                       moves.push(m);
-                       });
-                       return moves;
-               },
-               animateMove: function(move) {
-                       let startSquare = document.getElementById(this.getSquareId(move.start));
-                       let endSquare = document.getElementById(this.getSquareId(move.end));
-                       let rectStart = startSquare.getBoundingClientRect();
-                       let rectEnd = endSquare.getBoundingClientRect();
-                       let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
-                       let movingPiece = document.querySelector("#" + this.getSquareId(move.start) + " > img.piece");
-                       // HACK for animation (otherwise with positive translate, image slides "under background"...)
-                       // Possible improvement: just alter squares on the piece's way...
-                       squares = document.getElementsByClassName("board");
-                       for (let i=0; i<squares.length; i++)
-                       {
-                               let square = squares.item(i);
-                               if (square.id != this.getSquareId(move.start))
-                                       square.style.zIndex = "-1";
-                       }
-                       movingPiece.style.transform = "translate(" + translation.x + "px," + translation.y + "px)";
-                       movingPiece.style.transitionDuration = "0.2s";
-                       movingPiece.style.zIndex = "3000";
-                       setTimeout( () => {
-                               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);
-                       }, 200);
+                       this.timeStart = Date.now();
+                       this.compWorker.postMessage(["askmove"]);
                },
+               // OK, these last functions can stay here (?!)
                play: function(move, programmatic) {
-                       if (!!programmatic) //computer or human opponent
+                       if (!move)
                        {
-                               this.animateMove(move);
-                               return;
+                               // Navigate after game is over
+                               if (this.cursor >= this.moves.length)
+                                       return; //already at the end
+                               move = this.moves[this.cursor++];
                        }
+                       if (!!programmatic) //computer or human opponent
+                               return this.animateMove(move);
                        // Not programmatic, or animation is over
                        if (this.mode == "human" && this.vr.turn == this.mycolor)
+                               this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
+                       
+                       
+                       // TODO: play move, and stack it on this.moves (if a move was provided; otherwise just navigate)
+                       
+                       if (this.score == "*") //TODO: I don't like this if()
                        {
-                               if (!this.oppConnected)
-                                       return; //abort move if opponent is gone
-                               try {
-                                       this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
-                               } catch(INVALID_STATE_ERR) {
-                                       return; //abort also if sending failed
+                               // Emergency check, if human game started "at the same time"
+                               // TODO: robustify this...
+                               if (this.mode == "human" && !!move.computer)
+                                       return;
+                               this.vr.play(move, "ingame");
+                               // Is opponent in check?
+                               this.incheck = this.vr.getCheckSquares(this.vr.turn);
+                               if (this.sound == 2)
+                                       new Audio("/sounds/move.mp3").play().catch(err => {});
+                               if (this.mode == "computer")
+                               {
+                                       // Send the move to web worker (TODO: including his own moves?!)
+                                       this.compWorker.postMessage(["newmove",move]);
+                               }
+                               const eog = this.vr.getCurrentScore();
+                               if (eog != "*")
+                               {
+                                       if (["human","computer"].includes(this.mode))
+                                               this.endGame(eog);
+                                       else
+                                       {
+                                               // Just show score on screen (allow undo)
+                                               this.score = eog;
+                                               this.showScoreMsg();
+                                       }
                                }
                        }
-                       new Audio("/sounds/chessmove1.mp3").play();
-                       this.vr.play(move, "ingame");
-                       if (this.mode == "human")
+//                     else
+//                     {
+//                             VariantRules.PlayOnBoard(this.vr.board, move);
+//                             this.$forceUpdate(); //TODO: ?!
+//                     }
+                       if (["human","computer","friend"].includes(this.mode))
                                this.updateStorage(); //after our moves and opponent moves
-                       const eog = this.vr.checkGameOver(this.vr.turn);
-                       if (eog != "*")
-                               this.endGame(eog);
-                       else if (this.mode == "computer" && this.vr.turn != this.mycolor)
-                               setTimeout(this.playComputerMove, 500);
+                       if (this.mode == "computer" && this.vr.turn != this.mycolor && this.score == "*")
+                               this.playComputerMove();
+               },
+               // TODO: merge two next functions
+               undo: function() {
+                       // Navigate after game is over
+                       if (this.cursor == 0)
+                               return; //already at the beginning
+                       if (this.cursor == this.vr.moves.length)
+                               this.incheck = []; //in case of...
+                       const move = this.vr.moves[--this.cursor];
+                       VariantRules.UndoOnBoard(this.vr.board, move);
+                       this.$forceUpdate(); //TODO: ?!
+               },
+               undoInGame: function() {
+                       const lm = this.vr.lastMove;
+                       if (!!lm)
+                       {
+                               this.vr.undo(lm);
+                               if (this.sound == 2)
+                                       new Audio("/sounds/undo.mp3").play().catch(err => {});
+                               this.incheck = this.vr.getCheckSquares(this.vr.turn);
+                       }
                },
        },
 })
+
+//// TODO: keep moves list here
+//get lastMove()
+//     {
+//             const L = this.moves.length;
+//             return (L>0 ? this.moves[L-1] : null);
+//     }
+//
+//// here too:
+//                     move.notation = this.getNotation(move);
+//TODO: confirm dialog with "opponent offers draw", avec possible bouton "prevent future offers" + bouton "proposer nulle"
+//+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions,
+//comme sur lichess