X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=client%2Fsrc%2Fcomponents%2FBaseGame.vue;h=2517c78ab9832ed7685c4d6181bc8f8c15d25bf9;hp=1253c0634ea161e1ffd3c934033dee0a3a4de3f5;hb=d54f6261c9e30f4eabb402ad301dd5c5e40fb656;hpb=311cba767e3c461edb0c8c758bfb193ef670a68f diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 1253c063..2517c78a 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -8,24 +8,10 @@ div#baseGame .card.text-center label.modal-close(for="modalEog") h3.section {{ endgameMessage }} - input#modalAdjust.modal(type="checkbox") - div#adjuster( - role="dialog" - data-checkbox="modalAdjust" - ) - .card.text-center - label.modal-close(for="modalAdjust") - label(for="boardSize") {{ st.tr["Board size"] }} - input#boardSize.slider( - type="range" - min="0" - max="100" - value="50" - @input="adjustBoard()" - ) #gameContainer #boardContainer Board( + ref="board" :vr="vr" :last-move="lastMove" :analyze="game.mode=='analyze'" @@ -37,37 +23,35 @@ div#baseGame @play-move="play" ) #turnIndicator(v-if="showTurn") {{ turn }} - #controls - button(@click="gotoBegin()") << - button(@click="undo()") < - button(v-if="canFlip" @click="flip()") ⇅ - button(@click="play()") > - button(@click="gotoEnd()") >> - #belowControls - #downloadDiv(v-if="allowDownloadPGN") - a#download(href="#") - button(@click="download()") {{ st.tr["Download"] }} PGN - button(onClick="window.doClick('modalAdjust')") ⤢ - button( - v-if="canAnalyze" - @click="analyzePosition()" - ) - | {{ st.tr["Analyse"] }} - // NOTE: variants pages already have a "Rules" link on top + #controls.button-group + button(@click="gotoBegin()") + img.inline(src="/images/icons/fast-forward_rev.svg") + button(@click="undo()") + img.inline(src="/images/icons/play_rev.svg") + button(v-if="canFlip" @click="flip()") + img.inline(src="/images/icons/flip.svg") button( - v-if="!$route.path.match('/variants/')" - @click="showRules()" + @click="runAutoplay()" + :class="{'in-autoplay': autoplay}" ) - | {{ st.tr["Rules"] }} + img.inline(src="/images/icons/autoplay.svg") + button(@click="play()") + img.inline(src="/images/icons/play.svg") + button(@click="gotoEnd()") + img.inline(src="/images/icons/fast-forward.svg") #movesList MoveList( - v-if="showMoves != 'none'" :show="showMoves" + :canAnalyze="canAnalyze" + :canDownload="allowDownloadPGN" :score="game.score" :message="game.scoreMsg" :firstNum="firstMoveNumber" :moves="moves" :cursor="cursor" + @download="download" + @showrules="showRules" + @analyze="analyzePosition" @goto-move="gotoMove" ) .clearer @@ -99,20 +83,17 @@ export default { 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 + 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(); - }, - }, computed: { showMoves: function() { return this.game.score != "*" @@ -122,15 +103,13 @@ export default { showTurn: function() { return ( this.game.score == '*' && - this.vr && - (this.vr.showMoves != "all" || !this.vr.canFlip) + !!this.vr && this.vr.showTurn ); }, turn: function() { - if (!this.vr) - return ""; + if (!this.vr) return ""; if (this.vr.showMoves != "all") - return this.st.tr[(this.vr.turn == 'w' ? "White" : "Black") + " to move"] + 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!"] @@ -147,7 +126,7 @@ export default { } }, created: function() { - if (this.game.fenStart) this.re_setVariables(); + if (!!this.game.fenStart) this.re_setVariables(); }, mounted: function() { if (!("ontouchstart" in window)) { @@ -158,55 +137,16 @@ export default { 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; - } - 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); - // timeout to avoid calling too many time the adjust method - let timeoutLaunched = false; - window.addEventListener("resize", () => { - if (!timeoutLaunched) { - timeoutLaunched = true; - setTimeout(() => { - this.adjustBoard(); - timeoutLaunched = false; - }, 500); - } - }); + document.getElementById("eogDiv") + .addEventListener("click", processModalClick); + }, + beforeDestroy: function() { + if (!!this.autoplayLoop) clearInterval(this.autoplayLoop); }, methods: { focusBg: function() { document.getElementById("baseGame").focus(); }, - adjustBoard: function() { - const boardContainer = document.getElementById("boardContainer"); - if (!boardContainer) return; //no board on page - const k = document.getElementById("boardSize").value; - 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; - localStorage.setItem("boardSize", boardSize); - boardContainer.style.width = boardSize + "px"; - document.getElementById("gameContainer").style.width = - boardSize + movesWidth + "px"; - }, handleKeys: function(e) { if ([32, 37, 38, 39, 40].includes(e.keyCode)) e.preventDefault(); switch (e.keyCode) { @@ -236,22 +176,26 @@ export default { //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 = ""; // "w": default orientation for observed games - this.orientation = this.game.mycolor || "w"; - this.moves = JSON.parse(JSON.stringify(this.game.moves || [])); + 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(this.game.fenStart); - const parsedFen = V.ParseFen(this.game.fenStart); + 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 => { // Strategy working also for multi-moves: if (!Array.isArray(move)) move = [move]; - move.forEach(m => { + 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 (firstMoveColor == "b") { @@ -259,11 +203,20 @@ export default { this.moves.unshift({ notation: "...", start: { x: -1, y: -1 }, - end: { 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 += "+"; + } }, positionCursorTo: function(index) { this.cursor = index; @@ -275,16 +228,16 @@ export default { } else { this.lastMove = this.moves[index]; } - } - else - this.lastMove = null; + } else this.lastMove = null; }, analyzePosition: function() { - const newUrl = + 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); @@ -316,15 +269,35 @@ export default { }, 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(); @@ -335,6 +308,9 @@ export default { 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"); @@ -353,35 +329,55 @@ export default { callback(); }, 250); }, + // For Analyse mode: + emitFenIfAnalyze: function() { + if (this.game.mode == "analyze") { + this.$emit( + "fenchange", + !!this.lastMove ? this.lastMove.fen : this.game.fenStart + ); + } + }, // "light": if gotoMove() or gotoEnd() - // data: some custom data (addTime) to be re-emitted - play: function(move, received, light, data) { + 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.inPlay = true; + } const navigate = !move; const playSubmove = (smove) => { - if (!navigate) smove.notation = this.vr.getNotation(smove); + 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.inMultimove) { + // Condition is "!navigate" but we mean "!this.autoplay" + if (!navigate) { if (this.cursor < this.moves.length - 1) - this.moves = this.moves.slice(0, Math.max(this.cursor, 0)); + this.moves = this.moves.slice(0, this.cursor + 1); 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])); } + 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.$set(this.moves, L-1, this.moves.concat([smove])); } }; const playMove = () => { - const animate = V.ShowMoves == "all" && received; + const animate = ( + V.ShowMoves == "all" && + (this.autoplay || !!received) + ); if (!Array.isArray(move)) move = [move]; let moveIdx = 0; let self = this; @@ -402,24 +398,45 @@ export default { } })(); }; + const computeScore = () => { + const score = this.vr.getCurrentScore(); + 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 (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; + }; 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 + 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; - 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 + " . " + this.st.tr[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); + 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); + } } } }; @@ -428,19 +445,25 @@ export default { // 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 (!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; } - 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 ( this.game.mode != "analyze" && + !navigate && !received && (this.game.score != "*" || this.cursor < this.moves.length - 1) ) { @@ -452,7 +475,6 @@ export default { playMove(); }, cancelCurrentMultimove: function() { - // Cancel current multi-move const L = this.moves.length; let move = this.moves[L-1]; if (!Array.isArray(move)) move = [move]; @@ -468,26 +490,31 @@ 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.inMultimove) { this.cancelCurrentMultimove(); this.incheck = this.vr.getCheckSquares(this.vr.turn); } else { if (!move) { - if (this.cursor < 0) return; //no more moves + 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]; } - // 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); + this.emitFenIfAnalyze(); } } }, 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) { @@ -502,24 +529,28 @@ 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.emitFenIfAnalyze(); }, gotoBegin: function() { + if (this.$refs["board"].choices.length > 0) return; 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.lastMove = null; - } - this.incheck = []; + 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.$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); } } @@ -530,9 +561,6 @@ export default { [type="checkbox"]#modalEog+div .card min-height: 45px -[type="checkbox"]#modalAdjust+div .card - padding: 5px - #baseGame width: 100% &:focus @@ -546,29 +574,26 @@ export default { display: inline-block #controls - margin: 0 auto - text-align: center + user-select: none button - display: inline-block - width: 20% + border: none margin: 0 + padding-top: 5px + padding-bottom: 5px + +.in-autoplay + background-color: #FACF8C + +img.inline + height: 22px + padding-top: 5px + @media screen and (max-width: 767px) + height: 18px #turnIndicator text-align: center font-weight: bold -#belowControls - border-top: 1px solid #2f4f4f - text-align: center - margin: 0 auto - & > #downloadDiv - margin: 0 - & > button - margin: 0 - & > button - border-left: 1px solid #2f4f4f - margin: 0 - #boardContainer float: left // TODO: later, maybe, allow movesList of variable width