X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fcomponents%2Fgame.js;h=186c027c698199d4421e1da186f21701c06ef47b;hb=b5fb8e693dc82037eec2617a7dc49d838a9a8441;hp=c7ec715af2d6e5e526ac184765477a1c7a5ecddb;hpb=130db3eff9afbf58b30287d06f3a2360fe652e97;p=vchess.git diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index c7ec715a..186c027c 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.. @@ -20,12 +21,18 @@ Vue.component('my-game', { 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: getCookie("sound", "2"), + sound: parseInt(getCookie("sound", "2")), }; }, + watch: { + problem: function(p, pp) { + // 'problem' prop changed: update board state + this.newGame("problem", p.fen, V.ParseFen(p.fen).turn); + }, + }, 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; }); @@ -92,10 +99,10 @@ Vue.component('my-game', { const settingsBtnElt = document.getElementById("settingsBtn"); const indicWidth = !!settingsBtnElt //-2 for border: ? parseFloat(window.getComputedStyle(settingsBtnElt).height.slice(0,-2)) - 2 - : 37; //TODO: always 37? + : (smallScreen ? 31 : 37); if (this.mode == "human") { - let connectedIndic = h( + const connectedIndic = h( 'div', { "class": { @@ -112,7 +119,7 @@ Vue.component('my-game', { ); elementArray.push(connectedIndic); } - let turnIndic = h( + const turnIndic = h( 'div', { "class": { @@ -128,7 +135,7 @@ Vue.component('my-game', { } ); elementArray.push(turnIndic); - let settingsBtn = h( + const settingsBtn = h( 'button', { on: { click: this.showSettings }, @@ -140,14 +147,34 @@ Vue.component('my-game', { "tooltip": true, "topindicator": true, "indic-right": true, - "settings-btn": true, - "small": smallScreen, + "settings-btn": !smallScreen, + "settings-btn-small": smallScreen, }, }, [h('i', { 'class': { "material-icons": true } }, "settings")] ); elementArray.push(settingsBtn); - let choices = h('div', + if (this.mode == "problem") + { + // Show problem instructions + elementArray.push( + h('div', + { + attrs: { id: "instructions-div" }, + "class": { "section-content": true }, + }, + [ + h('p', + { + attrs: { id: "problem-instructions" }, + domProps: { innerHTML: this.problem.instructions } + } + ) + ] + ) + ); + } + const choices = h('div', { attrs: { "id": "choices" }, 'class': { 'row': true }, @@ -185,7 +212,7 @@ Vue.component('my-game', { const lm = this.vr.lastMove; const showLight = this.hints && (this.mode!="idle" || this.cursor==this.vr.moves.length); - let gameDiv = h('div', + const gameDiv = h('div', { 'class': { 'game': true }, }, @@ -299,7 +326,7 @@ Vue.component('my-game', { ] ); } - if (this.mode == "friend") + if (["friend","problem"].includes(this.mode)) { actionArray = actionArray.concat( [ @@ -400,7 +427,6 @@ Vue.component('my-game', { ); elementArray.push(reserves); } - const eogMessage = this.getEndgameMessage(this.score); const modalEog = [ h('input', { @@ -409,7 +435,7 @@ Vue.component('my-game', { }), h('div', { - attrs: { "role": "dialog", "aria-labelledby": "modal-eog" }, + attrs: { "role": "dialog", "aria-labelledby": "eogMessage" }, }, [ h('div', @@ -425,8 +451,9 @@ Vue.component('my-game', { ), h('h3', { + attrs: { "id": "eogMessage" }, "class": { "section": true }, - domProps: { innerHTML: eogMessage }, + domProps: { innerHTML: this.endgameMessage }, } ) ] @@ -445,7 +472,7 @@ Vue.component('my-game', { }), h('div', { - attrs: { "role": "dialog", "aria-labelledby": "modal-newgame" }, + attrs: { "role": "dialog", "aria-labelledby": "newGameTxt" }, }, [ h('div', @@ -461,6 +488,7 @@ Vue.component('my-game', { ), h('h3', { + attrs: { "id": "newGameTxt" }, "class": { "section": true }, domProps: { innerHTML: "New game" }, } @@ -485,7 +513,7 @@ Vue.component('my-game', { }), h('div', { - attrs: { "role": "dialog", "aria-labelledby": "modal-fenedit" }, + attrs: { "role": "dialog", "aria-labelledby": "titleFenedit" }, }, [ h('div', @@ -501,6 +529,7 @@ Vue.component('my-game', { ), h('h3', { + attrs: { "id": "titleFenedit" }, "class": { "section": true }, domProps: { innerHTML: "Position + flags (FEN):" }, } @@ -551,7 +580,7 @@ Vue.component('my-game', { }), h('div', { - attrs: { "role": "dialog", "aria-labelledby": "modal-settings" }, + attrs: { "role": "dialog", "aria-labelledby": "settingsTitle" }, }, [ h('div', @@ -567,6 +596,7 @@ Vue.component('my-game', { ), h('h3', { + attrs: { "id": "settingsTitle" }, "class": { "section": true }, domProps: { innerHTML: "Preferences" }, } @@ -660,6 +690,7 @@ Vue.component('my-game', { "value": "0", innerHTML: "None" }, + attrs: { "selected": this.sound==0 }, } ), h("option", @@ -668,6 +699,7 @@ Vue.component('my-game', { "value": "1", innerHTML: "Newgame" }, + attrs: { "selected": this.sound==1 }, } ), h("option", @@ -676,6 +708,7 @@ Vue.component('my-game', { "value": "2", innerHTML: "All" }, + attrs: { "selected": this.sound==2 }, } ), ], @@ -696,11 +729,14 @@ Vue.component('my-game', { actionArray ); elementArray.push(actions); - if (this.score != "*") + if (this.score != "*" && this.pgnTxt.length > 0) { elementArray.push( h('div', - { attrs: { id: "pgn-div" } }, + { + attrs: { id: "pgn-div" }, + "class": { "section-content": true }, + }, [ h('a', { @@ -713,25 +749,60 @@ 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" }, + } + ), ] ) ); } else if (this.mode != "idle") { + if (this.mode == "problem") + { + // Show problem solution (on click) + elementArray.push( + h('div', + { + attrs: { id: "solution-div" }, + "class": { "section-content": true }, + }, + [ + h('h3', + { + domProps: { innerHTML: "Show solution" }, + on: { click: this.toggleShowSolution }, + } + ), + h('p', + { + attrs: { id: "problem-solution" }, + domProps: { innerHTML: this.problem.solution } + } + ) + ] + ) + ); + } // Show current FEN elementArray.push( h('div', - { attrs: { id: "fen-div" } }, + { + attrs: { id: "fen-div" }, + "class": { "section-content": true }, + }, [ h('p', { attrs: { id: "fen-string" }, - domProps: { innerHTML: this.vr.getFen() } + domProps: { innerHTML: this.vr.getBaseFen() } } ) ] @@ -761,19 +832,32 @@ Vue.component('my-game', { elementArray ); }, + computed: { + endgameMessage: function() { + let eogMessage = "Unfinished"; + switch (this.score) + { + case "1-0": + eogMessage = "White win"; + break; + case "0-1": + eogMessage = "Black win"; + break; + case "1/2": + eogMessage = "Draw"; + break; + } + return eogMessage; + }, + }, 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"); @@ -783,16 +867,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); @@ -875,6 +967,13 @@ Vue.component('my-game', { }; }, methods: { + toggleShowSolution: function() { + let problemSolution = document.getElementById("problem-solution"); + problemSolution.style.display = + !problemSolution.style.display || problemSolution.style.display == "none" + ? "block" + : "none"; + }, download: function() { let content = document.getElementById("pgn-game").innerHTML; content = content.replace(/
/g, "\n"); @@ -885,56 +984,53 @@ Vue.component('my-game', { encodeURIComponent(content); downloadAnchor.click(); }, - endGame: function(score) { - this.score = score; + showScoreMsg: function() { let modalBox = document.getElementById("modal-eog"); modalBox.checked = true; + setTimeout(() => { modalBox.checked = false; }, 2000); + }, + endGame: function(score) { + this.score = score; + this.showScoreMsg(); // 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 this.oppid = ""; }, - getEndgameMessage: function(score) { - let eogMessage = "Unfinished"; - switch (this.score) + setStorage: function() { + if (this.mode=="human") { - case "1-0": - eogMessage = "White win"; - break; - case "0-1": - eogMessage = "Black win"; - break; - case "1/2": - eogMessage = "Draw"; - break; + localStorage.setItem("myid", this.myid); + localStorage.setItem("oppid", this.oppid); } - 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()); + // '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) { @@ -964,7 +1060,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 @@ -999,70 +1094,90 @@ 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... + this.seek = true; + let modalBox = document.getElementById("modal-newgame"); + modalBox.checked = true; + setTimeout(() => { modalBox.checked = false; }, 2000); + return; + } + if (mode == "computer" && !continuation) + { + const storageVariant = localStorage.getItem("comp-variant"); + if (!!storageVariant && storageVariant !== variant) { - let modalBox = document.getElementById("modal-newgame"); - modalBox.checked = true; - setTimeout(() => { modalBox.checked = false; }, 2000); + if (!confirm("Unfinished " + storageVariant + + " computer game will be erased")) + { + return; + } } - return; + } + 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.fenStart = (continuation ? localStorage.getItem("fenStart") : fen); + if (continuation && moves.length > 0) //NOTE: "continuation": redundant test + { + const lastMove = moves[moves.length-1]; + this.vr.undo(lastMove); + this.incheck = this.vr.getCheckSquares(lastMove); + this.vr.play(lastMove, "ingame"); + } + else + this.incheck = []; + if (continuation) + { + const prefix = (mode=="computer" ? "comp-" : ""); + this.fenStart = localStorage.getItem(prefix+"fenStart"); + } + else + this.fenStart = V.ParseFen(fen).position; //this is enough if (mode=="human") { // Opponent found! if (!continuation) //not playing sound on game continuation { if (this.sound >= 1) - new Audio("/sounds/newgame.mp3").play().then(() => {}).catch(err => {}); + new Audio("/sounds/newgame.mp3").play().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 - { - const lastMove = moves[moves.length-1]; - this.vr.undo(lastMove); - this.incheck = this.vr.getCheckSquares(lastMove); - this.vr.play(lastMove, "ingame"); - } - delete localStorage["newgame"]; 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, 500); + setTimeout(this.playComputerMove, 100); //small delay for drawing board } - //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(); + // TODO: next call asynchronous (avoid freezing interface while computer "think"). + // This would also allow to remove some artificial setTimeouts 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); + const delay = Math.max(250-(Date.now()-timeStart), 0); + 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) { @@ -1104,10 +1219,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); @@ -1194,7 +1315,7 @@ Vue.component('my-game', { squares.item(i).style.zIndex = "auto"; movingPiece.style = {}; //required e.g. for 0-0 with KR swap this.play(move); - }, 200); + }, 250); }, play: function(move, programmatic) { if (!move) @@ -1213,7 +1334,7 @@ Vue.component('my-game', { if (this.mode == "human" && this.vr.turn == this.mycolor) this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid})); if (this.sound == 2) - new Audio("/sounds/chessmove1.mp3").play().then(() => {}).catch(err => {}); + new Audio("/sounds/chessmove1.mp3").play().catch(err => {}); if (this.mode != "idle") { this.incheck = this.vr.getCheckSquares(move); //is opponent in check? @@ -1224,16 +1345,25 @@ 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") { const eog = this.vr.checkGameOver(); if (eog != "*") - this.endGame(eog); + { + if (["human","computer"].includes(this.mode)) + this.endGame(eog); + else + { + // Just show score on screen (allow undo) + this.score = eog; + this.showScoreMsg(); + } + } } if (this.mode == "computer" && this.vr.turn != this.mycolor) - setTimeout(this.playComputerMove, 500); + setTimeout(this.playComputerMove, 250); //small delay for animation }, undo: function() { // Navigate after game is over @@ -1248,7 +1378,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 = []; + } }, }, })