X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fcomponents%2FBaseGame.vue;h=70a304b8711fcf287cf44ca4f98be0b3aa11ae33;hb=107dc1bd5361e2538b1551bdcc37c1e90a444b83;hp=fd26d87204f5c0b9e48743c121c0e36bb792183b;hpb=1ef65040168ab7d55ce921abc9d63644a937d689;p=vchess.git diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index fd26d872..70a304b8 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -14,7 +14,7 @@ div#baseGame ref="board" :vr="vr" :last-move="lastMove" - :analyze="game.mode=='analyze'" + :analyze="mode=='analyze'" :score="game.score" :user-color="game.mycolor" :orientation="orientation" @@ -40,6 +40,7 @@ div#baseGame img.inline(src="/images/icons/play.svg") button(@click="gotoEnd()") img.inline(src="/images/icons/fast-forward.svg") + p#fenAnalyze(v-show="showFen") {{ (!!vr ? vr.getFen() : "") }} #movesList MoveList( :show="showMoves" @@ -52,9 +53,9 @@ div#baseGame :cursor="cursor" @download="download" @showrules="showRules" - @analyze="analyzePosition" + @analyze="toggleAnalyze" @goto-move="gotoMove" - @reset-arrows="resetArrows" + @redraw-board="redrawBoard" ) .clearer @@ -84,6 +85,7 @@ export default { vr: null, //VariantRules object, game state endgameMessage: "", orientation: "w", + mode: "", score: "*", //'*' means 'unfinished' moves: [], cursor: -1, //index of the move just played @@ -92,7 +94,6 @@ export default { incheck: [], //for Board inMultimove: false, autoplay: false, - autoplayLoop: null, inPlay: false, stackToPlay: [] }; @@ -104,13 +105,19 @@ export default { return this.st.tr[ (this.vr.turn == 'w' ? "White" : "Black") + " to move"]; } - // Cannot flip: racing king or circular chess + // Cannot flip (racing king or circular chess), or Monochrome return ( this.vr.movesCount == 0 && this.game.mycolor == "w" ? this.st.tr["It's your turn!"] : "" ); }, + showFen: function() { + return ( + this.mode == "analyze" && + this.$router.currentRoute.path.indexOf("/analyse") === -1 + ); + }, // TODO: is it OK to pass "computed" as properties? // Also, some are seemingly not recomputed when vr is initialized. showMoves: function() { @@ -121,7 +128,12 @@ export default { showTurn: function() { return ( this.game.score == '*' && - !!this.vr && (this.vr.showMoves != "all" || !this.vr.canFlip) + !!this.vr && + ( + this.vr.showMoves != "all" || + !this.vr.canFlip || + this.vr.showFirstTurn + ) ); }, canAnalyze: function() { @@ -156,7 +168,8 @@ export default { .addEventListener("click", processModalClick); }, beforeDestroy: function() { - if (!!this.autoplayLoop) clearInterval(this.autoplayLoop); + // TODO: probably not required + this.autoplay = false; }, methods: { focusBg: function() { @@ -187,42 +200,65 @@ export default { if (e.deltaY < 0) this.undo(); else if (e.deltaY > 0) this.play(); }, - resetArrows: function() { - // TODO: make arrows scale with board, and remove this - this.$refs["board"].cancelResetArrows(); + redrawBoard: function() { + this.$refs["board"].re_setDrawings(); }, showRules: function() { - //this.$router.push("/variants/" + this.game.vname); - window.open("#/variants/" + this.game.vname, "_blank"); //better + // The button is here only on Game page: + document.getElementById("modalRules").checked = true; }, re_setVariables: function(game) { if (!game) game = this.game; //in case of... this.endgameMessage = ""; // "w": default orientation for observed games this.orientation = game.mycolor || "w"; + this.mode = game.mode || game.type; //TODO: merge... this.moves = JSON.parse(JSON.stringify(game.moves || [])); // Post-processing: decorate each move with notation and FEN this.vr = new V(game.fenStart); + this.inMultimove = false; //in case of + if (!!this.$refs["board"]) + // Also in case of: + this.$refs["board"].resetCurrentAttempt(); + let analyseBtn = document.getElementById("analyzeBtn"); + if (!!analyseBtn) analyseBtn.classList.remove("active"); const parsedFen = V.ParseFen(game.fenStart); const firstMoveColor = parsedFen.turn; - this.firstMoveNumber = Math.floor(parsedFen.movesCount / 2); + this.firstMoveNumber = Math.floor(parsedFen.movesCount / 2) + 1; let L = this.moves.length; - this.moves.forEach(move => { - // Strategy working also for multi-moves: - if (!Array.isArray(move)) move = [move]; - move.forEach((m,idx) => { - m.index = this.vr.movesCount; - m.notation = this.vr.getNotation(m); - m.unambiguous = V.GetUnambiguousNotation(m); - this.vr.play(m); - if (idx < L - 1 && this.vr.getCheckSquares(this.vr.turn).length > 0) - m.notation += "+"; + if (L == 0) { + // Could be started on a random position in analysis mode: + this.incheck = this.vr.getCheckSquares(); + this.score = this.vr.getCurrentScore(); + if (this.score != '*') { + // Show score on screen + const message = getScoreMessage(this.score); + this.showEndgameMsg(this.score + " . " + this.st.tr[message]); + } + } + else { + this.moves.forEach((move,idx) => { + // Strategy working also for multi-moves: + if (!Array.isArray(move)) move = [move]; + const Lm = move.length; + move.forEach((m,idxM) => { + m.notation = this.vr.getNotation(m); + m.unambiguous = V.GetUnambiguousNotation(m); + this.vr.play(m); + const checkSquares = this.vr.getCheckSquares(); + if (checkSquares.length > 0) m.notation += "+"; + if (idxM == Lm - 1) m.fen = this.vr.getFen(); + if (idx == L - 1 && idxM == Lm - 1) { + this.incheck = checkSquares; + this.score = this.vr.getCurrentScore(); + if (["1-0", "0-1"].includes(this.score)) m.notation += "#"; + } + }); }); - }); + } if (firstMoveColor == "b") { // 'start' & 'end' is required for Board component this.moves.unshift({ - index: parsedFen.movesCount, notation: "...", unambiguous: "...", start: { x: -1, y: -1 }, @@ -231,34 +267,45 @@ export default { }); 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.incheck.length > 0) this.moves[L - 1].notation += "+"; - } + this.positionCursorTo(L - 1); }, 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; + // Note: last move in moves array might be a multi-move + if (index >= 0) this.lastMove = this.moves[index]; + else this.lastMove = null; }, - analyzePosition: function() { - let newUrl = - "/analyse/" + - this.game.vname + - "/?fen=" + - this.vr.getFen().replace(/ /g, "_"); - if (!!this.game.mycolor) newUrl += "&side=" + this.game.mycolor; - window.open("#" + newUrl); + toggleAnalyze: function() { + // Freeze while choices are shown (and autoplay has priority) + if (this.$refs["board"].choices.length > 0 || this.autoplay) return; + if (this.mode != "analyze") { + // Enter analyze mode: + if (this.inMultimove) this.cancelCurrentMultimove(); + this.gameMode = this.mode; //was not 'analyze' + this.mode = "analyze"; + this.gameCursor = this.cursor; + this.gameMoves = JSON.parse(JSON.stringify(this.moves)); + document.getElementById("analyzeBtn").classList.add("active"); + } + else { + // Exit analyze mode: + this.mode = this.gameMode ; + this.cursor = this.gameCursor; + this.moves = this.gameMoves; + let fen = this.game.fenStart; + if (this.cursor >= 0) { + let mv = this.moves[this.cursor]; + if (!Array.isArray(mv)) mv = [mv]; + fen = mv[mv.length-1].fen; + } + this.vr = new V(fen); + this.inMultimove = false; //in case of + this.$refs["board"].resetCurrentAttempt(); //also in case of + this.incheck = this.vr.getCheckSquares(); + if (this.cursor >= 0) this.lastMove = this.moves[this.cursor]; + else this.lastMove = null; + document.getElementById("analyzeBtn").classList.remove("active"); + } }, download: function() { const content = this.getPgn(); @@ -279,23 +326,23 @@ export default { pgn += '[Black "' + this.game.players[1].name + '"]\n'; pgn += '[Fen "' + this.game.fenStart + '"]\n'; pgn += '[Result "' + this.game.score + '"]\n'; - if (!!this.game.id) { - pgn += '[Cadence "' + this.game.cadence + '"]\n'; + if (!!this.game.id) pgn += '[Url "' + params.serverUrl + '/game/' + this.game.id + '"]\n'; - } + if (!!this.game.cadence) + pgn += '[Cadence "' + this.game.cadence + '"]\n'; pgn += '\n'; for (let i = 0; i < this.moves.length; i += 2) { if (i > 0) pgn += " "; // Adjust dots notation for a better display: let fullNotation = getFullNotation(this.moves[i]); if (fullNotation == "...") fullNotation = ".."; - pgn += (this.moves[i].index / 2 + 1) + "." + fullNotation; + pgn += (i / 2 + this.firstMoveNumber) + "." + fullNotation; if (i+1 < this.moves.length) pgn += " " + getFullNotation(this.moves[i+1]); } pgn += "\n\n"; for (let i = 0; i < this.moves.length; i += 2) { - const moveNumber = this.moves[i].index / 2 + 1; + const moveNumber = i / 2 + this.firstMoveNumber; // Skip "dots move", useless for machine reading: if (this.moves[i].notation != "...") { pgn += moveNumber + ".w " + @@ -313,32 +360,15 @@ export default { 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 { + if (this.stackToPlay.length > 0) + // Move(s) arrived in-between + this.play(this.stackToPlay.pop(), "received"); + } + else if (this.cursor < this.moves.length - 1) { this.autoplay = true; - setTimeout( - () => { - infinitePlay(); - this.autoplayLoop = setInterval(infinitePlay, 1500); - }, - // Small delay otherwise the first move is played too fast - 500 - ); + this.play(null, null, null, "autoplay"); } }, // Animate an elementary move @@ -380,10 +410,15 @@ export default { // For Analyse mode: emitFenIfAnalyze: function() { if (this.game.mode == "analyze") { - this.$emit( - "fenchange", - !!this.lastMove ? this.lastMove.fen : this.game.fenStart - ); + let fen = this.game.fenStart; + if (!!this.lastMove) { + if (Array.isArray(this.lastMove)) { + const L = this.lastMove.length; + fen = this.lastMove[L-1].fen; + } + else fen = this.lastMove.fen; + } + this.$emit("fenchange", fen); } }, clickSquare: function(square) { @@ -392,24 +427,57 @@ export default { if (!!move) this.play(move); }, // "light": if gotoMove() or gotoEnd() - play: function(move, received, light, noemit) { + play: function(move, received, light, autoplay) { // 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: + if ( + !!this.$refs["board"].selectedPiece || + this.$refs["board"].choices.length > 0 + ) { + return; + } + const navigate = !move; + // Forbid navigation during autoplay: + if (navigate && this.autoplay && !autoplay) return; + // Forbid playing outside analyze mode, except if move is received. + // Sufficient condition because Board already knows which turn it is. + if ( + this.mode != "analyze" && + !navigate && + !received && + (this.game.score != "*" || this.cursor < this.moves.length - 1) + ) { + return; + } + if (!!received) { + if (this.autoplay || this.inPlay) { + // Received moves while autoplaying are stacked, + // and in observed games they could arrive too fast: this.stackToPlay.unshift(move); return; } this.inPlay = true; + if (this.mode == "analyze") this.toggleAnalyze(); + if (this.cursor < this.moves.length - 1) + // To play a received move, cursor must be at the end of the game: + this.gotoEnd(); } - const navigate = !move; + // The board may show some the possible moves: (TODO: bad solution) + this.$refs["board"].resetCurrentAttempt(); const playSubmove = (smove) => { smove.notation = this.vr.getNotation(smove); smove.unambiguous = V.GetUnambiguousNotation(smove); this.vr.play(smove); - this.lastMove = smove; + if (this.inMultimove && !!this.lastMove) { + if (!Array.isArray(this.lastMove)) + this.lastMove = [this.lastMove, smove]; + else this.lastMove.push(smove); + } + // Is opponent (or me) in check? + this.incheck = this.vr.getCheckSquares(); + if (this.incheck.length > 0) smove.notation += "+"; if (!this.inMultimove) { + // First sub-move: + this.lastMove = smove; // Condition is "!navigate" but we mean "!this.autoplay" if (!navigate) { if (this.cursor < this.moves.length - 1) @@ -423,8 +491,7 @@ export default { 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])); + else this.moves[L-1].push(smove); } }; const playMove = () => { @@ -438,11 +505,12 @@ export default { const initurn = this.vr.turn; (function executeMove() { const smove = move[moveIdx++]; - if (animate) { + // NOTE: condition "smove.start.x >= 0" required for Dynamo, + // because second move may be empty. + if (animate && smove.start.x >= 0) { self.animateMove(smove, () => { playSubmove(smove); - if (moveIdx < move.length) - setTimeout(executeMove, 500); + if (moveIdx < move.length) setTimeout(executeMove, 500); else afterMove(smove, initurn); }); } else { @@ -455,12 +523,17 @@ export default { const computeScore = () => { const score = this.vr.getCurrentScore(); if (!navigate) { - if (["1-0","0-1"].includes(score)) this.lastMove.notation += "#"; - else if (this.incheck.length > 0) this.lastMove.notation += "+"; + if (["1-0", "0-1"].includes(score)) { + if (Array.isArray(this.lastMove)) { + const L = this.lastMove.length; + this.lastMove[L - 1].notation += "#"; + } + else this.lastMove.notation += "#"; + } } - if (score != "*" && this.game.mode == "analyze") { + if (score != "*" && ["analyze", "versus"].includes(this.mode)) { const message = getScoreMessage(score); - // Just show score on screen (allow undo) + // Show score on screen this.showEndgameMsg(score + " . " + this.st.tr[message]); } return score; @@ -471,23 +544,30 @@ export default { if (!smove.fen) // NOTE: only FEN of last sub-move is required (=> 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) { + if (this.autoplay) { + if (this.cursor < this.moves.length - 1) + setTimeout(() => this.play(null, null, null, "autoplay"), 1000); + else { + this.autoplay = false; + if (this.stackToPlay.length > 0) + // Move(s) arrived in-between + this.play(this.stackToPlay.pop(), "received"); + } + } + if (this.mode != "analyze" && !navigate) { + if (!received) { // 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. + // NOTE: always emit the score, even in unfinished 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); + this.play(this.stackToPlay.pop(), "received"); } } } @@ -502,8 +582,8 @@ export default { 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.lastMove = move; + this.incheck = this.vr.getCheckSquares(); this.score = computeScore(); this.emitFenIfAnalyze(); } @@ -511,26 +591,13 @@ export default { return; } } - // 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]); + for (let i = move.length - 1; i >= 0; i--) this.vr.undo(move[i]); this.moves.pop(); this.cursor--; this.inMultimove = false; @@ -542,11 +609,19 @@ export default { }, // "light": if gotoMove() or gotoBegin() undo: function(move, light) { - // Freeze while choices are shown: - if (this.$refs["board"].choices.length > 0) return; + if ( + this.autoplay || + !!this.$refs["board"].selectedPiece || + this.$refs["board"].choices.length > 0 + ) { + return; + } + this.$refs["board"].resetCurrentAttempt(); if (this.inMultimove) { this.cancelCurrentMultimove(); - this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.incheck = this.vr.getCheckSquares(); + if (this.cursor >= 0) this.lastMove = this.moves[this.cursor]; + else this.lastMove = null; } else { if (!move) { const minCursor = @@ -556,17 +631,25 @@ export default { if (this.cursor < minCursor) return; //no more moves move = this.moves[this.cursor]; } + this.$refs["board"].resetCurrentAttempt(); undoMove(move, this.vr); if (light) this.cursor--; else { this.positionCursorTo(this.cursor - 1); - this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.incheck = this.vr.getCheckSquares(); this.emitFenIfAnalyze(); } } }, gotoMove: function(index) { - if (this.$refs["board"].choices.length > 0) return; + if ( + this.autoplay || + !!this.$refs["board"].selectedPiece || + this.$refs["board"].choices.length > 0 + ) { + return; + } + this.$refs["board"].resetCurrentAttempt(); if (this.inMultimove) this.cancelCurrentMultimove(); if (index == this.cursor) return; if (index < this.cursor) { @@ -580,11 +663,18 @@ export default { } // NOTE: next line also re-assign cursor, but it's very light this.positionCursorTo(index); - this.incheck = this.vr.getCheckSquares(this.vr.turn); + this.incheck = this.vr.getCheckSquares(); this.emitFenIfAnalyze(); }, gotoBegin: function() { - if (this.$refs["board"].choices.length > 0) return; + if ( + this.autoplay || + !!this.$refs["board"].selectedPiece || + this.$refs["board"].choices.length > 0 + ) { + return; + } + this.$refs["board"].resetCurrentAttempt(); if (this.inMultimove) this.cancelCurrentMultimove(); const minCursor = this.moves.length > 0 && this.moves[0].notation == "..." @@ -592,14 +682,12 @@ export default { : 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.incheck = this.vr.getCheckSquares(); this.emitFenIfAnalyze(); }, gotoEnd: function() { - 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; @@ -612,6 +700,7 @@ export default {