X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fcomponents%2FBaseGame.vue;h=488567b5b3ec9b22cc4f1c2261104c6482f19702;hb=e71161fbfffe53b0f4b174e0467cdd98cc70b7b0;hp=ef6ef01e17f3b5d20870301e4d680a2b163dba71;hpb=9ef63965401837983286ac6a95767af3923405fe;p=vchess.git diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index ef6ef01e..488567b5 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -1,46 +1,75 @@ @@ -52,26 +81,30 @@ 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' moves: [], + // TODO: later, use subCursor to navigate intra-multimoves? cursor: -1, //index of the move just played lastMove: null, firstMoveNumber: 0, //for printing incheck: [], //for Board + inMultimove: false }; }, watch: { @@ -79,63 +112,76 @@ export default { "game.fenStart": function() { this.re_setVariables(); }, - // Received a new move to play: - "game.moveToPlay": function(newMove) { - if (!!newMove) //if stop + launch new game, get undefined move - this.play(newMove, "receive"); - }, - // ...Or to undo (corr game, move not validated) - "game.moveToUndo": function(move) { - if (!!move) - this.undo(move); - }, }, 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.showMoves != "all" || !this.vr.canFlip) + ); }, 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 this.st.tr[color + " to move"]; + 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() { - [document.getElementById("eogDiv"),document.getElementById("adjuster")] - .forEach(elt => elt.addEventListener("click", processModalClick)); + 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); + } + [ + document.getElementById("eogDiv"), + document.getElementById("adjuster") + ].forEach(elt => elt.addEventListener("click", processModalClick)); // Take full width on small screens: let boardSize = parseInt(localStorage.getItem("boardSize")); - if (!boardSize) - { - boardSize = (window.innerWidth >= 768 - ? 0.75 * Math.min(window.innerWidth, window.innerHeight) - : window.innerWidth); + if (!boardSize) { + boardSize = + window.innerWidth >= 768 + ? 0.75 * Math.min(window.innerWidth, window.innerHeight) + : window.innerWidth; } - const movesWidth = (window.innerWidth >= 768 ? 280 : 0); + 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("boardSize").value = (boardSize * 100) / (window.innerWidth - movesWidth); + gameContainer.style.width = boardSize + movesWidth + "px"; + document.getElementById("boardSize").value = + (boardSize * 100) / (window.innerWidth - movesWidth); // timeout to avoid calling too many time the adjust method let timeoutLaunched = false; - window.addEventListener("resize", (e) => { - if (!timeoutLaunched) - { + window.addEventListener("resize", () => { + if (!timeoutLaunched) { timeoutLaunched = true; - setTimeout( () => { + setTimeout(() => { this.adjustBoard(); timeoutLaunched = false; }, 500); @@ -144,29 +190,26 @@ export default { }, methods: { focusBg: function() { - // NOTE: small blue border appears... document.getElementById("baseGame").focus(); }, adjustBoard: function() { const boardContainer = document.getElementById("boardContainer"); - if (!boardContainer) - return; //no board on page + if (!boardContainer) return; //no board on page const k = document.getElementById("boardSize").value; - const movesWidth = (window.innerWidth >= 768 ? 280 : 0); + const movesWidth = window.innerWidth >= 768 ? 280 : 0; const minBoardWidth = 240; //TODO: these 240 and 280 are arbitrary... // Value of 0 is board min size; 100 is window.width [- movesWidth] - const boardSize = minBoardWidth + - k * (window.innerWidth - (movesWidth+minBoardWidth)) / 100; + const boardSize = + minBoardWidth + + (k * (window.innerWidth - (movesWidth + minBoardWidth))) / 100; localStorage.setItem("boardSize", boardSize); boardContainer.style.width = boardSize + "px"; document.getElementById("gameContainer").style.width = - (boardSize + movesWidth) + "px"; + boardSize + movesWidth + "px"; }, 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; @@ -185,15 +228,9 @@ export default { } }, 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); @@ -201,46 +238,63 @@ export default { }, re_setVariables: function() { this.endgameMessage = ""; - this.orientation = this.game.mycolor || "w"; //default orientation for observed games + // "w": default orientation for observed games + this.orientation = this.game.mycolor || "w"; 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); + // Post-processing: decorate each move with notation and FEN + this.vr = new V(this.game.fenStart); + const parsedFen = V.ParseFen(this.game.fenStart); + const firstMoveColor = parsedFen.turn; + this.firstMoveNumber = Math.floor(parsedFen.movesCount / 2); 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 => { + m.notation = this.vr.getNotation(m); + this.vr.play(m); + }); }); - if ((this.moves.length > 0 && this.moves[0].color == "b") || - (this.moves.length == 0 && this.vr_tmp.turn == "b")) - { + if (firstMoveColor == "b") { // 'end' is required for Board component to check lastMove for e.p. - this.moves.unshift({color: "w", notation: "...", end: {x:-1,y:-1}}); + this.moves.unshift({ + notation: "...", + end: { x: -1, y: -1 } + }); } - const L = this.moves.length; - this.cursor = L-1; - this.lastMove = (L > 0 ? this.moves[L-1] : null); + this.positionCursorTo(this.moves.length - 1); this.incheck = this.vr.getCheckSquares(this.vr.turn); }, - analyzePosition: function() { - const newUrl = "/analyse/" + this.game.vname + - "/?fen=" + this.vr.getFen().replace(/ /g, "_"); - if (this.game.type == "live") - this.$router.push(newUrl); //open in same tab: against cheating... + 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 - window.open("#" + newUrl); //open in a new tab: more comfortable + this.lastMove = null; + }, + analyzePosition: function() { + const newUrl = + "/analyse/" + + this.game.vname + + "/?fen=" + + this.vr.getFen().replace(/ /g, "_"); + // 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() { @@ -252,19 +306,10 @@ export default { 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"; }, @@ -272,151 +317,218 @@ export default { this.endgameMessage = message; let modalBox = document.getElementById("modalEog"); modalBox.checked = true; - setTimeout(() => { modalBox.checked = false; }, 2000); + setTimeout(() => { + modalBox.checked = false; + }, 2000); }, + // Animate an elementary move animateMove: function(move, callback) { let startSquare = document.getElementById(getSquareId(move.start)); - // TODO: error "flush nextTick callbacks" when observer reloads page: - // this late check is not a fix! - if (!startSquare) - return; 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"); - if (!movingPiece) //TODO: shouldn't happen - return; + let translation = { + x: rectEnd.x - rectStart.x, + y: rectEnd.y - rectStart.y + }; + let movingPiece = document.querySelector( + "#" + getSquareId(move.start) + " > img.piece" + ); // 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 + // "light": if gotoMove() or gotoEnd() + // data: some custom data (addTime) to be re-emitted + play: function(move, received, light, data) { const navigate = !move; + const playSubmove = (smove) => { + if (!navigate) smove.notation = this.vr.getNotation(smove); + this.vr.play(smove); + this.lastMove = smove; + // Is opponent in check? + this.incheck = this.vr.getCheckSquares(this.vr.turn); + if (!navigate) { + if (!this.inMultimove) { + if (this.cursor < this.moves.length - 1) + this.moves = this.moves.slice(0, Math.max(this.cursor, 0)); + this.moves.push(smove); + this.inMultimove = true; //potentially + this.cursor++; + } else { + // 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.$set(this.moves, L-1, this.moves.concat([smove])); + } + } + }; + const playMove = () => { + const animate = V.ShowMoves == "all" && 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 afterMove = (smove, initurn) => { + if (this.st.settings.sound == 2) + new Audio("/sounds/move.mp3").play().catch(() => {}); + if (this.vr.turn != initurn) { + // Turn has changed: move is complete + this.inMultimove = false; + const score = this.vr.getCurrentScore(); + if (score != "*") { + const message = getScoreMessage(score); + if (!navigate && this.game.mode != "analyze") + this.$emit("gameover", score, message); + // Just show score on screen (allow undo) + else this.showEndgameMsg(score + " . " + message); + } + if (!navigate && this.game.mode != "analyze") { + const L = this.moves.length; + // Post-processing (e.g. computer play) + this.$emit("newmove", this.moves[L-1], data); + } + } + }; + // 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 (light) { + // Just play the move, nothing else: + if (!Array.isArray(move)) move = [move]; + for (let i=0; i < move.length; i++) this.vr.play(move[i]); + } + else playMove(); + this.cursor++; + return; + } // 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)) - { + if ( + this.game.mode != "analyze" && + !received && + (this.game.score != "*" || this.cursor < this.moves.length - 1) + ) { return; } - 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); - } - 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); - else - this.moves = this.moves.slice(0,this.cursor).concat([move]); - } - // Is opponent in check? + // 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() { + // Cancel current multi-move + 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.inMultimove = false; + }, + cancelLastMove: function() { + // The last played move was canceled (corr game) + this.undo(); + this.moves.pop(); + }, + // "light": if gotoMove() or gotoBegin() + undo: function(move, light) { + if (this.inMultimove) { + this.cancelCurrentMultimove(); this.incheck = this.vr.getCheckSquares(this.vr.turn); - const score = this.vr.getCurrentScore(); - if (score != "*") - { - const message = getScoreMessage(score); - if (this.game.mode != "analyze") - this.$emit("gameover", score, message); - else //just show score on screen (allow undo) - this.showEndgameMsg(score + " . " + message); + } else { + if (!move) { + if (this.cursor < 0) return; //no more moves + move = this.moves[this.cursor]; + } + // Caution; if multi-move, undo all submoves from last to first + undoMove(move, this.vr); + if (light) this.cursor--; + else { + this.positionCursorTo(this.cursor - 1); + if (this.st.settings.sound == 2) + new Audio("/sounds/undo.mp3").play().catch(() => {}); + this.incheck = this.vr.getCheckSquares(this.vr.turn); } - if (!navigate && this.game.mode!="analyze") - this.$emit("newmove", move); //post-processing (e.g. computer play) - }; - 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]; } - this.vr.undo(move); - 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(); }, gotoMove: function(index) { - this.vr.re_init(this.moves[index].fen); - this.cursor = index; - this.lastMove = this.moves[index]; + 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); }, gotoBegin: function() { - if (this.cursor == -1) - return; - this.vr.re_init(this.game.fenStart); - if (this.moves.length > 0 && this.moves[0].notation == "...") - { + if (this.inMultimove) this.cancelCurrentMultimove(); + while (this.cursor >= 0) + this.undo(null, null, "light"); + if (this.moves.length > 0 && this.moves[0].notation == "...") { this.cursor = 0; this.lastMove = this.moves[0]; - } - else - { - this.cursor = -1; + } else { this.lastMove = null; } + this.incheck = []; }, gotoEnd: function() { - if (this.cursor == this.moves.length - 1) - return; - this.gotoMove(this.moves.length-1); + if (this.cursor == this.moves.length - 1) return; + this.gotoMove(this.moves.length - 1); }, flip: function() { this.orientation = V.GetOppCol(this.orientation); - }, - }, + } + } };