X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=public%2Fjavascripts%2Fcomponents%2Fgame.js;h=6af331b3158cd6436e858a633eb933a979f97511;hp=11ec4c91a5ce529b0fccb27c0bc9e0908acd0840;hb=9d218497ab97bc0e94ec4c1f0a40cf02df3ea0d4;hpb=2748531f7dbe3202b29c911eede36a0069a33528 diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index 11ec4c91..6af331b3 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -1,13 +1,14 @@ +// Game logic on a variant page Vue.component('my-game', { 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) + choices: [], //promotion pieces, or checkered captures... (as moves) start: {}, //pixels coordinates + id of starting square (click or drag) selectedPiece: null, //moving piece (or clicked piece) - conn: null, //socket messages + conn: null, //socket connection score: "*", //'*' means 'unfinished' mode: "idle", //human, friend, computer or idle (when not playing) oppid: "", //opponent ID in case of HH game @@ -16,12 +17,15 @@ 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")), }; }, render(h) { const [sizeX,sizeY] = VariantRules.size; + const smallScreen = (screen.width <= 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; }); @@ -30,23 +34,21 @@ Vue.component('my-game', { this.incheck.forEach(sq => { incheckSq[sq[0]][sq[1]] = true; }); let elementArray = []; let actionArray = []; - if (["idle","human"].includes(this.mode)) - { - actionArray.push( - h('button', - { - on: { click: this.clickGameSeek }, - attrs: { "aria-label": 'New online game' }, - 'class': { - "tooltip": true, - "bottom": true, //display below - "seek": this.seek, - "playing": this.mode == "human", - }, + actionArray.push( + h('button', + { + on: { click: this.clickGameSeek }, + attrs: { "aria-label": 'New online game' }, + 'class': { + "tooltip": true, + "bottom": true, //display below + "seek": this.seek, + "playing": this.mode == "human", + "small": smallScreen, }, - [h('i', { 'class': { "material-icons": true } }, "accessibility")]) - ); - } + }, + [h('i', { 'class': { "material-icons": true } }, "accessibility")]) + ); if (["idle","computer"].includes(this.mode)) { actionArray.push( @@ -58,6 +60,7 @@ Vue.component('my-game', { "tooltip":true, "bottom": true, "playing": this.mode == "computer", + "small": smallScreen, }, }, [h('i', { 'class': { "material-icons": true } }, "computer")]) @@ -74,6 +77,7 @@ Vue.component('my-game', { "tooltip":true, "bottom": true, "playing": this.mode == "friend", + "small": smallScreen, }, }, [h('i', { 'class': { "material-icons": true } }, "people")]) @@ -85,7 +89,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( @@ -121,22 +128,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, + "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" }, @@ -172,6 +182,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 }, @@ -208,7 +221,7 @@ Vue.component('my-game', { ) ); } - if (!this.expert && hintSquares[ci][cj]) + if (this.hints && hintSquares[ci][cj]) { elems.push( h( @@ -224,9 +237,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', { @@ -235,6 +245,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], }, @@ -258,6 +269,7 @@ Vue.component('my-game', { 'class': { "tooltip":true, "bottom": true, + "small": smallScreen, }, }, [h('i', { 'class': { "material-icons": true } }, "flag")]) @@ -269,15 +281,19 @@ Vue.component('my-game', { actionArray = actionArray.concat([ h('button', { - style: { "margin-left": "30px" }, on: { click: e => this.undo() }, attrs: { "aria-label": 'Undo' }, + "class": { + "small": smallScreen, + "marginleft": true, + }, }, [h('i', { 'class': { "material-icons": true } }, "fast_rewind")]), h('button', { on: { click: e => this.play() }, attrs: { "aria-label": 'Play' }, + "class": { "small": smallScreen }, }, [h('i', { 'class': { "material-icons": true } }, "fast_forward")]), ] @@ -289,9 +305,12 @@ Vue.component('my-game', { [ h('button', { - style: { "margin-left": "30px" }, on: { click: this.undoInGame }, attrs: { "aria-label": 'Undo' }, + "class": { + "small": smallScreen, + "marginleft": true, + }, }, [h('i', { 'class': { "material-icons": true } }, "undo")] ), @@ -299,6 +318,7 @@ Vue.component('my-game', { { on: { click: () => { this.mycolor = this.vr.getOppCol(this.mycolor) } }, attrs: { "aria-label": 'Flip' }, + "class": { "small": smallScreen }, }, [h('i', { 'class': { "material-icons": true } }, "cached")] ), @@ -326,7 +346,7 @@ Vue.component('my-game', { } }), h('sup', - {style: { "padding-left":"40%"} }, + {"class": { "reserve-count": true } }, [ this.vr.reserve[this.mycolor][VariantRules.RESERVE_PIECES[i]] ] ) ])); @@ -350,21 +370,25 @@ Vue.component('my-game', { } }), h('sup', - {style: { "padding-left":"40%"} }, + {"class": { "reserve-count": true } }, [ this.vr.reserve[oppCol][VariantRules.RESERVE_PIECES[i]] ] ) ])); } let reserves = h('div', { - 'class':{'game':true}, - style: {"margin-bottom": "20px"}, + 'class':{ + 'game': true, + "reserve-div": true, + }, }, [ h('div', { - 'class': { 'row': true }, - style: {"margin-bottom": "15px"}, + 'class': { + 'row': true, + "reserve-row-1": true, + }, }, myReservePiecesArray ), @@ -412,6 +436,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', { @@ -518,6 +543,154 @@ 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": "modal-settings" }, + }, + [ + h('div', + { + "class": { "card": true, "smallpad": true }, + }, + [ + h('label', + { + attrs: { "id": "close-settings", "for": "modal-settings" }, + "class": { "modal-close": true }, + } + ), + h('h3', + { + "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" }, @@ -530,7 +703,10 @@ Vue.component('my-game', { { elementArray.push( h('div', - { attrs: { id: "pgn-div" } }, + { + attrs: { id: "pgn-div" }, + "class": { "section-content": true }, + }, [ h('a', { @@ -543,10 +719,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" }, + } + ), ] ) ); @@ -556,7 +738,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', { @@ -597,7 +782,8 @@ Vue.component('my-game', { 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 + // 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 => {}); } this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant); @@ -623,7 +809,8 @@ Vue.component('my-game', { switch (data.code) { case "newgame": //opponent found - this.newGame("human", data.fen, data.color, data.oppid); //oppid: opponent socket ID + // oppid: opponent socket ID + this.newGame("human", data.fen, data.color, data.oppid); break; case "newmove": //..he played! this.play(data.move, "animate"); @@ -709,7 +896,8 @@ Vue.component('my-game', { // 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.href = "data:text/plain;charset=utf-8," + + encodeURIComponent(content); downloadAnchor.click(); }, endGame: function(score) { @@ -768,6 +956,22 @@ Vue.component('my-game', { 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") @@ -791,11 +995,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) @@ -809,7 +1008,7 @@ Vue.component('my-game', { this.endGame(this.mycolor=="w"?"0-1":"1-0"); }, newGame: function(mode, fenInit, color, oppId, moves, continuation) { - const fen = fenInit || VariantRules.GenRandInitFen(); + const fen = "M7/8/8/3K1k2/8/8/8/8 0000";//fenInit || VariantRules.GenRandInitFen(); console.log(fen); //DEBUG if (mode=="human" && !oppId) { @@ -836,7 +1035,6 @@ Vue.component('my-game', { } return; } - this.gameId = getRandString(); this.vr = new VariantRules(fen, moves || []); this.score = "*"; this.pgnTxt = ""; //redundant with this.score = "*", but cleaner @@ -846,10 +1044,10 @@ Vue.component('my-game', { if (mode=="human") { // 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; @@ -868,7 +1066,7 @@ Vue.component('my-game', { } else if (mode == "computer") { - this.mycolor = Math.random() < 0.5 ? 'w' : 'b'; + this.mycolor = "w";//Math.random() < 0.5 ? 'w' : 'b'; if (this.mycolor == 'b') setTimeout(this.playComputerMove, 500); } @@ -876,16 +1074,6 @@ Vue.component('my-game', { }, playComputerMove: function() { const timeStart = Date.now(); - const nbMoves = this.vr.moves.length; //using played moves to know if search finished - 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); @@ -935,7 +1123,8 @@ Vue.component('my-game', { const iCanPlay = this.mode!="idle" && (this.mode=="friend" || this.vr.canIplay(this.mycolor,startSquare)); this.possibleMoves = iCanPlay ? this.vr.getPossibleMovesFrom(startSquare) : []; - // Next line add moving piece just after current image (required for Crazyhouse reserve) + // Next line add moving piece just after current image + // (required for Crazyhouse reserve) e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling); } }, @@ -958,16 +1147,20 @@ Vue.component('my-game', { 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 + this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords const [offsetX,offsetY] = !!e.clientX ? [e.clientX,e.clientY] : [e.changedTouches[0].pageX, e.changedTouches[0].pageY]; let landing = document.elementFromPoint(offsetX, offsetY); this.selectedPiece.style.zIndex = 3000; - while (landing.tagName == "IMG") //classList.contains(piece) fails because of mark/highlight + // Next condition: classList.contains(piece) fails because of marks + while (landing.tagName == "IMG") landing = landing.parentNode; - if (this.start.id == landing.id) //a click: selectedPiece and possibleMoves already filled + if (this.start.id == landing.id) + { + // A click: selectedPiece and possibleMoves are already filled return; + } // OK: process move attempt let endSquare = this.getSquareFromId(landing.id); let moves = this.findMatchingMoves(endSquare); @@ -998,7 +1191,7 @@ Vue.component('my-game', { 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 (with positive translate, image slides "under background"...) + // HACK for animation (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 { @@ -1033,7 +1227,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?