X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=client%2Fsrc%2Fcomponents%2FBaseGame.vue;h=2517c78ab9832ed7685c4d6181bc8f8c15d25bf9;hp=d035e0093f0a99b1db525e8258cfbbd9d2b9120c;hb=d54f6261c9e30f4eabb402ad301dd5c5e40fb656;hpb=ed06d9e92387e60fc15d7c9768dd5680605e35d1 diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index d035e009..2517c78a 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -1,38 +1,59 @@ @@ -42,19 +63,22 @@ import MoveList from "@/components/MoveList.vue"; import { store } from "@/store"; import { getSquareId } from "@/utils/squareId"; import { getDate } from "@/utils/datetime"; - +import { processModalClick } from "@/utils/modalClick"; +import { getScoreMessage } from "@/utils/scoring"; +import { getFullNotation } from "@/utils/notation"; +import { undoMove } from "@/utils/playUndo"; export default { - name: 'my-base-game', + name: "my-base-game", components: { Board, - MoveList, + MoveList }, - // "vr": VariantRules object, describing the game state + rules - props: ["vr","game"], + props: ["game"], data: function() { return { st: store.state, // NOTE: all following variables must be reset at the beginning of a game + vr: null, //VariantRules object, game state endgameMessage: "", orientation: "w", score: "*", //'*' means 'unfinished' @@ -62,69 +86,70 @@ export default { cursor: -1, //index of the move just played lastMove: null, firstMoveNumber: 0, //for printing + incheck: [], //for Board + inMultimove: false, + autoplay: false, + autoplayLoop: null, + inPlay: false, + stackToPlay: [] }; }, - watch: { - // game initial FEN changes when a new game starts - "game.fenStart": function() { - this.re_setVariables(); - }, - // Received a new move to play: - "game.moveToPlay": function(newMove) { - -console.log(newMove); - - if (!!newMove) //if stop + launch new game, get undefined move - this.play(newMove, "receive"); - }, - }, computed: { showMoves: function() { - return this.game.vname != "Dark" || this.game.score != "*"; + return this.game.score != "*" + ? "all" + : (this.vr ? this.vr.showMoves : "none"); + }, + showTurn: function() { + return ( + this.game.score == '*' && + !!this.vr && this.vr.showTurn + ); }, turn: function() { - let color = ""; - const L = this.moves.length; - if (L == 0 || this.moves[L-1].color == "b") - color = "White"; - else //if (this.moves[L-1].color == "w") - color = "Black"; - return color + " turn"; + if (!this.vr) return ""; + if (this.vr.showMoves != "all") + return this.st.tr[(this.vr.turn == 'w' ? "White" : "Black") + " to move"]; + // Cannot flip: racing king or circular chess + return this.vr.movesCount == 0 && this.game.mycolor == "w" + ? this.st.tr["It's your turn!"] + : ""; + }, + canAnalyze: function() { + return this.game.mode != "analyze" && this.vr && this.vr.canAnalyze; }, - analyze: function() { - return this.game.mode=="analyze" || - // From Board viewpoint, a finished Dark game == analyze (TODO: unclear) - (this.game.vname == "Dark" && this.game.score != "*"); + canFlip: function() { + return this.vr && this.vr.canFlip; }, + allowDownloadPGN: function() { + return this.game.score != "*" || (this.vr && this.vr.showMoves == "all"); + } }, created: function() { - if (!!this.game.fenStart) - this.re_setVariables(); + if (!!this.game.fenStart) this.re_setVariables(); }, mounted: function() { - // Take full width on small screens: - let boardSize = parseInt(localStorage.getItem("boardSize")); - if (!boardSize) - { - boardSize = (window.innerWidth >= 768 - ? Math.min(600, 0.5*window.innerWidth) //heuristic... - : window.innerWidth); + if (!("ontouchstart" in window)) { + // Desktop browser: + const baseGameDiv = document.getElementById("baseGame"); + baseGameDiv.tabIndex = 0; + baseGameDiv.addEventListener("click", this.focusBg); + baseGameDiv.addEventListener("keydown", this.handleKeys); + baseGameDiv.addEventListener("wheel", this.handleScroll); } - const movesWidth = (window.innerWidth >= 768 ? 280 : 0); - document.getElementById("boardContainer").style.width = boardSize + "px"; - let gameContainer = document.getElementById("gameContainer"); - gameContainer.style.width = (boardSize + movesWidth) + "px"; + document.getElementById("eogDiv") + .addEventListener("click", processModalClick); + }, + beforeDestroy: function() { + if (!!this.autoplayLoop) clearInterval(this.autoplayLoop); }, methods: { focusBg: function() { - // NOTE: small blue border appears... document.getElementById("baseGame").focus(); }, handleKeys: function(e) { - if ([32,37,38,39,40].includes(e.keyCode)) - e.preventDefault(); - switch (e.keyCode) - { + if ([32, 37, 38, 39, 40].includes(e.keyCode)) e.preventDefault(); + switch (e.keyCode) { case 37: this.undo(); break; @@ -143,61 +168,87 @@ console.log(newMove); } }, handleScroll: function(e) { - // NOTE: since game.mode=="analyze" => no score, next condition is enough - if (this.game.score != "*") - { - e.preventDefault(); - if (e.deltaY < 0) - this.undo(); - else if (e.deltaY > 0) - this.play(); - } + e.preventDefault(); + if (e.deltaY < 0) this.undo(); + else if (e.deltaY > 0) this.play(); }, showRules: function() { //this.$router.push("/variants/" + this.game.vname); window.open("#/variants/" + this.game.vname, "_blank"); //better }, - re_setVariables: function() { + re_setVariables: function(game) { + if (!game) game = this.game; //in case of... this.endgameMessage = ""; - this.orientation = this.game.mycolor || "w"; //default orientation for observed games - this.moves = JSON.parse(JSON.stringify(this.game.moves || [])); - // Post-processing: decorate each move with color + current FEN: - // (to be able to jump to any position quickly) - let vr_tmp = new V(this.game.fenStart); //vr is already at end of game - this.firstMoveNumber = - Math.floor(V.ParseFen(this.game.fenStart).movesCount / 2); + // "w": default orientation for observed games + this.orientation = game.mycolor || "w"; + this.moves = JSON.parse(JSON.stringify(game.moves || [])); + // Post-processing: decorate each move with notation and FEN + this.vr = new V(game.fenStart); + const parsedFen = V.ParseFen(game.fenStart); + const firstMoveColor = parsedFen.turn; + this.firstMoveNumber = Math.floor(parsedFen.movesCount / 2); + let L = this.moves.length; this.moves.forEach(move => { - // NOTE: this is doing manually what play() function below achieve, - // but in a lighter "fast-forward" way - move.color = vr_tmp.turn; - move.notation = vr_tmp.getNotation(move); - vr_tmp.play(move); - move.fen = vr_tmp.getFen(); + // Strategy working also for multi-moves: + if (!Array.isArray(move)) move = [move]; + move.forEach((m,idx) => { + m.notation = this.vr.getNotation(m); + this.vr.play(m); + if (idx < L - 1 && this.vr.getCheckSquares(this.vr.turn).length > 0) + m.notation += "+"; + }); }); - if (this.game.fenStart.indexOf(" b ") >= 0 || - (this.moves.length > 0 && this.moves[0].color == "b")) - { - // 'end' is required for Board component to check lastMove for e.p. - this.moves.unshift({color: "w", notation: "...", end: {x:-1,y:-1}}); + if (firstMoveColor == "b") { + // 'start' & 'end' is required for Board component + this.moves.unshift({ + notation: "...", + start: { x: -1, y: -1 }, + end: { x: -1, y: -1 }, + fen: game.fenStart + }); + L++; + } + this.positionCursorTo(this.moves.length - 1); + this.incheck = this.vr.getCheckSquares(this.vr.turn); + const score = this.vr.getCurrentScore(); + if (L > 0 && this.moves[L - 1].notation != "...") { + if (["1-0","0-1"].includes(score)) + this.moves[L - 1].notation += "#"; + else if (this.vr.getCheckSquares(this.vr.turn).length > 0) + this.moves[L - 1].notation += "+"; } - const L = this.moves.length; - this.cursor = L-1; - this.lastMove = (L > 0 ? this.moves[L-1] : null); + }, + positionCursorTo: function(index) { + this.cursor = index; + // Caution: last move in moves array might be a multi-move + if (index >= 0) { + if (Array.isArray(this.moves[index])) { + const L = this.moves[index].length; + this.lastMove = this.moves[index][L - 1]; + } else { + this.lastMove = this.moves[index]; + } + } else this.lastMove = null; }, analyzePosition: function() { - const newUrl = "/analyze/" + this.game.vname + - "/?fen=" + this.vr.getFen().replace(/ /g, "_"); - if (this.game.type == "live") - this.$router.push(newUrl); //open in same tab: against cheating... - else - window.open("#" + newUrl); //open in a new tab: more comfortable + let newUrl = + "/analyse/" + + this.game.vname + + "/?fen=" + + this.vr.getFen().replace(/ /g, "_"); + if (this.game.mycolor) + newUrl += "&side=" + this.game.mycolor; + // Open in same tab in live games (against cheating) + if (this.game.type == "live") this.$router.push(newUrl); + else window.open("#" + newUrl); }, download: function() { const content = this.getPgn(); // 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(); }, getPgn: function() { @@ -209,182 +260,307 @@ console.log(newMove); pgn += '[Black "' + this.game.players[1].name + '"]\n'; pgn += '[Fen "' + this.game.fenStart + '"]\n'; pgn += '[Result "' + this.game.score + '"]\n\n'; - let counter = 1; - let i = 0; - while (i < this.moves.length) - { - pgn += (counter++) + "."; - for (let color of ["w","b"]) - { - let move = ""; - while (i < this.moves.length && this.moves[i].color == color) - move += this.moves[i++].notation + ","; - move = move.slice(0,-1); //remove last comma - pgn += move + (i < this.moves.length ? " " : ""); - } + for (let i = 0; i < this.moves.length; i += 2) { + pgn += (i/2+1) + "." + getFullNotation(this.moves[i]) + " "; + if (i+1 < this.moves.length) + pgn += getFullNotation(this.moves[i+1]) + " "; } return pgn + "\n"; }, - getScoreMessage: function(score) { - let eogMessage = "Undefined"; - switch (score) - { - case "1-0": - eogMessage = this.st.tr["White win"]; - break; - case "0-1": - eogMessage = this.st.tr["Black win"]; - break; - case "1/2": - eogMessage = this.st.tr["Draw"]; - break; - case "?": - eogMessage = this.st.tr["Unfinished"]; - break; - } - return eogMessage; - }, showEndgameMsg: function(message) { this.endgameMessage = message; - let modalBox = document.getElementById("modalEog"); - modalBox.checked = true; - setTimeout(() => { modalBox.checked = false; }, 2000); + document.getElementById("modalEog").checked = true; }, + runAutoplay: function() { + const infinitePlay = () => { + if (this.cursor == this.moves.length - 1) { + clearInterval(this.autoplayLoop); + this.autoplayLoop = null; + this.autoplay = false; + return; + } + if (this.inPlay || this.inMultimove) + // Wait next tick + return; + this.play(); + }; + if (this.autoplay) { + this.autoplay = false; + clearInterval(this.autoplayLoop); + this.autoplayLoop = null; + } else { + this.autoplay = true; + infinitePlay(); + this.autoplayLoop = setInterval(infinitePlay, 1500); + } + }, + // Animate an elementary move animateMove: function(move, callback) { let startSquare = document.getElementById(getSquareId(move.start)); + if (!startSquare) return; //shouldn't happen but... let endSquare = document.getElementById(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("#" + getSquareId(move.start) + " > img.piece"); + let translation = { + x: rectEnd.x - rectStart.x, + y: rectEnd.y - rectStart.y + }; + let movingPiece = document.querySelector( + "#" + getSquareId(move.start) + " > img.piece" + ); + // For some unknown reasons Opera get "movingPiece == null" error + // TOOO: is it calling 'animate()' twice ? One extra time ? + if (!movingPiece) return; // HACK for animation (with positive translate, image slides "under background") // Possible improvement: just alter squares on the piece's way... const squares = document.getElementsByClassName("board"); - for (let i=0; i { - for (let i=0; i { + 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 callback(); }, 250); }, - play: function(move, receive) { - // NOTE: navigate and receive are mutually exclusive - const navigate = !move; - // Forbid playing outside analyze mode, except if move is received. - // Sufficient condition because Board already knows which turn it is. - if (!navigate && this.game.mode!="analyze" && !receive - && (this.game.score != "*" || this.cursor < this.moves.length-1)) - { - return; + // For Analyse mode: + emitFenIfAnalyze: function() { + if (this.game.mode == "analyze") { + this.$emit( + "fenchange", + !!this.lastMove ? this.lastMove.fen : this.game.fenStart + ); } - const doPlayMove = () => { - if (!!receive && this.cursor < this.moves.length-1) - this.gotoEnd(); //required to play the move - if (navigate) - { - if (this.cursor == this.moves.length-1) - return; //no more moves - move = this.moves[this.cursor+1]; - } - else - { - move.color = this.vr.turn; - move.notation = this.vr.getNotation(move); + }, + // "light": if gotoMove() or gotoEnd() + play: function(move, received, light, noemit) { + // Freeze while choices are shown: + if (this.$refs["board"].choices.length > 0) return; + if (!!noemit) { + if (this.inPlay) { + // Received moves in observed games can arrive too fast: + this.stackToPlay.unshift(move); + return; } - this.vr.play(move); - this.cursor++; - this.lastMove = move; - if (this.st.settings.sound == 2) - new Audio("/sounds/move.mp3").play().catch(err => {}); - if (!navigate) - { - move.fen = this.vr.getFen(); - // Stack move on movesList at current cursor - if (this.cursor == this.moves.length) - this.moves.push(move); + this.inPlay = true; + } + const navigate = !move; + const playSubmove = (smove) => { + smove.notation = this.vr.getNotation(smove); + this.vr.play(smove); + this.lastMove = smove; + if (!this.inMultimove) { + // Condition is "!navigate" but we mean "!this.autoplay" + if (!navigate) { + if (this.cursor < this.moves.length - 1) + this.moves = this.moves.slice(0, this.cursor + 1); + this.moves.push(smove); + } + this.inMultimove = true; //potentially + this.cursor++; + } else if (!navigate) { + // Already in the middle of a multi-move + const L = this.moves.length; + if (!Array.isArray(this.moves[L-1])) + this.$set(this.moves, L-1, [this.moves[L-1], smove]); else - this.moves = this.moves.slice(0,this.cursor).concat([move]); + this.$set(this.moves, L-1, this.moves.concat([smove])); } - // Is opponent in check? - this.incheck = this.vr.getCheckSquares(this.vr.turn); + }; + const playMove = () => { + const animate = ( + V.ShowMoves == "all" && + (this.autoplay || !!received) + ); + if (!Array.isArray(move)) move = [move]; + let moveIdx = 0; + let self = this; + const initurn = this.vr.turn; + (function executeMove() { + const smove = move[moveIdx++]; + if (animate) { + self.animateMove(smove, () => { + playSubmove(smove); + if (moveIdx < move.length) + setTimeout(executeMove, 500); + else afterMove(smove, initurn); + }); + } else { + playSubmove(smove); + if (moveIdx < move.length) executeMove(); + else afterMove(smove, initurn); + } + })(); + }; + const computeScore = () => { const score = this.vr.getCurrentScore(); - if (score != "*") - { - const message = this.getScoreMessage(score); - if (this.game.mode != "analyze") - this.$emit("gameover", score, message); - else //just show score on screen (allow undo) - this.showEndgameMsg(score + " . " + message); + if (!navigate) { + if (["1-0","0-1"].includes(score)) + this.lastMove.notation += "#"; + else if (this.vr.getCheckSquares(this.vr.turn).length > 0) + this.lastMove.notation += "+"; } - if (!navigate && this.game.mode!="analyze") - this.$emit("newmove", move); //post-processing (e.g. computer play) + if (score != "*" && this.game.mode == "analyze") { + const message = getScoreMessage(score); + // Just show score on screen (allow undo) + this.showEndgameMsg(score + " . " + this.st.tr[message]); + } + return score; }; - if (!!receive && this.game.vname != "Dark") - this.animateMove(move, doPlayMove); - else - doPlayMove(); - }, - undo: function(move) { - const navigate = !move; - if (navigate) - { - if (this.cursor < 0) - return; //no more moves - move = this.moves[this.cursor]; + const afterMove = (smove, initurn) => { + if (this.vr.turn != initurn) { + // Turn has changed: move is complete + if (!smove.fen) + // NOTE: only FEN of last sub-move is required (thus setting it here) + smove.fen = this.vr.getFen(); + // Is opponent in check? + this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.emitFenIfAnalyze(); + this.inMultimove = false; + this.score = computeScore(); + if (this.game.mode != "analyze" && !navigate) { + if (!noemit) { + // Post-processing (e.g. computer play). + const L = this.moves.length; + // NOTE: always emit the score, even in unfinished, + // to tell Game::processMove() that it's not a received move. + this.$emit("newmove", this.moves[L-1], { score: this.score }); + } else { + this.inPlay = false; + if (this.stackToPlay.length > 0) + // Move(s) arrived in-between + this.play(this.stackToPlay.pop(), received, light, noemit); + } + } + } + }; + // NOTE: navigate and received are mutually exclusive + if (navigate) { + // The move to navigate to is necessarily full: + if (this.cursor == this.moves.length - 1) return; //no more moves + move = this.moves[this.cursor + 1]; + if (!this.autoplay) { + // Just play the move: + if (!Array.isArray(move)) move = [move]; + for (let i=0; i < move.length; i++) this.vr.play(move[i]); + if (!light) { + this.lastMove = move[move.length-1]; + this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.score = computeScore(); + this.emitFenIfAnalyze(); + } + this.cursor++; + return; + } } - this.vr.undo(move); + // Forbid playing outside analyze mode, except if move is received. + // Sufficient condition because Board already knows which turn it is. + if ( + this.game.mode != "analyze" && + !navigate && + !received && + (this.game.score != "*" || this.cursor < this.moves.length - 1) + ) { + return; + } + // To play a received move, cursor must be at the end of the game: + if (received && this.cursor < this.moves.length - 1) + this.gotoEnd(); + playMove(); + }, + cancelCurrentMultimove: function() { + const L = this.moves.length; + let move = this.moves[L-1]; + if (!Array.isArray(move)) move = [move]; + for (let i=move.length -1; i >= 0; i--) this.vr.undo(move[i]); + this.moves.pop(); this.cursor--; - this.lastMove = (this.cursor >= 0 ? this.moves[this.cursor] : undefined); - if (this.st.settings.sound == 2) - new Audio("/sounds/undo.mp3").play().catch(err => {}); - this.incheck = this.vr.getCheckSquares(this.vr.turn); - if (!navigate) - this.moves.pop(); + this.inMultimove = false; }, - gotoMove: function(index) { - this.vr.re_init(this.moves[index].fen); - this.cursor = index; - this.lastMove = this.moves[index]; + cancelLastMove: function() { + // The last played move was canceled (corr game) + this.undo(); + this.moves.pop(); }, - gotoBegin: function() { - if (this.cursor == -1) - return; - this.vr.re_init(this.game.fenStart); - if (this.moves.length > 0 && this.moves[0].notation == "...") - { - this.cursor = 0; - this.lastMove = this.moves[0]; + // "light": if gotoMove() or gotoBegin() + undo: function(move, light) { + // Freeze while choices are shown: + if (this.$refs["board"].choices.length > 0) return; + if (this.inMultimove) { + this.cancelCurrentMultimove(); + this.incheck = this.vr.getCheckSquares(this.vr.turn); + } else { + if (!move) { + const minCursor = + this.moves.length > 0 && this.moves[0].notation == "..." + ? 1 + : 0; + if (this.cursor < minCursor) return; //no more moves + move = this.moves[this.cursor]; + } + undoMove(move, this.vr); + if (light) this.cursor--; + else { + this.positionCursorTo(this.cursor - 1); + this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.emitFenIfAnalyze(); + } } - else - { - this.cursor = -1; - this.lastMove = null; + }, + gotoMove: function(index) { + if (this.$refs["board"].choices.length > 0) return; + if (this.inMultimove) this.cancelCurrentMultimove(); + if (index == this.cursor) return; + if (index < this.cursor) { + while (this.cursor > index) + this.undo(null, null, "light"); } + else { + // index > this.cursor) + while (this.cursor < index) + this.play(null, null, "light"); + } + // NOTE: next line also re-assign cursor, but it's very light + this.positionCursorTo(index); + this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.emitFenIfAnalyze(); + }, + gotoBegin: function() { + if (this.$refs["board"].choices.length > 0) return; + if (this.inMultimove) this.cancelCurrentMultimove(); + const minCursor = + this.moves.length > 0 && this.moves[0].notation == "..." + ? 1 + : 0; + while (this.cursor >= minCursor) this.undo(null, null, "light"); + this.lastMove = (minCursor == 1 ? this.moves[0] : null); + this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.emitFenIfAnalyze(); }, gotoEnd: function() { - if (this.cursor == this.moves.length - 1) - return; - this.gotoMove(this.moves.length-1); + if (this.$refs["board"].choices.length > 0) return; + if (this.cursor == this.moves.length - 1) return; + this.gotoMove(this.moves.length - 1); + this.emitFenIfAnalyze(); }, flip: function() { + if (this.$refs["board"].choices.length > 0) return; this.orientation = V.GetOppCol(this.orientation); - }, - }, + } + } };