Update some TODOs
[vchess.git] / public / javascripts / components / game.js
CommitLineData
fd373b27 1// TODO: envoyer juste "light move", sans FEN ni notation ...etc
2305d34a 2// TODO: also "observers" prop, we should send moves to them too (in a web worker ? webRTC ?)
fd373b27
BA
3
4// Game logic on a variant page: 3 modes, analyze, computer or human
1d184b4c 5Vue.component('my-game', {
fd373b27
BA
6 // gameId: to find the game in storage (assumption: it exists)
7 props: ["gameId","mode","allowChat","allowMovelist"],
1d184b4c
BA
8 data: function() {
9 return {
81da2786 10 // if oppid == "computer" then mode = "computer" (otherwise human)
3a609580 11 myid: "", //our ID, always set
fd373b27 12 //this.myid = localStorage.getItem("myid")
1d184b4c 13 oppid: "", //opponent ID in case of HH game
81da2786
BA
14 score: "*", //'*' means 'unfinished'
15 mycolor: "w",
fd373b27
BA
16 conn: null, //socket connection (et WebRTC connection ?!)
17 oppConnected: false, //TODO?
3840e240 18 pgnTxt: "",
a897b421 19 // sound level: 0 = no sound, 1 = sound only on newgame, 2 = always
e6dcb115 20 sound: parseInt(localStorage["sound"] || "2"),
643479f8
BA
21 // Web worker to play computer moves without freezing interface:
22 compWorker: new Worker('/javascripts/playCompMove.js'),
23 timeStart: undefined, //time when computer starts thinking
fd373b27 24 vr: null, //VariantRules object, describing the game state + rules
1d184b4c
BA
25 };
26 },
81da2786 27 computed: {
81da2786 28 showChat: function() {
fd373b27 29 return this.allowChat && this.mode=='human' && this.score != '*';
81da2786
BA
30 },
31 showMoves: function() {
fd373b27
BA
32 return this.allowMovelist && window.innerWidth >= 768;
33 },
34 showFen: function() {
35 return variant.name != "Dark" || this.score != "*";
c794dbb8
BA
36 },
37 },
81da2786 38 // Modal end of game, and then sub-components
fd373b27
BA
39 // TODO: provide chat parameters (connection, players ID...)
40 // and alwo moveList parameters (just moves ?)
41 // TODO: connection + turn indicators en haut à droite (superposé au menu)
42 // TODO: controls: abort, clear, resign, draw (avec confirm box)
43 // et si partie terminée : (mode analyse) just clear, back / play
44 // + flip button toujours disponible
45 // gotoMove : vr = new VariantRules(fen stocké dans le coup [TODO])
81da2786
BA
46 template: `
47 <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
48 <input id="modal-eog" type="checkbox" class="modal"/>
49 <div role="dialog" aria-labelledby="eogMessage">
50 <div class="card smallpad small-modal text-center">
fd373b27
BA
51 <label for="modal-eog" class="modal-close">
52 </label>
53 <h3 id="eogMessage" class="section">
54 {{ endgameMessage }}
55 </h3>
936dc463
BA
56 </div>
57 </div>
fd373b27
BA
58 <my-chat v-if="showChat">
59 </my-chat>
60 <my-board v-bind:vr="vr">
61 </my-board>
62 <div v-show="showFen" id="fen-div" class="section-content">
63 <p id="fen-string" class="text-center">
64 {{ vr.getFen() }}
65 </p>
66 </div>
81da2786 67 <div id="pgn-div" class="section-content">
fd373b27
BA
68 <a id="download" href: "#">
69 </a>
81da2786
BA
70 <button id="downloadBtn" @click="download">
71 {{ translations["Download PGN"] }}
72 </button>
fd373b27
BA
73 </div>
74 <my-move-list v-if="showMoves">
75 </my-move-list>
81da2786
BA
76 </div>
77 `,
1d184b4c
BA
78 created: function() {
79 const url = socketUrl;
8d7e2786 80 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant._id);
fd373b27 81 // TODO: after game, archive in indexedDB
067c675b 82 // TODO: this events listener is central. Refactor ? How ?
d35f20e4 83 const socketMessageListener = msg => {
1d184b4c 84 const data = JSON.parse(msg.data);
edcd679a 85 let L = undefined;
1d184b4c
BA
86 switch (data.code)
87 {
1d184b4c 88 case "newmove": //..he played!
8d7e2786 89 this.play(data.move, (variant.name!="Dark" ? "animate" : null));
1d184b4c 90 break;
f3802fcd 91 case "pong": //received if we sent a ping (game still alive on our side)
56a683cd
BA
92 if (this.gameId != data.gameId)
93 break; //games IDs don't match: definitely over...
1d184b4c 94 this.oppConnected = true;
f3802fcd 95 // Send our "last state" informations to opponent
edcd679a 96 L = this.vr.moves.length;
a29d9d6b 97 this.conn.send(JSON.stringify({
56a683cd
BA
98 code: "lastate",
99 oppid: this.oppid,
100 gameId: this.gameId,
101 lastMove: (L>0?this.vr.moves[L-1]:undefined),
102 movesCount: L,
a29d9d6b 103 }));
1d184b4c 104 break;
56a683cd 105 case "lastate": //got opponent infos about last move
edcd679a 106 L = this.vr.moves.length;
56a683cd
BA
107 if (this.gameId != data.gameId)
108 break; //games IDs don't match: nothing we can do...
109 // OK, opponent still in game (which might be over)
edcd679a 110 if (this.score != "*")
a29d9d6b 111 {
56a683cd 112 // We finished the game (any result possible)
a29d9d6b 113 this.conn.send(JSON.stringify({
56a683cd
BA
114 code: "lastate",
115 oppid: data.oppid,
116 gameId: this.gameId,
117 score: this.score,
a29d9d6b
BA
118 }));
119 }
56a683cd
BA
120 else if (!!data.score) //opponent finished the game
121 this.endGame(data.score);
122 else if (data.movesCount < L)
a29d9d6b
BA
123 {
124 // We must tell last move to opponent
a29d9d6b 125 this.conn.send(JSON.stringify({
56a683cd
BA
126 code: "lastate",
127 oppid: this.oppid,
edcd679a 128 gameId: this.gameId,
56a683cd
BA
129 lastMove: this.vr.moves[L-1],
130 movesCount: L,
a29d9d6b
BA
131 }));
132 }
56a683cd 133 else if (data.movesCount > L) //just got last move from him
a29d9d6b 134 this.play(data.lastMove, "animate");
ecf44502 135 break;
1d184b4c 136 case "resign": //..you won!
dfb4afc1 137 this.endGame(this.mycolor=="w"?"1-0":"0-1");
1d184b4c 138 break;
f3802fcd 139 // TODO: also use (dis)connect info to count online players?
1d184b4c
BA
140 case "connect":
141 case "disconnect":
4f7723a1 142 if (this.mode=="human" && this.oppid == data.id)
1d184b4c 143 this.oppConnected = (data.code == "connect");
4f7723a1 144 if (this.oppConnected && this.score != "*")
3a609580
BA
145 {
146 // Send our name to the opponent, in case of he hasn't it
147 this.conn.send(JSON.stringify({
148 code:"myname", name:this.myname, oppid: this.oppid}));
149 }
1d184b4c
BA
150 break;
151 }
152 };
067c675b 153
d35f20e4 154 const socketCloseListener = () => {
8d7e2786 155 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant._id);
d35f20e4
BA
156 this.conn.addEventListener('message', socketMessageListener);
157 this.conn.addEventListener('close', socketCloseListener);
158 };
d35f20e4
BA
159 this.conn.onmessage = socketMessageListener;
160 this.conn.onclose = socketCloseListener;
81da2786 161
067c675b 162 // Computer moves web worker logic: (TODO: also for observers in HH games)
8d7e2786 163 this.compWorker.postMessage(["scripts",variant.name]);
643479f8
BA
164 const self = this;
165 this.compWorker.onmessage = function(e) {
aa78cc74 166 let compMove = e.data;
78bab51e
BA
167 if (!compMove)
168 return; //may happen if MarseilleRules and subTurn==2 (TODO: a bit ugly...)
6e62b1c7
BA
169 if (!Array.isArray(compMove))
170 compMove = [compMove]; //to deal with MarseilleRules
171 // TODO: imperfect attempt to avoid ghost move:
172 compMove.forEach(m => { m.computer = true; });
643479f8
BA
173 // (first move) HACK: small delay to avoid selecting elements
174 // before they appear on page:
175 const delay = Math.max(500-(Date.now()-self.timeStart), 0);
176 setTimeout(() => {
8d7e2786 177 const animate = (variant.name!="Dark" ? "animate" : null);
aa78cc74 178 if (self.mode == "computer") //warning: mode could have changed!
5915f720 179 self.play(compMove[0], animate);
6e62b1c7
BA
180 if (compMove.length == 2)
181 setTimeout( () => {
182 if (self.mode == "computer")
5915f720 183 self.play(compMove[1], animate);
78bab51e 184 }, 750);
643479f8
BA
185 }, delay);
186 }
1d184b4c 187 },
fd373b27
BA
188 //TODO: conn pourrait être une prop, donnée depuis variant.js
189 //dans variant.js (plutôt room.js) conn gère aussi les challenges
190 // Puis en webRTC, repenser tout ça.
81da2786 191 methods: {
fd373b27
BA
192 setEndgameMessage: function(score) {
193 let eogMessage = "Undefined";
194 switch (score)
195 {
196 case "1-0":
197 eogMessage = translations["White win"];
198 break;
199 case "0-1":
200 eogMessage = translations["Black win"];
201 break;
202 case "1/2":
203 eogMessage = translations["Draw"];
204 break;
205 case "?":
206 eogMessage = "Unfinished";
207 break;
208 }
209 this.endgameMessage = eogMessage;
210 },
01ca2adc 211 download: function() {
edcd679a 212 // Variants may have special PGN structure (so next function isn't defined here)
fd373b27
BA
213 // TODO: get fenStart from local game (using gameid)
214 const content = V.GetPGN(this.moves, this.mycolor, this.score, fenStart, this.mode);
01ca2adc
BA
215 // Prepare and trigger download link
216 let downloadAnchor = document.getElementById("download");
217 downloadAnchor.setAttribute("download", "game.pgn");
edcd679a 218 downloadAnchor.href = "data:text/plain;charset=utf-8," + encodeURIComponent(content);
01ca2adc
BA
219 downloadAnchor.click();
220 },
fd373b27
BA
221 showScoreMsg: function(score) {
222 this.setEndgameMessage(score);
ecf44502 223 let modalBox = document.getElementById("modal-eog");
186516b8 224 modalBox.checked = true;
1a788978
BA
225 setTimeout(() => { modalBox.checked = false; }, 2000);
226 },
227 endGame: function(score) {
228 this.score = score;
56a683cd
BA
229 if (["human","computer"].includes(this.mode))
230 {
231 const prefix = (this.mode=="computer" ? "comp-" : "");
232 localStorage.setItem(prefix+"score", score);
233 }
fd373b27 234 this.showScoreMsg(score);
3a609580
BA
235 if (this.mode == "human" && this.oppConnected)
236 {
237 // Send our nickname to opponent
238 this.conn.send(JSON.stringify({
239 code:"myname", name:this.myname, oppid:this.oppid}));
240 }
fd373b27
BA
241 // TODO: what about cursor ?
242 //this.cursor = this.vr.moves.length; //to navigate in finished game
1d184b4c 243 },
067c675b
BA
244 resign: function(e) {
245 this.getRidOfTooltip(e.currentTarget);
246 if (this.mode == "human" && this.oppConnected)
247 {
248 try {
249 this.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
250 } catch (INVALID_STATE_ERR) {
251 return; //socket is not ready (and not yet reconnected)
252 }
253 }
254 this.endGame(this.mycolor=="w"?"0-1":"1-0");
255 },
1d184b4c 256 playComputerMove: function() {
643479f8
BA
257 this.timeStart = Date.now();
258 this.compWorker.postMessage(["askmove"]);
1d184b4c 259 },
fd373b27
BA
260 animateMove: function(move) {
261 let startSquare = document.getElementById(this.getSquareId(move.start));
262 let endSquare = document.getElementById(this.getSquareId(move.end));
263 let rectStart = startSquare.getBoundingClientRect();
264 let rectEnd = endSquare.getBoundingClientRect();
265 let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
266 let movingPiece =
267 document.querySelector("#" + this.getSquareId(move.start) + " > img.piece");
268 // HACK for animation (with positive translate, image slides "under background")
269 // Possible improvement: just alter squares on the piece's way...
270 squares = document.getElementsByClassName("board");
271 for (let i=0; i<squares.length; i++)
272 {
273 let square = squares.item(i);
274 if (square.id != this.getSquareId(move.start))
275 square.style.zIndex = "-1";
276 }
277 movingPiece.style.transform = "translate(" + translation.x + "px," +
278 translation.y + "px)";
279 movingPiece.style.transitionDuration = "0.2s";
280 movingPiece.style.zIndex = "3000";
281 setTimeout( () => {
282 for (let i=0; i<squares.length; i++)
283 squares.item(i).style.zIndex = "auto";
284 movingPiece.style = {}; //required e.g. for 0-0 with KR swap
285 this.play(move); //TODO: plutôt envoyer message "please play"
286 }, 250);
287 },
288 play: function(move, programmatic) {
289 if (!!programmatic) //computer or human opponent
290 return this.animateMove(move);
291 // Not programmatic, or animation is over
292 if (!move.notation)
293 move.notation = this.vr.getNotation(move);
294 this.vr.play(move);
295 if (!move.fen)
296 move.fen = this.vr.getFen();
297 if (this.sound == 2)
298 new Audio("/sounds/move.mp3").play().catch(err => {});
299 if (this.mode == "human")
300 {
301 updateStorage(move); //after our moves and opponent moves
302 if (this.vr.turn == this.userColor)
303 this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
304 }
305 else if (this.mode == "computer")
306 {
307 // Send the move to web worker (including his own moves)
308 this.compWorker.postMessage(["newmove",move]);
309 }
310 if (this.score == "*" || this.mode == "analyze")
311 {
312 // Stack move on movesList
313 this.moves.push(move);
314 }
315 // Is opponent in check?
316 this.incheck = this.vr.getCheckSquares(this.vr.turn);
317 const score = this.vr.getCurrentScore();
318 if (score != "*")
319 {
320 if (["human","computer"].includes(this.mode))
321 this.endGame(score);
322 else //just show score on screen (allow undo)
323 this.showScoreMsg(score);
324 // TODO: notify end of game (give score)
325 }
326 else if (this.mode == "computer" && this.vr.turn != this.userColor)
327 this.playComputerMove();
328 },
329 undo: function(move) {
330 this.vr.undo(move);
331 if (this.sound == 2)
332 new Audio("/sounds/undo.mp3").play().catch(err => {});
333 this.incheck = this.vr.getCheckSquares(this.vr.turn);
334 if (this.mode == "analyze")
335 this.moves.pop();
336 },
1d184b4c
BA
337 },
338})
fd373b27 339// cursor + ........
067c675b
BA
340//TODO: confirm dialog with "opponent offers draw", avec possible bouton "prevent future offers" + bouton "proposer nulle"
341//+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions,
342//comme sur lichess
fd373b27
BA
343//
344//TODO: quand partie terminée (ci-dessus) passer partie dans indexedDB