Fix clocks update + double move effect
[vchess.git] / client / src / views / Game.vue
... / ...
CommitLineData
1<!-- TODO: component Game, + handle players + observers connect/disconnect
2 event = "gameconnect" ...etc
3 connect/disconnect with sid+name (ID not required); name slightly redundant but easier
4quand on arrive dans la partie, on poll les sids pour savoir qui est en ligne (ping)
5(éventuel échange lastate avec les connectés, pong ...etc)
6ensuite quand qqun se deco il suffit d'écouter "disconnect"
7pareil quand quelqu'un reco.
8(c'est assez rudimentaire et écoute trop de messages, mais dans un premier temps...)
9 // TODO: [in game] send move + elapsed time (in milliseconds); in case of "lastate" message too
10// TODO: if I'm an observer and player(s) disconnect/reconnect, how to find me ?
11// onClick :: ask full game to remote player, and register as an observer in game
12// (use gameId to communicate)
13// on landing on game :: if gameId not found locally, check remotely
14// ==> il manque un param dans game : "remoteId"
15-->
16<template lang="pug">
17.row
18 .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
19 BaseGame(:game="game" :vr="vr" ref="basegame"
20 @newmove="processMove" @gameover="gameOver")
21 .button-group(v-if="game.mode!='analyze'")
22 button(@click="offerDraw") Draw
23 button(@click="abortGame") Abort
24 button(@click="resign") Resign
25 div(v-if="game.mode=='corr'")
26 textarea(v-show="score=='*' && vr.turn==game.mycolor" v-model="corrMsg")
27 div(v-show="cursor>=0") {{ moves[cursor].message }}
28</template>
29
30<script>
31import BaseGame from "@/components/BaseGame.vue";
32//import Chat from "@/components/Chat.vue";
33//import MoveList from "@/components/MoveList.vue";
34import { store } from "@/store";
35import { GameStorage } from "@/utils/storage";
36
37export default {
38 name: 'my-game',
39 components: {
40 BaseGame,
41 },
42 // gameRef: to find the game in (potentially remote) storage
43 data: function() {
44 return {
45 st: store.state,
46 gameRef: { //given in URL (rid = remote ID)
47 id: "",
48 rid: ""
49 },
50 game: { }, //passed to BaseGame
51 vr: null, //"variant rules" object initialized from FEN
52 drawOfferSent: false, //did I just ask for draw? (TODO: draw variables?)
53 people: [ ], //potential observers (TODO)
54 };
55 },
56 watch: {
57 '$route' (to, from) {
58 if (!!to.params["id"])
59 {
60 this.gameRef.id = to.params["id"];
61 this.gameRef.rid = to.query["rid"];
62 this.loadGame();
63 }
64 },
65 },
66 created: function() {
67 if (!!this.$route.params["id"])
68 {
69 this.gameRef.id = this.$route.params["id"];
70 this.gameRef.rid = this.$route.query["rid"];
71 this.loadGame();
72 }
73 // TODO: how to know who is observing ? Send message to everyone with game ID ?
74 // and then just listen to (dis)connect events
75
76
77 // server always send "connect on " + URL ; then add to observers if game...
78 // detect multiple tabs connected (when connect ask server if my SID is already in use)
79// router when access a game page tell to server I joined + game ID (no need rid)
80// and ask server for current joined (= observers)
81// when send to chat (or a move), reach only this group (send gid along)
82
83 // --> doivent être enregistrés comme observers au niveau du serveur...
84 // non: poll users + events startObserving / stopObserving
85 // (à faire au niveau du routeur ?)
86
87
88 // TODO: also handle "draw accepted" (use opponents array?)
89 // --> must give this info also when sending lastState...
90 // and, if all players agree then OK draw (end game ...etc)
91 const socketMessageListener = msg => {
92 const data = JSON.parse(msg.data);
93 let L = undefined;
94 switch (data.code)
95 {
96 case "newmove":
97 // TODO: observer on dark games must see all board ? Or alternate ? (seems better)
98 // ...or just see nothing as on buho21
99 // NOTE: next call will trigger processMove()
100 this.$refs["basegame"].play(data.move,
101 "receive", this.game.vname!="Dark" ? "animate" : null);
102 break;
103 case "pong": //received if we sent a ping (game still alive on our side)
104 if (this.gameRef.id != data.gameId)
105 break; //games IDs don't match: the game is definitely over...
106 this.oppConnected = true;
107 // Send our "last state" informations to opponent(s)
108 L = this.vr.moves.length;
109 Object.keys(this.opponents).forEach(oid => {
110 this.st.conn.send(JSON.stringify({
111 code: "lastate",
112 oppid: oid,
113 gameId: this.gameRef.id,
114 lastMove: (L>0?this.vr.moves[L-1]:undefined),
115 movesCount: L,
116 }));
117 });
118 break;
119 // TODO: refactor this, because at 3 or 4 players we may have missed 2 or 3 moves
120 case "lastate": //got opponent infos about last move
121 L = this.vr.moves.length;
122 if (this.gameRef.id != data.gameId)
123 break; //games IDs don't match: nothing we can do...
124 // OK, opponent still in game (which might be over)
125 if (this.score != "*")
126 {
127 // We finished the game (any result possible)
128 this.st.conn.send(JSON.stringify({
129 code: "lastate",
130 oppid: data.oppid,
131 gameId: this.gameRef.id,
132 score: this.score,
133 }));
134 }
135 else if (!!data.score) //opponent finished the game
136 this.endGame(data.score);
137 else if (data.movesCount < L)
138 {
139 // We must tell last move to opponent
140 this.st.conn.send(JSON.stringify({
141 code: "lastate",
142 oppid: this.opponent.id,
143 gameId: this.gameRef.id,
144 lastMove: this.vr.moves[L-1],
145 movesCount: L,
146 }));
147 }
148 else if (data.movesCount > L) //just got last move from him
149 this.play(data.lastMove, "animate"); //TODO: wrong call (3 args)
150 break;
151 case "resign": //..you won!
152 this.endGame(this.game.mycolor=="w"?"1-0":"0-1");
153 break;
154 // TODO: also use (dis)connect info to count online players?
155 case "gameconnect":
156 case "gamedisconnect":
157 if (this.mode=="human")
158 {
159 const online = (data.code == "connect");
160 // If this is an opponent ?
161 if (!!this.opponents[data.id])
162 this.opponents[data.id].online = online;
163 else
164 {
165 // Or an observer ?
166 if (!online)
167 delete this.people[data.id];
168 else
169 this.people[data.id] = data.name;
170 }
171 }
172 break;
173 }
174 };
175 const socketCloseListener = () => {
176 this.st.conn.addEventListener('message', socketMessageListener);
177 this.st.conn.addEventListener('close', socketCloseListener);
178 };
179 this.st.conn.onmessage = socketMessageListener;
180 this.st.conn.onclose = socketCloseListener;
181 },
182 // dans variant.js (plutôt room.js) conn gère aussi les challenges
183 // et les chats dans chat.js. Puis en webRTC, repenser tout ça.
184 methods: {
185 offerDraw: function() {
186 if (!confirm("Offer draw?"))
187 return;
188 // Stay in "draw offer sent" state until next move is played
189 this.drawOfferSent = true;
190 if (this.subMode == "corr")
191 {
192 // TODO: set drawOffer on in game (how ?)
193 }
194 else //live game
195 {
196 this.opponents.forEach(o => {
197 if (!!o.online)
198 {
199 try {
200 this.st.conn.send(JSON.stringify({code: "draw", oppid: o.id}));
201 } catch (INVALID_STATE_ERR) {
202 return;
203 }
204 }
205 });
206 }
207 },
208 // + conn handling: "draw" message ==> agree for draw (if we have "drawOffered" at true)
209 receiveDrawOffer: function() {
210 //if (...)
211 // TODO: ignore if preventDrawOffer is set; otherwise show modal box with option "prevent future offers"
212 // if accept: send message "draw"
213 },
214 abortGame: function() {
215 if (!confirm("Abort the game?"))
216 return;
217 //+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions,
218 //send message: "gameOver" avec score "?"
219 // ==> BaseGame must listen to game.score change, and call "endgame(score)" in this case
220 },
221 resign: function(e) {
222 if (!confirm("Resign the game?"))
223 return;
224 if (this.mode == "human" && this.oppConnected(this.oppid))
225 {
226 try {
227 this.st.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
228 } catch (INVALID_STATE_ERR) {
229 return;
230 }
231 }
232 this.endGame(this.mycolor=="w"?"0-1":"1-0");
233 },
234 // 4 cases for loading a game:
235 // - from localStorage (one running game I play)
236 // - from indexedDB (one completed live game)
237 // - from server (one correspondance game I play[ed] or not)
238 // - from remote peer (one live game I don't play, finished or not)
239 loadGame: function() {
240 GameStorage.get(this.gameRef, async (game) => {
241 this.game = Object.assign({},
242 game,
243 // NOTE: assign mycolor here, since BaseGame could also bs VS computer
244 {mycolor: [undefined,"w","b"][1 + game.players.findIndex(
245 p => p.sid == this.st.user.sid)]},
246 );
247 const vModule = await import("@/variants/" + game.vname + ".js");
248 window.V = vModule.VariantRules;
249 this.vr = new V(game.fen);
250 // Post-processing: decorate each move with current FEN:
251 // (to be able to jump to any position quickly)
252 game.moves.forEach(move => {
253 // NOTE: this is doing manually what BaseGame.play() achieve...
254 // but in a lighter "fast-forward" way
255 move.color = this.vr.turn;
256 this.vr.play(move);
257 move.fen = this.vr.getFen();
258 });
259 this.vr.re_init(game.fen);
260 });
261// // Poll all players except me (if I'm playing) to know online status.
262// // --> Send ping to server (answer pong if players[s] are connected)
263// if (this.gameInfo.players.some(p => p.sid == this.st.user.sid))
264// {
265// this.game.players.forEach(p => {
266// if (p.sid != this.st.user.sid)
267// this.st.conn.send(JSON.stringify({code:"ping", oppid:p.sid}));
268// });
269// }
270 },
271 // TODO: refactor this old "oppConnected" logic
272// oppConnected: function(uid) {
273// return this.opponents.some(o => o.id == uid && o.online);
274// },
275 // Post-process a move (which was just played)
276 processMove: function(move) {
277 if (!this.game.mycolor)
278 return; //I'm just an observer
279 // Update storage (corr or live)
280 const colorIdx = ["w","b","g","r"].indexOf(move.color);
281 // https://stackoverflow.com/a/38750895
282 const allowed_fields = ["appear", "vanish", "start", "end"];
283 const filtered_move = Object.keys(move)
284 .filter(key => allowed_fields.includes(key))
285 .reduce((obj, key) => {
286 obj[key] = move[key];
287 return obj;
288 }, {});
289 // Send move ("newmove" event) to opponent(s) (if ours)
290 // (otherwise move.elapsed is supposed to be already transmitted)
291 let addTime = undefined;
292 if (move.color == this.game.mycolor)
293 {
294 const elapsed = Date.now() - GameStorage.getInitime();
295 this.game.players.forEach(p => {
296 if (p.sid != this.st.user.sid)
297 this.st.conn.send(JSON.stringify({
298 code: "newmove",
299 target: p.sid,
300 move: Object.assign({}, filtered_move, {elapsed: elapsed}),
301 }));
302 });
303 move.elapsed = elapsed;
304 // elapsed time is measured in milliseconds
305 addTime = this.game.increment - elapsed/1000;
306 }
307 GameStorage.update({
308 colorIdx: colorIdx,
309 move: filtered_move,
310 fen: move.fen,
311 addTime: addTime,
312 initime: (this.vr.turn == this.game.mycolor), //my turn now?
313 });
314 },
315 // NOTE: this update function should also work for corr games
316 gameOver: function(score) {
317 GameStorage.update({
318 score: score,
319 });
320 },
321 },
322};
323</script>
324
325<!-- TODO:
326// Abort possible à tout moment avec message
327// Sorry I have to go / Game seems over / Game is not interesting
328// code "T" pour score "perte au temps" ?
329-->