X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fcomponents%2Fgame.js;h=0ab12d6271976d4e2154c1091965675866677a9a;hb=8ddc00a072c5a4aa679e5a420a7f9664d18e03f3;hp=7b238f5c2b701f35c4c11f4c7cac1d4a149da244;hpb=9234226104764b91df9d677fb360ad538b98510c;p=vchess.git diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index 7b238f5c..0ab12d62 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -1,5 +1,6 @@ // Game logic on a variant page Vue.component('my-game', { + props: ["problem"], data: function() { return { vr: null, //object to check moves, store them, FEN.. @@ -17,13 +18,24 @@ Vue.component('my-game', { fenStart: "", incheck: [], pgnTxt: "", - expert: (getCookie("expert") === "1" ? true : false), - gameId: "", //used to limit computer moves' time + hints: (getCookie("hints") === "1" ? true : false), + 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")), }; }, + watch: { + problem: function(p, pp) { + // 'problem' prop changed: update board state + // TODO: FEN + turn + flags + rappel instructions / solution on click sous l'échiquier + // TODO: trouver moyen de passer la situation des reserves pour Crazyhouse, + // et l'état des captures pour Grand... bref compléter le descriptif de l'état. + this.newGame("problem", p.fen, p.fen.split(" ")[2]); + }, + }, render(h) { - const [sizeX,sizeY] = VariantRules.size; - const smallScreen = (screen.width <= 420); + const [sizeX,sizeY] = [V.size.x,V.size.y]; + const smallScreen = (window.innerWidth <= 420); // Precompute hints squares to facilitate rendering let hintSquares = doubleArray(sizeX, sizeY, false); this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; }); @@ -87,7 +99,10 @@ Vue.component('my-game', { const squareWidth = !!square00 ? parseFloat(window.getComputedStyle(square00).width.slice(0,-2)) : 0; - const indicWidth = (squareWidth>0 ? squareWidth/2 : 20); + const settingsBtnElt = document.getElementById("settingsBtn"); + const indicWidth = !!settingsBtnElt //-2 for border: + ? parseFloat(window.getComputedStyle(settingsBtnElt).height.slice(0,-2)) - 2 + : (smallScreen ? 31 : 37); if (this.mode == "human") { let connectedIndic = h( @@ -123,23 +138,25 @@ Vue.component('my-game', { } ); elementArray.push(turnIndic); - let expertSwitch = h( + let settingsBtn = h( 'button', { - on: { click: this.toggleExpertMode }, - attrs: { "aria-label": 'Toggle expert mode' }, + on: { click: this.showSettings }, + attrs: { + "aria-label": 'Settings', + "id": "settingsBtn", + }, 'class': { - "tooltip":true, + "tooltip": true, "topindicator": true, "indic-right": true, - "expert-switch": true, - "expert-mode": this.expert, - "small": smallScreen, + "settings-btn": !smallScreen, + "settings-btn-small": smallScreen, }, }, - [h('i', { 'class': { "material-icons": true } }, "visibility_off")] + [h('i', { 'class': { "material-icons": true } }, "settings")] ); - elementArray.push(expertSwitch); + elementArray.push(settingsBtn); let choices = h('div', { attrs: { "id": "choices" }, @@ -175,6 +192,9 @@ 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); let gameDiv = h('div', { 'class': { 'game': true }, @@ -211,7 +231,7 @@ Vue.component('my-game', { ) ); } - if (!this.expert && hintSquares[ci][cj]) + if (this.hints && hintSquares[ci][cj]) { elems.push( h( @@ -227,9 +247,6 @@ Vue.component('my-game', { ) ); } - const lm = this.vr.lastMove; - const showLight = !this.expert && - (this.mode!="idle" || this.cursor==this.vr.moves.length); return h( 'div', { @@ -238,6 +255,7 @@ Vue.component('my-game', { ['board'+sizeY]: true, 'light-square': (i+j)%2==0, 'dark-square': (i+j)%2==1, + [this.color]: true, 'highlight': showLight && !!lm && _.isMatch(lm.end, {x:ci,y:cj}), 'incheck': showLight && incheckSq[ci][cj], }, @@ -401,7 +419,7 @@ Vue.component('my-game', { }), h('div', { - attrs: { "role": "dialog", "aria-labelledby": "modal-eog" }, + attrs: { "role": "dialog", "aria-labelledby": "eogMessage" }, }, [ h('div', @@ -417,6 +435,7 @@ Vue.component('my-game', { ), h('h3', { + attrs: { "id": "eogMessage" }, "class": { "section": true }, domProps: { innerHTML: eogMessage }, } @@ -428,6 +447,7 @@ Vue.component('my-game', { ]; elementArray = elementArray.concat(modalEog); } + // NOTE: this modal could be in Pug view (no usage of Vue functions or variables) const modalNewgame = [ h('input', { @@ -436,7 +456,7 @@ Vue.component('my-game', { }), h('div', { - attrs: { "role": "dialog", "aria-labelledby": "modal-newgame" }, + attrs: { "role": "dialog", "aria-labelledby": "newGameTxt" }, }, [ h('div', @@ -452,6 +472,7 @@ Vue.component('my-game', { ), h('h3', { + attrs: { "id": "newGameTxt" }, "class": { "section": true }, domProps: { innerHTML: "New game" }, } @@ -476,7 +497,7 @@ Vue.component('my-game', { }), h('div', { - attrs: { "role": "dialog", "aria-labelledby": "modal-fenedit" }, + attrs: { "role": "dialog", "aria-labelledby": "titleFenedit" }, }, [ h('div', @@ -492,6 +513,7 @@ Vue.component('my-game', { ), h('h3', { + attrs: { "id": "titleFenedit" }, "class": { "section": true }, domProps: { innerHTML: "Position + flags (FEN):" }, } @@ -534,6 +556,155 @@ Vue.component('my-game', { ) ]; elementArray = elementArray.concat(modalFenEdit); + const modalSettings = [ + h('input', + { + attrs: { "id": "modal-settings", type: "checkbox" }, + "class": { "modal": true }, + }), + h('div', + { + attrs: { "role": "dialog", "aria-labelledby": "settingsTitle" }, + }, + [ + h('div', + { + "class": { "card": true, "smallpad": true }, + }, + [ + h('label', + { + attrs: { "id": "close-settings", "for": "modal-settings" }, + "class": { "modal-close": true }, + } + ), + h('h3', + { + attrs: { "id": "settingsTitle" }, + "class": { "section": true }, + domProps: { innerHTML: "Preferences" }, + } + ), + h('fieldset', + { }, + [ + //h('legend', { domProps: { innerHTML: "Legend title" } }), + h('label', + { + attrs: { for: "setHints" }, + domProps: { innerHTML: "Show hints?" }, + }, + ), + h('input', + { + attrs: { + "id": "setHints", + type: "checkbox", + checked: this.hints, + }, + on: { "change": this.toggleHints }, + } + ), + ] + ), + h('fieldset', + { }, + [ + h('label', + { + attrs: { for: "selectColor" }, + domProps: { innerHTML: "Board colors" }, + }, + ), + h("select", + { + attrs: { "id": "selectColor" }, + on: { "change": this.setColor }, + }, + [ + h("option", + { + domProps: { + "value": "lichess", + innerHTML: "brown" + }, + attrs: { "selected": this.color=="lichess" }, + } + ), + h("option", + { + domProps: { + "value": "chesscom", + innerHTML: "green" + }, + attrs: { "selected": this.color=="chesscom" }, + } + ), + h("option", + { + domProps: { + "value": "chesstempo", + innerHTML: "blue" + }, + attrs: { "selected": this.color=="chesstempo" }, + } + ), + ], + ), + ] + ), + h('fieldset', + { }, + [ + h('label', + { + attrs: { for: "selectSound" }, + domProps: { innerHTML: "Play sounds?" }, + }, + ), + h("select", + { + attrs: { "id": "selectSound" }, + on: { "change": this.setSound }, + }, + [ + h("option", + { + domProps: { + "value": "0", + innerHTML: "None" + }, + attrs: { "selected": this.sound==0 }, + } + ), + h("option", + { + domProps: { + "value": "1", + innerHTML: "Newgame" + }, + attrs: { "selected": this.sound==1 }, + } + ), + h("option", + { + domProps: { + "value": "2", + innerHTML: "All" + }, + attrs: { "selected": this.sound==2 }, + } + ), + ], + ), + ] + ), + ] + ) + ] + ) + ]; + elementArray = elementArray.concat(modalSettings); const actions = h('div', { attrs: { "id": "actions" }, @@ -546,7 +717,10 @@ Vue.component('my-game', { { elementArray.push( h('div', - { attrs: { id: "pgn-div" } }, + { + attrs: { id: "pgn-div" }, + "class": { "section-content": true }, + }, [ h('a', { @@ -559,10 +733,16 @@ Vue.component('my-game', { h('p', { attrs: { id: "pgn-game" }, - on: { click: this.download }, domProps: { innerHTML: this.pgnTxt } } - ) + ), + h('button', + { + attrs: { "id": "downloadBtn" }, + on: { click: this.download }, + domProps: { innerHTML: "Download game" }, + } + ), ] ) ); @@ -572,7 +752,10 @@ Vue.component('my-game', { // Show current FEN elementArray.push( h('div', - { attrs: { id: "fen-div" } }, + { + attrs: { id: "fen-div" }, + "class": { "section-content": true }, + }, [ h('p', { @@ -609,17 +792,12 @@ Vue.component('my-game', { }, created: function() { const url = socketUrl; - const continuation = (localStorage.getItem("variant") === variant); - this.myid = continuation ? localStorage.getItem("myid") : getRandString(); - if (!continuation) - { - // HACK: play a small silent sound to allow "new game" sound later - // if tab not focused (TODO: does it really work ?!) - new Audio("/sounds/silent.mp3").play().then(() => {}).catch(err => {}); - } + const humanContinuation = (localStorage.getItem("variant") === variant); + const computerContinuation = (localStorage.getItem("comp-variant") === variant); + this.myid = (humanContinuation ? localStorage.getItem("myid") : getRandString()); this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant); const socketOpenListener = () => { - if (continuation) + if (humanContinuation) //game VS human has priority { const fen = localStorage.getItem("fen"); const mycolor = localStorage.getItem("mycolor"); @@ -629,16 +807,24 @@ Vue.component('my-game', { // Send ping to server (answer pong if opponent is connected) this.conn.send(JSON.stringify({code:"ping",oppid:this.oppid})); } - else if (localStorage.getItem("newgame") === variant) + else if (computerContinuation) { - // New game request has been cancelled on disconnect - this.newGame("human", undefined, undefined, undefined, undefined, "reconnect"); + const fen = localStorage.getItem("comp-fen"); + const mycolor = localStorage.getItem("comp-mycolor"); + const moves = JSON.parse(localStorage.getItem("comp-moves")); + this.newGame("computer", fen, mycolor, undefined, moves, true); } }; const socketMessageListener = msg => { const data = JSON.parse(msg.data); switch (data.code) { + case "duplicate": + // We opened another tab on the same game + this.mode = "idle"; + this.vr = null; + alert("Already playing a game in this variant on another tab!"); + break; case "newgame": //opponent found // oppid: opponent socket ID this.newGame("human", data.fen, data.color, data.oppid); @@ -738,7 +924,7 @@ Vue.component('my-game', { // Variants may have special PGN structure (so next function isn't defined here) this.pgnTxt = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode); setTimeout(() => { modalBox.checked = false; }, 2000); - if (this.mode == "human") + if (["human","computer"].includes(this.mode)) this.clearStorage(); this.mode = "idle"; this.cursor = this.vr.moves.length; //to navigate in finished game @@ -761,32 +947,58 @@ Vue.component('my-game', { return eogMessage; }, setStorage: function() { - localStorage.setItem("myid", this.myid); - localStorage.setItem("variant", variant); - localStorage.setItem("mycolor", this.mycolor); - localStorage.setItem("oppid", this.oppid); - localStorage.setItem("fenStart", this.fenStart); - localStorage.setItem("moves", JSON.stringify(this.vr.moves)); - localStorage.setItem("fen", this.vr.getFen()); + if (this.mode=="human") + { + localStorage.setItem("myid", this.myid); + localStorage.setItem("oppid", this.oppid); + } + // 'prefix' = "comp-" to resume games vs. computer + const prefix = (this.mode=="computer" ? "comp-" : ""); + localStorage.setItem(prefix+"variant", variant); + localStorage.setItem(prefix+"mycolor", this.mycolor); + localStorage.setItem(prefix+"fenStart", this.fenStart); + localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves)); + localStorage.setItem(prefix+"fen", this.vr.getFen()); }, updateStorage: function() { - localStorage.setItem("moves", JSON.stringify(this.vr.moves)); - localStorage.setItem("fen", this.vr.getFen()); + const prefix = (this.mode=="computer" ? "comp-" : ""); + localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves)); + localStorage.setItem(prefix+"fen", this.vr.getFen()); }, clearStorage: function() { - delete localStorage["variant"]; - delete localStorage["myid"]; - delete localStorage["mycolor"]; - delete localStorage["oppid"]; - delete localStorage["fenStart"]; - delete localStorage["fen"]; - delete localStorage["moves"]; + if (this.mode=="human") + { + delete localStorage["myid"]; + delete localStorage["oppid"]; + } + const prefix = (this.mode=="computer" ? "comp-" : ""); + delete localStorage[prefix+"variant"]; + delete localStorage[prefix+"mycolor"]; + delete localStorage[prefix+"fenStart"]; + delete localStorage[prefix+"fen"]; + delete localStorage[prefix+"moves"]; }, // HACK because mini-css tooltips are persistent after click... getRidOfTooltip: function(elt) { elt.style.visibility = "hidden"; setTimeout(() => { elt.style.visibility="visible"; }, 100); }, + showSettings: function(e) { + this.getRidOfTooltip(e.currentTarget); + document.getElementById("modal-settings").checked = true; + }, + toggleHints: function() { + this.hints = !this.hints; + setCookie("hints", this.hints ? "1" : "0"); + }, + setColor: function(e) { + this.color = e.target.options[e.target.selectedIndex].value; + setCookie("color", this.color); + }, + setSound: function(e) { + this.sound = parseInt(e.target.options[e.target.selectedIndex].value); + setCookie("sound", this.sound); + }, clickGameSeek: function(e) { this.getRidOfTooltip(e.currentTarget); if (this.mode == "human") @@ -794,7 +1006,6 @@ Vue.component('my-game', { if (this.seek) { this.conn.send(JSON.stringify({code:"cancelnewgame"})); - delete localStorage["newgame"]; //cancel game seek this.seek = false; } else @@ -810,11 +1021,6 @@ Vue.component('my-game', { this.getRidOfTooltip(e.currentTarget); document.getElementById("modal-fenedit").checked = true; }, - toggleExpertMode: function(e) { - this.getRidOfTooltip(e.currentTarget); - this.expert = !this.expert; - setCookie("expert", this.expert ? "1" : "0"); - }, resign: function(e) { this.getRidOfTooltip(e.currentTarget); if (this.mode == "human" && this.oppConnected) @@ -834,45 +1040,51 @@ Vue.component('my-game', { { const storageVariant = localStorage.getItem("variant"); if (!!storageVariant && storageVariant !== variant) - { - alert("Finish your " + storageVariant + " game first!"); - return; - } + return alert("Finish your " + storageVariant + " game first!"); // Send game request and wait.. - localStorage["newgame"] = variant; this.seek = true; - this.clearStorage(); //in case of try { this.conn.send(JSON.stringify({code:"newgame", fen:fen})); } catch (INVALID_STATE_ERR) { return; //nothing achieved } - if (continuation !== "reconnect") //TODO: bad HACK... - { - let modalBox = document.getElementById("modal-newgame"); - modalBox.checked = true; - setTimeout(() => { modalBox.checked = false; }, 2000); - } + let modalBox = document.getElementById("modal-newgame"); + modalBox.checked = true; + setTimeout(() => { modalBox.checked = false; }, 2000); return; } - this.gameId = getRandString(); + 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 this.mode = mode; - this.incheck = []; //in case of + this.incheck = continuation + ? this.vr + : []; this.fenStart = (continuation ? localStorage.getItem("fenStart") : fen); if (mode=="human") { + + + +//TODO: refactor this. (for computer mode too), lastMove getCheckSquares... + + + + // Opponent found! - if (!continuation) + if (!continuation) //not playing sound on game continuation { - // Not playing sound on game continuation: - new Audio("/sounds/newgame.mp3").play().then(() => {}).catch(err => {}); + if (this.sound >= 1) + new Audio("/sounds/newgame.mp3").play().then(() => {}).catch(err => {}); document.getElementById("modal-newgame").checked = false; } this.oppid = oppId; - this.oppConnected = true; + this.oppConnected = !continuation; this.mycolor = color; this.seek = false; if (!!moves && moves.length > 0) //imply continuation @@ -882,7 +1094,6 @@ Vue.component('my-game', { this.incheck = this.vr.getCheckSquares(lastMove); this.vr.play(lastMove, "ingame"); } - delete localStorage["newgame"]; this.setStorage(); //in case of interruptions } else if (mode == "computer") @@ -891,25 +1102,17 @@ Vue.component('my-game', { if (this.mycolor == 'b') setTimeout(this.playComputerMove, 500); } - //else: against a (IRL) friend: nothing more to do + //else: against a (IRL) friend or problem solving: nothing more to do }, playComputerMove: function() { const timeStart = Date.now(); - // We use moves' count to know if search finished: - const nbMoves = this.vr.moves.length; - const gameId = this.gameId; //to know if game was reset before timer end - setTimeout( - () => { - if (gameId != this.gameId) - return; //game stopped - const L = this.vr.moves.length; - if (nbMoves == L || !this.vr.moves[L-1].notation) //move search didn't finish - this.vr.shouldReturn = true; - }, 5000); const compMove = this.vr.getComputerMove(); // (first move) HACK: avoid selecting elements before they appear on page: const delay = Math.max(500-(Date.now()-timeStart), 0); - setTimeout(() => this.play(compMove, "animate"), delay); + setTimeout(() => { + if (this.mode == "computer") //Warning: mode could have changed! + this.play(compMove, "animate") + }, delay); }, // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y. getSquareId: function(o) { @@ -951,10 +1154,16 @@ Vue.component('my-game', { this.selectedPiece.style.top = 0; this.selectedPiece.style.display = "inline-block"; this.selectedPiece.style.zIndex = 3000; - let startSquare = this.getSquareFromId(e.target.parentNode.id); - const iCanPlay = this.mode!="idle" - && (this.mode=="friend" || this.vr.canIplay(this.mycolor,startSquare)); - this.possibleMoves = iCanPlay ? this.vr.getPossibleMovesFrom(startSquare) : []; + const startSquare = this.getSquareFromId(e.target.parentNode.id); + this.possibleMoves = []; + if (this.mode != "idle") + { + const color = ["friend","problem"].includes(this.mode) + ? this.vr.turn + : this.mycolor; + if (this.vr.canIplay(color,startSquare)) + this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare); + } // Next line add moving piece just after current image // (required for Crazyhouse reserve) e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling); @@ -1059,7 +1268,8 @@ Vue.component('my-game', { // 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})); - new Audio("/sounds/chessmove1.mp3").play().then(() => {}).catch(err => {}); + if (this.sound == 2) + new Audio("/sounds/chessmove1.mp3").play().then(() => {}).catch(err => {}); if (this.mode != "idle") { this.incheck = this.vr.getCheckSquares(move); //is opponent in check? @@ -1070,7 +1280,7 @@ Vue.component('my-game', { VariantRules.PlayOnBoard(this.vr.board, move); this.$forceUpdate(); //TODO: ?! } - if (this.mode == "human") + if (["human","computer"].includes(this.mode)) this.updateStorage(); //after our moves and opponent moves if (this.mode != "idle") {