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