From: Benjamin Auder <benjamin.auder@somewhere> Date: Thu, 23 Jan 2020 09:31:07 +0000 (+0100) Subject: Add basic analyze view from FEN X-Git-Url: https://git.auder.net/doc/html/css/img/scripts/pieces/config.php?a=commitdiff_plain;h=652f40de91b2694093ba9755f24b76b81caff232;p=vchess.git Add basic analyze view from FEN --- diff --git a/client/src/router.js b/client/src/router.js index a4d32369..1eb88f91 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -68,7 +68,7 @@ const router = new Router({ { path: "/analyze/:vname([a-zA-Z0-9]+)", name: "analyze", - component: loadView("Game"), + component: loadView("Analyze"), }, { path: "/about", diff --git a/client/src/views/Analyze.vue b/client/src/views/Analyze.vue index 4ec370c5..cecb7737 100644 --- a/client/src/views/Analyze.vue +++ b/client/src/views/Analyze.vue @@ -1,522 +1,54 @@ <template lang="pug"> .row .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 - input#modalAbort.modal(type="checkbox") - div(role="dialog" aria-labelledby="abortBoxTitle") - .card.smallpad.small-modal.text-center - label.modal-close(for="modalAbort") - h3#abortBoxTitle.section {{ st.tr["Terminate game?"] }} - button(@click="abortGame") {{ st.tr["Sorry I have to go"] }} - button(@click="abortGame") {{ st.tr["Game seems over"] }} - button(@click="abortGame") {{ st.tr["Opponent is gone"] }} - BaseGame(:game="game" :vr="vr" ref="basegame" - @newmove="processMove" @gameover="gameOver") - div Names: {{ game.players[0].name }} - {{ game.players[1].name }} - div(v-if="game.score=='*'") Time: {{ virtualClocks[0] }} - {{ virtualClocks[1] }} - .button-group(v-if="game.mode!='analyze' && game.score=='*'") - button(@click="offerDraw") Draw - button(@click="() => abortGame()") Abort - button(@click="resign") Resign - textarea(v-if="game.score=='*'" v-model="corrMsg") - Chat(:players="game.players") + BaseGame(:game="game" :vr="vr" ref="basegame") </template> <script> import BaseGame from "@/components/BaseGame.vue"; -import Chat from "@/components/Chat.vue"; import { store } from "@/store"; -import { GameStorage } from "@/utils/gameStorage"; -import { ppt } from "@/utils/datetime"; -import { extractTime } from "@/utils/timeControl"; import { ArrayFun } from "@/utils/array"; export default { - name: 'my-game', + name: 'my-analyze', components: { BaseGame, - Chat, }, // gameRef: to find the game in (potentially remote) storage data: function() { return { st: store.state, gameRef: { //given in URL (rid = remote ID) - id: "", - rid: "" + vname: "", + fen: "" + }, + game: { + players:[{name:"Analyze"},{name:"Analyze"}], + mode: "analyze" }, - game: {players:[{name:""},{name:""}]}, //passed to BaseGame - corrMsg: "", //to send offline messages in corr games - virtualClocks: [0, 0], //initialized with true game.clocks vr: null, //"variant rules" object initialized from FEN - drawOffer: "", //TODO: use for button style - people: [], //players + observers + //people: [], //players + observers //TODO later: interactive analyze... }; }, watch: { "$route": function(to, from) { - this.gameRef.id = to.params["id"]; - this.gameRef.rid = to.query["rid"]; + this.gameRef.fen = to.query["fen"].replace(/_/g, " "); + this.gameRef.vname = to.params["vname"]; this.loadGame(); }, - "game.clocks": function(newState) { - if (this.game.moves.length < 2) - { - // 1st move not completed yet: freeze time - this.virtualClocks = newState.map(s => ppt(s)); - return; - } - const currentTurn = this.vr.turn; - const colorIdx = ["w","b"].indexOf(currentTurn); - let countdown = newState[colorIdx] - - (Date.now() - this.game.initime[colorIdx])/1000; - this.virtualClocks = [0,1].map(i => { - const removeTime = i == colorIdx - ? (Date.now() - this.game.initime[colorIdx])/1000 - : 0; - return ppt(newState[i] - removeTime); - }); - let clockUpdate = setInterval(() => { - if (countdown < 0 || this.vr.turn != currentTurn || this.game.score != "*") - { - clearInterval(clockUpdate); - if (countdown < 0) - { - this.$refs["basegame"].endGame( - this.vr.turn=="w" ? "0-1" : "1-0", "Time"); - } - } - else - { - // TODO: with Vue 3, just do this.virtualClocks[colorIdx] = ppt(--countdown) - this.$set(this.virtualClocks, colorIdx, ppt(Math.max(0, --countdown))); - } - }, 1000); - }, }, - // TODO: redundant code with Hall.vue (related to people array) created: function() { - // Always add myself to players' list - const my = this.st.user; - this.people.push({sid:my.sid, id:my.id, name:my.name}); - this.gameRef.id = this.$route.params["id"]; - this.gameRef.rid = this.$route.query["rid"]; //may be undefined - if (!this.gameRef.rid) - this.loadGame(); //local or corr: can load from now - // 0.1] Ask server for room composition: - const initialize = () => { - // Poll clients + load game if stored remotely - this.st.conn.send(JSON.stringify({code:"pollclients"})); - if (!!this.gameRef.rid) - this.loadGame(); - }; - if (!!this.st.conn && this.st.conn.readyState == 1) //1 == OPEN state - initialize(); - else //socket not ready yet (initial loading) - this.st.conn.onopen = initialize; - this.st.conn.onmessage = this.socketMessageListener; - const socketCloseListener = () => { - store.socketCloseListener(); //reinitialize connexion (in store.js) - this.st.conn.addEventListener('message', this.socketMessageListener); - this.st.conn.addEventListener('close', socketCloseListener); - }; - this.st.conn.onclose = socketCloseListener; + this.gameRef.fen = this.$route.query["fen"].replace(/_/g, " "); + this.gameRef.vname = this.$route.params["vname"]; + this.loadGame(); }, methods: { - getOppSid: function() { - if (!!this.game.oppsid) - return this.game.oppsid; - const opponent = this.people.find(p => p.id == this.game.oppid); - return (!!opponent ? opponent.sid : null); - }, - socketMessageListener: function(msg) { - const data = JSON.parse(msg.data); - switch (data.code) - { - // 0.2] Receive clients list (just socket IDs) - case "pollclients": - { - data.sockIds.forEach(sid => { - this.people.push({sid:sid, id:0, name:""}); - // Ask only identity - this.st.conn.send(JSON.stringify({code:"askidentity", target:sid})); - }); - break; - } - case "askidentity": - { - // Request for identification: reply if I'm not anonymous - if (this.st.user.id > 0) - { - this.st.conn.send(JSON.stringify( - // people[0] instead of st.user to avoid sending email - {code:"identity", user:this.people[0], target:data.from})); - } - break; - } - case "identity": - { - let player = this.people.find(p => p.sid == data.user.sid); - // NOTE: sometimes player.id fails because player is undefined... - // Probably because the event was meant for Hall? - if (!player) - return; - player.id = data.user.id; - player.name = data.user.name; - // Sending last state only for live games: corr games are complete - if (this.game.type == "live" && this.game.oppsid == player.sid) - { - // Send our "last state" informations to opponent - const L = this.game.moves.length; - this.st.conn.send(JSON.stringify({ - code: "lastate", - target: player.sid, - state: - { - lastMove: (L>0 ? this.game.moves[L-1] : undefined), - score: this.game.score, - movesCount: L, - drawOffer: this.drawOffer, - clocks: this.game.clocks, - } - })); - } - break; - } - case "askgame": - // Send current (live) game - const myGame = - { - // Minimal game informations: - id: this.game.id, - players: this.game.players.map(p => { return {name:p.name}; }), - vid: this.game.vid, - timeControl: this.game.timeControl, - }; - this.st.conn.send(JSON.stringify({code:"game", - game:myGame, target:data.from})); - break; - case "newmove": - // NOTE: this call to play() will trigger processMove() - this.$refs["basegame"].play(data.move, - "receive", this.game.vname!="Dark" ? "animate" : null); - break; - case "lastate": //got opponent infos about last move - { - const L = this.game.moves.length; - if (data.movesCount > L) - { - // Just got last move from him - this.$refs["basegame"].play(data.lastMove, - "receive", this.game.vname!="Dark" ? "animate" : null); - if (data.score != "*" && this.game.score == "*") - { - // Opponent resigned or aborted game, or accepted draw offer - // (this is not a stalemate or checkmate) - this.$refs["basegame"].endGame(data.score, "Opponent action"); - } - this.game.clocks = data.clocks; //TODO: check this? - this.drawOffer = data.drawOffer; //does opponent offer draw? - } - break; - } - case "resign": - this.$refs["basegame"].endGame( - this.game.mycolor=="w" ? "1-0" : "0-1", "Resign"); - break; - case "abort": - this.$refs["basegame"].endGame("?", "Abort: " + data.msg); - break; - case "draw": - this.$refs["basegame"].endGame("1/2", "Mutual agreement"); - break; - case "drawoffer": - this.drawOffer = "received"; - break; - case "askfullgame": - // TODO: use data.id to retrieve game in indexedDB (but for now only one running game so OK) - this.st.conn.send(JSON.stringify({code:"fullgame", game:this.game, target:data.from})); - break; - case "fullgame": - this.loadGame(data.game); - break; - // TODO: drawaccepted (click draw button before sending move - // ==> draw offer in move) - // ==> on "newmove", check "drawOffer" field - case "connect": - { - this.people.push({name:"", id:0, sid:data.from}); - this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from})); - break; - } - case "disconnect": - ArrayFun.remove(this.people, p => p.sid == data.from); - break; - } - }, - offerDraw: function() { - // TODO: also for corr games - if (this.drawOffer == "received") - { - if (!confirm("Accept draw?")) - return; - const oppsid = this.getOppSid(); - if (!!oppsid) - this.st.conn.send(JSON.stringify({code:"draw", target:oppsid})); - this.$refs["basegame"].endGame("1/2", "Mutual agreement"); - } - else if (this.drawOffer == "sent") - this.drawOffer = ""; - else - { - if (!confirm("Offer draw?")) - return; - const oppsid = this.getOppSid(); - if (!!oppsid) - this.st.conn.send(JSON.stringify({code:"drawoffer", target:oppsid})); - } - }, - // + conn handling: "draw" message ==> agree for draw (if we have "drawOffered" at true) - receiveDrawOffer: function() { - //if (...) - // TODO: ignore if preventDrawOffer is set; otherwise show modal box with option "prevent future offers" - // if accept: send message "draw" - }, - abortGame: function(event) { - let modalBox = document.getElementById("modalAbort"); - if (!event) - { - // First call show options: - modalBox.checked = true; - } - else - { - modalBox.checked = false; //decision made: box disappear - const message = event.target.innerText; - // Next line will trigger a "gameover" event, bubbling up till here - this.$refs["basegame"].endGame("?", "Abort: " + message); - const oppsid = this.getOppSid(); - if (!!oppsid) - { - this.st.conn.send(JSON.stringify({ - code: "abort", - msg: message, - target: oppsid, - })); - } - } - }, - resign: function(e) { - if (!confirm("Resign the game?")) - return; - const oppsid = this.getOppSid(); - if (!!oppsid) - { - this.st.conn.send(JSON.stringify({ - code: "resign", - target: oppsid, - })); - } - // Next line will trigger a "gameover" event, bubbling up till here - this.$refs["basegame"].endGame( - this.game.mycolor=="w" ? "0-1" : "1-0", "Resign"); - }, - // 3 cases for loading a game: - // - from indexedDB (running or completed live game I play) - // - from server (one correspondance game I play[ed] or not) - // - from remote peer (one live game I don't play, finished or not) - loadGame: function(game) { - const afterRetrieval = async (game) => { - const vModule = await import("@/variants/" + game.vname + ".js"); - window.V = vModule.VariantRules; - this.vr = new V(game.fen); - const gtype = (game.timeControl.indexOf('d') >= 0 ? "corr" : "live"); - const tc = extractTime(game.timeControl); - if (gtype == "corr") - { - if (game.players[0].color == "b") - { - // Adopt the same convention for live and corr games: [0] = white - [ game.players[0], game.players[1] ] = - [ game.players[1], game.players[0] ]; - } - // corr game: needs to compute the clocks + initime - // NOTE: clocks in seconds, initime in milliseconds - game.clocks = [tc.mainTime, tc.mainTime]; - game.initime = [0, 0]; - const L = game.moves.length; - game.moves.sort((m1,m2) => m1.idx - m2.idx); //in case of - if (L >= 3) - { - let addTime = [0, 0]; - for (let i=2; i<L; i++) - { - addTime[i%2] += tc.increment - - (game.moves[i].played - game.moves[i-1].played) / 1000; - } - for (let i=0; i<=1; i++) - game.clocks[i] += addTime[i]; - } - if (L >= 1) - game.initime[L%2] = game.moves[L-1].played; - // Now that we used idx and played, re-format moves as for live games - game.moves = game.moves.map( (m) => { - const s = m.squares; - return { - appear: s.appear, - vanish: s.vanish, - start: s.start, - end: s.end, - message: m.message, - }; - }); - } - const myIdx = game.players.findIndex(p => { - return p.sid == this.st.user.sid || p.uid == this.st.user.id; - }); - if (gtype == "live" && game.clocks[0] < 0) //game unstarted - { - game.clocks = [tc.mainTime, tc.mainTime]; - game.initime[0] = Date.now(); - if (myIdx >= 0) - { - // I play in this live game; corr games don't have clocks+initime - GameStorage.update(game.id, - { - clocks: game.clocks, - initime: game.initime, - }); - } - } - this.game = Object.assign({}, - game, - // NOTE: assign mycolor here, since BaseGame could also be VS computer - { - type: gtype, - increment: tc.increment, - mycolor: [undefined,"w","b"][myIdx+1], - // opponent sid not strictly required (or available), but easier - // at least oppsid or oppid is available anyway: - oppsid: (myIdx < 0 ? undefined : game.players[1-myIdx].sid), - oppid: (myIdx < 0 ? undefined : game.players[1-myIdx].uid), - } - ); - }; - if (!!game) - return afterRetrieval(game); - if (!!this.gameRef.rid) - { - // Remote live game - // (TODO: send game ID as well, and receiver would pick the corresponding - // game in his current games; if allowed to play several) - this.st.conn.send(JSON.stringify( - {code:"askfullgame", target:this.gameRef.rid})); - // (send moves updates + resign/abort/draw actions) - } - else - { - // Local or corr game - GameStorage.get(this.gameRef.id, afterRetrieval); - } - }, - // Post-process a move (which was just played) - processMove: function(move) { - if (!this.game.mycolor) - return; //I'm just an observer - // Update storage (corr or live) - const colorIdx = ["w","b"].indexOf(move.color); - // https://stackoverflow.com/a/38750895 - const allowed_fields = ["appear", "vanish", "start", "end"]; - const filtered_move = Object.keys(move) - .filter(key => allowed_fields.includes(key)) - .reduce((obj, key) => { - obj[key] = move[key]; - return obj; - }, {}); - // Send move ("newmove" event) to people in the room (if our turn) - let addTime = 0; - if (move.color == this.game.mycolor) - { - if (this.game.moves.length >= 2) //after first move - { - const elapsed = Date.now() - this.game.initime[colorIdx]; - // elapsed time is measured in milliseconds - addTime = this.game.increment - elapsed/1000; - } - let sendMove = Object.assign({}, filtered_move, {addTime: addTime}); - if (this.game.type == "corr") - sendMove.message = this.corrMsg; - const oppsid = this.getOppSid(); - this.people.forEach(p => { - if (p.sid != this.st.user.sid) - { - this.st.conn.send(JSON.stringify({ - code: "newmove", - target: p.sid, - move: sendMove, - })); - } - }); - if (this.game.type == "corr" && this.corrMsg != "") - { - // Add message to last move in BaseGame: - // TODO: not very good style... - this.$refs["basegame"].setCurrentMessage(this.corrMsg); - } - } - else - addTime = move.addTime; //supposed transmitted - const nextIdx = ["w","b"].indexOf(this.vr.turn); - // Since corr games are stored at only one location, update should be - // done only by one player for each move: - if (this.game.type == "live" || move.color == this.game.mycolor) - { - if (this.game.type == "corr") - { - GameStorage.update(this.gameRef.id, - { - fen: move.fen, - move: - { - squares: filtered_move, - message: this.corrMsg, - played: Date.now(), //TODO: on server? - idx: this.game.moves.length, - }, - }); - } - else //live - { - GameStorage.update(this.gameRef.id, - { - fen: move.fen, - move: filtered_move, - clocks: this.game.clocks.map((t,i) => i==colorIdx - ? this.game.clocks[i] + addTime - : this.game.clocks[i]), - initime: this.game.initime.map((t,i) => i==nextIdx - ? Date.now() - : this.game.initime[i]), - }); - } - } - // Also update current game object: - this.game.moves.push(move); - this.game.fen = move.fen; - //TODO: just this.game.clocks[colorIdx] += addTime; - this.$set(this.game.clocks, colorIdx, this.game.clocks[colorIdx] + addTime); - this.game.initime[nextIdx] = Date.now(); - // Finally reset curMoveMessage if needed - if (this.game.type == "corr" && move.color == this.game.mycolor) - this.corrMsg = ""; - }, - gameOver: function(score) { - this.game.mode = "analyze"; - this.game.score = score; - const myIdx = this.game.players.findIndex(p => { - return p.sid == this.st.user.sid || p.uid == this.st.user.id; - }); - if (myIdx >= 0) //OK, I play in this game - GameStorage.update(this.gameRef.id, { score: score }); + loadGame: async function() { + this.game.vname = this.gameRef.vname; + this.game.fen = this.gameRef.fen; + const vModule = await import("@/variants/" + this.game.vname + ".js"); + window.V = vModule.VariantRules; + this.vr = new V(this.game.fen); }, }, };