Fix moves sending. TODO: fix double opp move + clocks update
[vchess.git] / client / src / views / Game.vue
CommitLineData
a6bddfc6
BA
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
4ce15fd9
BA
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...)
7b626bdd 9 // TODO: [in game] send move + elapsed time (in milliseconds); in case of "lastate" message too
ba82879c
BA
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"
a6bddfc6 15-->
a6088c90
BA
16<template lang="pug">
17.row
18 .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
b4fb1612
BA
19 BaseGame(:game="game" :vr="vr" ref="basegame"
20 @newmove="processMove" @gameover="gameOver")
6dd02928 21 .button-group(v-if="game.mode!='analyze'")
a6088c90
BA
22 button(@click="offerDraw") Draw
23 button(@click="abortGame") Abort
24 button(@click="resign") Resign
6dd02928 25 div(v-if="game.mode=='corr'")
4b0384fa 26 textarea(v-show="score=='*' && vr.turn==game.mycolor" v-model="corrMsg")
a6088c90
BA
27 div(v-show="cursor>=0") {{ moves[cursor].message }}
28</template>
29
30<script>
46284a2f 31import BaseGame from "@/components/BaseGame.vue";
a6088c90
BA
32//import Chat from "@/components/Chat.vue";
33//import MoveList from "@/components/MoveList.vue";
34import { store } from "@/store";
d2634386 35import { GameStorage } from "@/utils/storage";
a6088c90
BA
36
37export default {
38 name: 'my-game',
39 components: {
40 BaseGame,
41 },
f7121527 42 // gameRef: to find the game in (potentially remote) storage
a6088c90
BA
43 data: function() {
44 return {
45 st: store.state,
4b0384fa
BA
46 gameRef: { //given in URL (rid = remote ID)
47 id: "",
48 rid: ""
49 },
50 game: { }, //passed to BaseGame
6dd02928 51 vr: null, //"variant rules" object initialized from FEN
d2634386 52 drawOfferSent: false, //did I just ask for draw? (TODO: draw variables?)
4b0384fa 53 people: [ ], //potential observers (TODO)
a6088c90
BA
54 };
55 },
56 watch: {
f7121527 57 '$route' (to, from) {
4fe5664d
BA
58 if (!!to.params["id"])
59 {
60 this.gameRef.id = to.params["id"];
61 this.gameRef.rid = to.query["rid"];
62 this.loadGame();
63 }
a6088c90
BA
64 },
65 },
a6088c90 66 created: function() {
f7121527 67 if (!!this.$route.params["id"])
a6088c90 68 {
f7121527
BA
69 this.gameRef.id = this.$route.params["id"];
70 this.gameRef.rid = this.$route.query["rid"];
b196f8ea 71 this.loadGame();
f7121527 72 }
f7121527
BA
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
3b450453
BA
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
b4fb1612 85 // (à faire au niveau du routeur ?)
3b450453
BA
86
87
a6088c90
BA
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 {
f7121527
BA
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
4fe5664d 99 this.$refs["basegame"].play(
6ec161b9 100 data.move, this.game.vname!="Dark" ? "animate" : null); //TODO: arg to know if opponent move (or comp) in case of dark variant...
9d54ab89 101 this.processMove(data.move);
a6088c90
BA
102 break;
103 case "pong": //received if we sent a ping (game still alive on our side)
104 if (this.gameRef.id != data.gameId)
f7121527 105 break; //games IDs don't match: the game is definitely over...
a6088c90
BA
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 => {
4b0384fa 110 this.st.conn.send(JSON.stringify({
a6088c90
BA
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;
f7121527 119 // TODO: refactor this, because at 3 or 4 players we may have missed 2 or 3 moves
a6088c90
BA
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)
4b0384fa 128 this.st.conn.send(JSON.stringify({
a6088c90
BA
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
4b0384fa 140 this.st.conn.send(JSON.stringify({
a6088c90
BA
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");
150 break;
151 case "resign": //..you won!
4b0384fa 152 this.endGame(this.game.mycolor=="w"?"1-0":"0-1");
a6088c90
BA
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 = () => {
4b0384fa
BA
176 this.st.conn.addEventListener('message', socketMessageListener);
177 this.st.conn.addEventListener('close', socketCloseListener);
a6088c90 178 };
4b0384fa
BA
179 this.st.conn.onmessage = socketMessageListener;
180 this.st.conn.onclose = socketCloseListener;
a6088c90
BA
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 {
4b0384fa 200 this.st.conn.send(JSON.stringify({code: "draw", oppid: o.id}));
a6088c90
BA
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 "?"
4b0384fa 219 // ==> BaseGame must listen to game.score change, and call "endgame(score)" in this case
a6088c90
BA
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 {
4b0384fa 227 this.st.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
a6088c90
BA
228 } catch (INVALID_STATE_ERR) {
229 return;
230 }
231 }
232 this.endGame(this.mycolor=="w"?"0-1":"1-0");
233 },
b196f8ea
BA
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)
6dd02928
BA
239 loadGame: function() {
240 GameStorage.get(this.gameRef, async (game) => {
4b0384fa
BA
241 this.game = Object.assign({},
242 game,
243 // NOTE: assign mycolor here, since BaseGame could also bs VS computer
b4fb1612
BA
244 {mycolor: [undefined,"w","b"][1 + game.players.findIndex(
245 p => p.sid == this.st.user.sid)]},
4b0384fa 246 );
6dd02928 247 const vModule = await import("@/variants/" + game.vname + ".js");
d6c1bf37 248 window.V = vModule.VariantRules;
6dd02928 249 this.vr = new V(game.fen);
6274a545
BA
250 // Post-processing: decorate each move with current FEN:
251 // (to be able to jump to any position quickly)
252 game.moves.forEach(move => {
8a7452b5 253 // TODO: this is doing manually what BaseGame.play() achieve...
9d54ab89 254 move.color = this.vr.turn;
8a7452b5 255 this.vr.play(move);
9d54ab89 256 move.fen = this.vr.getFen();
6274a545
BA
257 });
258 this.vr.re_init(game.fen);
d6c1bf37 259 });
4fe5664d
BA
260// // Poll all players except me (if I'm playing) to know online status.
261// // --> Send ping to server (answer pong if players[s] are connected)
262// if (this.gameInfo.players.some(p => p.sid == this.st.user.sid))
263// {
264// this.game.players.forEach(p => {
265// if (p.sid != this.st.user.sid)
266// this.st.conn.send(JSON.stringify({code:"ping", oppid:p.sid}));
267// });
268// }
a6088c90 269 },
b4fb1612 270 // TODO: refactor this old "oppConnected" logic
6ec161b9
BA
271// oppConnected: function(uid) {
272// return this.opponents.some(o => o.id == uid && o.online);
273// },
9d54ab89 274 // Post-process a move (which was just played)
ce87ac6a 275 processMove: function(move) {
b4fb1612
BA
276 if (!this.game.mycolor)
277 return; //I'm just an observer
9d54ab89
BA
278 // Update storage (corr or live)
279 const colorIdx = ["w","b","g","r"][move.color];
280 // https://stackoverflow.com/a/38750895
281 const allowed_fields = ["appear", "vanish", "start", "end"];
282 const filtered_move = Object.keys(move)
283 .filter(key => allowed_fields.includes(key))
284 .reduce((obj, key) => {
8a7452b5 285 obj[key] = move[key];
9d54ab89
BA
286 return obj;
287 }, {});
b4fb1612 288 // Send move ("newmove" event) to opponent(s) (if ours)
9d54ab89
BA
289 // (otherwise move.elapsed is supposed to be already transmitted)
290 if (move.color == this.game.mycolor)
b4fb1612 291 {
9d54ab89 292 const elapsed = Date.now() - GameStorage.getInitime();
b4fb1612
BA
293 this.game.players.forEach(p => {
294 if (p.sid != this.st.user.sid)
8a7452b5
BA
295 this.st.conn.send(JSON.stringify({
296 code: "newmove",
9d54ab89
BA
297 target: p.sid,
298 move: Object.assign({}, filtered_move, {elapsed: elapsed}),
8a7452b5 299 }));
b4fb1612 300 });
9d54ab89 301 move.elapsed = elapsed;
b4fb1612 302 }
9d54ab89
BA
303 GameStorage.update({
304 colorIdx: colorIdx,
305 move: filtered_move,
306 fen: move.fen,
307 elapsed: move.elapsed,
308 increment: this.game.increment, //redundant but faster
6ec161b9 309 initime: (this.vr.turn == this.game.mycolor), //is it my turn?
9d54ab89 310 });
b4fb1612 311 },
9d54ab89 312 // NOTE: this update function should also work for corr games
b4fb1612 313 gameOver: function(score) {
9d54ab89
BA
314 GameStorage.update({
315 score: score,
316 });
ce87ac6a 317 },
a6088c90
BA
318 },
319};
320</script>
6274a545 321
9d54ab89
BA
322<!-- TODO:
323// Abort possible à tout moment avec message
6274a545 324// Sorry I have to go / Game seems over / Game is not interesting
9d54ab89
BA
325// code "T" pour score "perte au temps" ?
326-->