X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fcomponents%2Fgame.js;h=7b238f5c2b701f35c4c11f4c7cac1d4a149da244;hb=9234226104764b91df9d677fb360ad538b98510c;hp=1178b64abc7b54707f2dbdedb269eea8b3716652;hpb=c148615e9e7296869f95895ef434fd8371d637c2;p=vchess.git diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index 1178b64a..7b238f5c 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -1,27 +1,29 @@ +// 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, computer or idle (when not playing) + mode: "idle", //human, friend, computer or idle (when not playing) oppid: "", //opponent ID in case of HH game oppConnected: false, seek: false, fenStart: "", incheck: [], pgnTxt: "", - expert: document.cookie.length>0 ? document.cookie.substr(-1)=="1" : false, + expert: (getCookie("expert") === "1" ? true : false), gameId: "", //used to limit computer moves' time }; }, 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; }); @@ -29,33 +31,56 @@ Vue.component('my-game', { let incheckSq = doubleArray(sizeX, sizeY, false); this.incheck.forEach(sq => { incheckSq[sq[0]][sq[1]] = true; }); let elementArray = []; - const playingHuman = (this.mode == "human"); - const playingComp = (this.mode == "computer"); - let actionArray = [ + let actionArray = []; + actionArray.push( h('button', - { - on: { click: this.clickGameSeek }, - attrs: { "aria-label": 'New game VS human' }, - 'class': { - "tooltip": true, - "bottom": true, //display below - "seek": this.seek, - "playing": playingHuman, - }, + { + 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('button', + }, + [h('i', { 'class': { "material-icons": true } }, "accessibility")]) + ); + if (["idle","computer"].includes(this.mode)) + { + actionArray.push( + h('button', { on: { click: this.clickComputerGame }, attrs: { "aria-label": 'New game VS computer' }, 'class': { "tooltip":true, "bottom": true, - "playing": playingComp, + "playing": this.mode == "computer", + "small": smallScreen, }, }, [h('i', { 'class': { "material-icons": true } }, "computer")]) - ]; + ); + } + if (["idle","friend"].includes(this.mode)) + { + actionArray.push( + h('button', + { + on: { click: this.clickFriendGame }, + attrs: { "aria-label": 'New IRL game' }, + 'class': { + "tooltip":true, + "bottom": true, + "playing": this.mode == "friend", + "small": smallScreen, + }, + }, + [h('i', { 'class': { "material-icons": true } }, "people")]) + ); + } if (!!this.vr) { const square00 = document.getElementById("sq-0-0"); @@ -109,9 +134,10 @@ Vue.component('my-game', { "indic-right": true, "expert-switch": true, "expert-mode": this.expert, + "small": smallScreen, }, }, - [h('i', { 'class': { "material-icons": true } }, "remove_red_eye")] + [h('i', { 'class': { "material-icons": true } }, "visibility_off")] ); elementArray.push(expertSwitch); let choices = h('div', @@ -235,6 +261,7 @@ Vue.component('my-game', { 'class': { "tooltip":true, "bottom": true, + "small": smallScreen, }, }, [h('i', { 'class': { "material-icons": true } }, "flag")]) @@ -246,20 +273,49 @@ 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")]), ] ); } + if (this.mode == "friend") + { + actionArray = actionArray.concat( + [ + h('button', + { + on: { click: this.undoInGame }, + attrs: { "aria-label": 'Undo' }, + "class": { + "small": smallScreen, + "marginleft": true, + }, + }, + [h('i', { 'class': { "material-icons": true } }, "undo")] + ), + h('button', + { + on: { click: () => { this.mycolor = this.vr.getOppCol(this.mycolor) } }, + attrs: { "aria-label": 'Flip' }, + "class": { "small": smallScreen }, + }, + [h('i', { 'class': { "material-icons": true } }, "cached")] + ), + ]); + } elementArray.push(gameDiv); if (!!this.vr.reserve) { @@ -282,7 +338,7 @@ Vue.component('my-game', { } }), h('sup', - {style: { "padding-left":"40%"} }, + {"class": { "reserve-count": true } }, [ this.vr.reserve[this.mycolor][VariantRules.RESERVE_PIECES[i]] ] ) ])); @@ -306,21 +362,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 ), @@ -408,6 +468,72 @@ Vue.component('my-game', { ) ]; elementArray = elementArray.concat(modalNewgame); + const modalFenEdit = [ + h('input', + { + attrs: { "id": "modal-fenedit", type: "checkbox" }, + "class": { "modal": true }, + }), + h('div', + { + attrs: { "role": "dialog", "aria-labelledby": "modal-fenedit" }, + }, + [ + h('div', + { + "class": { "card": true, "smallpad": true }, + }, + [ + h('label', + { + attrs: { "id": "close-fenedit", "for": "modal-fenedit" }, + "class": { "modal-close": true }, + } + ), + h('h3', + { + "class": { "section": true }, + domProps: { innerHTML: "Position + flags (FEN):" }, + } + ), + h('input', + { + attrs: { + "id": "input-fen", + type: "text", + value: VariantRules.GenRandInitFen(), + }, + } + ), + h('button', + { + on: { click: + () => { + const fen = document.getElementById("input-fen").value; + document.getElementById("modal-fenedit").checked = false; + this.newGame("friend", fen); + } + }, + domProps: { innerHTML: "Ok" }, + } + ), + h('button', + { + on: { click: + () => { + document.getElementById("input-fen").value = + VariantRules.GenRandInitFen(); + } + }, + domProps: { innerHTML: "Random" }, + } + ), + ] + ) + ] + ) + ]; + elementArray = elementArray.concat(modalFenEdit); const actions = h('div', { attrs: { "id": "actions" }, @@ -443,7 +569,7 @@ Vue.component('my-game', { } else if (this.mode != "idle") { - // Show current FEN (at least for debug) + // Show current FEN elementArray.push( h('div', { attrs: { id: "fen-div" } }, @@ -451,7 +577,7 @@ Vue.component('my-game', { h('p', { attrs: { id: "fen-string" }, - domProps: { innerHTML: this.vr.getBaseFen() } + domProps: { innerHTML: this.vr.getFen() } } ) ] @@ -484,13 +610,11 @@ Vue.component('my-game', { 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.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); @@ -516,7 +640,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"); @@ -584,7 +709,7 @@ Vue.component('my-game', { this.conn.onclose = socketCloseListener; // Listen to keyboard left/right to navigate in game document.onkeydown = event => { - if (this.mode == "idle" && this.vr.moves.length > 0 + if (this.mode == "idle" && !!this.vr && this.vr.moves.length > 0 && [37,39].includes(event.keyCode)) { event.preventDefault(); @@ -602,7 +727,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) { @@ -667,6 +793,7 @@ Vue.component('my-game', { return; //no newgame while playing if (this.seek) { + this.conn.send(JSON.stringify({code:"cancelnewgame"})); delete localStorage["newgame"]; //cancel game seek this.seek = false; } @@ -679,12 +806,17 @@ Vue.component('my-game', { return; //no newgame while playing this.newGame("computer"); }, + clickFriendGame: function(e) { + this.getRidOfTooltip(e.currentTarget); + document.getElementById("modal-fenedit").checked = true; + }, toggleExpertMode: function(e) { this.getRidOfTooltip(e.currentTarget); this.expert = !this.expert; - document.cookie = "expert=" + (this.expert ? "1" : "0"); + setCookie("expert", this.expert ? "1" : "0"); }, - resign: function() { + resign: function(e) { + this.getRidOfTooltip(e.currentTarget); if (this.mode == "human" && this.oppConnected) { try { @@ -723,16 +855,13 @@ Vue.component('my-game', { } return; } - // random enough (TODO: function) - this.gameId = (Date.now().toString(36) + Math.random().toString(36).substr(2, 7)).toUpperCase(); + this.gameId = getRandString(); 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.split(" ")[0]; //Only the position matters + this.fenStart = (continuation ? localStorage.getItem("fenStart") : fen); if (mode=="human") { // Opponent found! @@ -756,16 +885,18 @@ Vue.component('my-game', { delete localStorage["newgame"]; this.setStorage(); //in case of interruptions } - else //against computer + else if (mode == "computer") { this.mycolor = Math.random() < 0.5 ? 'w' : 'b'; if (this.mycolor == 'b') setTimeout(this.playComputerMove, 500); } + //else: against a (IRL) friend: nothing more to do }, playComputerMove: function() { const timeStart = Date.now(); - const nbMoves = this.vr.moves.length; //using played moves to know if search finished + // 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( () => { @@ -821,10 +952,11 @@ Vue.component('my-game', { this.selectedPiece.style.display = "inline-block"; this.selectedPiece.style.zIndex = 3000; let startSquare = this.getSquareFromId(e.target.parentNode.id); - this.possibleMoves = this.mode!="idle" && this.vr.canIplay(this.mycolor,startSquare) - ? this.vr.getPossibleMovesFrom(startSquare) - : []; - // Next line add moving piece just after current image (required for Crazyhouse reserve) + 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) e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling); } }, @@ -847,16 +979,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); @@ -887,7 +1023,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 { @@ -953,6 +1090,11 @@ Vue.component('my-game', { 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); + }, }, })