Display tooltips below (better on smartphones I think)
[vchess.git] / public / javascripts / components / game.js
CommitLineData
a29d9d6b 1// TODO: use indexedDB instead of localStorage? (more flexible: allow several games)
1d184b4c
BA
2Vue.component('my-game', {
3 data: function() {
4 return {
5 vr: null, //object to check moves, store them, FEN..
6 mycolor: "w",
7 possibleMoves: [], //filled after each valid click/dragstart
8 choices: [], //promotion pieces, or checkered captures... (contain possible pieces)
9 start: {}, //pixels coordinates + id of starting square (click or drag)
10 selectedPiece: null, //moving piece (or clicked piece)
11 conn: null, //socket messages
dfb4afc1 12 score: "*", //'*' means 'unfinished'
1d184b4c
BA
13 mode: "idle", //human, computer or idle (when not playing)
14 oppid: "", //opponent ID in case of HH game
15 oppConnected: false,
186516b8 16 seek: false,
762b7c9c 17 fenStart: "",
4b5fe306 18 incheck: [],
1d184b4c
BA
19 };
20 },
21 render(h) {
22 let [sizeX,sizeY] = VariantRules.size;
23 // Precompute hints squares to facilitate rendering
24 let hintSquares = doubleArray(sizeX, sizeY, false);
25 this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
4b5fe306
BA
26 // Also precompute in-check squares
27 let incheckSq = doubleArray(sizeX, sizeY, false);
28 this.incheck.forEach(sq => { incheckSq[sq[0]][sq[1]] = true; });
1d184b4c
BA
29 let elementArray = [];
30 let square00 = document.getElementById("sq-0-0");
31 let squareWidth = !!square00
32 ? parseFloat(window.getComputedStyle(square00).width.slice(0,-2))
33 : 0;
186516b8
BA
34 const playingHuman = (this.mode == "human");
35 const playingComp = (this.mode == "computer");
1d184b4c
BA
36 let actionArray = [
37 h('button',
38 {
f3802fcd 39 on: { click: this.clickGameSeek },
1d184b4c 40 attrs: { "aria-label": 'New game VS human' },
186516b8
BA
41 'class': {
42 "tooltip": true,
0706ea91 43 "bottom": true, //display below
186516b8
BA
44 "seek": this.seek,
45 "playing": playingHuman,
46 },
1d184b4c
BA
47 },
48 [h('i', { 'class': { "material-icons": true } }, "accessibility")]),
49 h('button',
50 {
f3802fcd 51 on: { click: this.clickComputerGame },
1d184b4c 52 attrs: { "aria-label": 'New game VS computer' },
186516b8
BA
53 'class': {
54 "tooltip":true,
0706ea91 55 "bottom": true,
186516b8
BA
56 "playing": playingComp,
57 },
1d184b4c
BA
58 },
59 [h('i', { 'class': { "material-icons": true } }, "computer")])
60 ];
61 if (!!this.vr)
62 {
63 if (this.mode == "human")
64 {
65 let connectedIndic = h(
66 'div',
67 {
68 "class": {
69 "connected": this.oppConnected,
70 "disconnected": !this.oppConnected,
71 },
72 }
73 );
74 elementArray.push(connectedIndic);
75 }
76 let choices = h('div',
77 {
78 attrs: { "id": "choices" },
79 'class': { 'row': true },
80 style: {
81 //"position": "relative",
82 "display": this.choices.length>0?"block":"none",
83 "top": "-" + ((sizeY/2)*squareWidth+squareWidth/2) + "px",
84 "width": (this.choices.length * squareWidth) + "px",
85 "height": squareWidth + "px",
86 },
87 },
88 this.choices.map( m => { //a "choice" is a move
89 return h('div',
90 {
91 'class': { 'board': true },
92 style: {
93 'width': (100/this.choices.length) + "%",
94 'padding-bottom': (100/this.choices.length) + "%",
95 },
96 },
97 [h('img',
98 {
99 attrs: { "src": '/images/pieces/' + VariantRules.getPpath(m.appear[0].c+m.appear[0].p) + '.svg' },
100 'class': { 'choice-piece': true, 'board': true },
101 on: { "click": e => { this.play(m); this.choices=[]; } },
102 })
103 ]
104 );
105 })
106 );
107 // Create board element (+ reserves if needed by variant or mode)
108 let gameDiv = h('div',
109 {
110 'class': { 'game': true },
111 },
112 [_.range(sizeX).map(i => {
113 let ci = this.mycolor=='w' ? i : sizeX-i-1;
114 return h(
115 'div',
116 {
117 'class': {
118 'row': true,
119 },
120 style: { 'opacity': this.choices.length>0?"0.5":"1" },
121 },
122 _.range(sizeY).map(j => {
123 let cj = this.mycolor=='w' ? j : sizeY-j-1;
124 let elems = [];
125 if (this.vr.board[ci][cj] != VariantRules.EMPTY)
126 {
127 elems.push(
128 h(
129 'img',
130 {
131 'class': {
132 'piece': true,
133 'ghost': !!this.selectedPiece && this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
134 },
135 attrs: {
136 src: "/images/pieces/" + VariantRules.getPpath(this.vr.board[ci][cj]) + ".svg",
137 },
138 }
139 )
140 );
141 }
142 if (hintSquares[ci][cj])
143 {
144 elems.push(
145 h(
146 'img',
147 {
148 'class': {
149 'mark-square': true,
150 },
151 attrs: {
152 src: "/images/mark.svg",
153 },
154 }
155 )
156 );
157 }
30ff6e04 158 const lm = this.vr.lastMove;
e64a4eff 159 const highlight = !!lm && _.isMatch(lm.end, {x:ci,y:cj});
1d184b4c
BA
160 return h(
161 'div',
162 {
163 'class': {
164 'board': true,
165 'light-square': !highlight && (i+j)%2==0,
166 'dark-square': !highlight && (i+j)%2==1,
167 'highlight': highlight,
4b5fe306 168 'incheck': incheckSq[ci][cj],
1d184b4c
BA
169 },
170 attrs: {
171 id: this.getSquareId({x:ci,y:cj}),
172 },
173 },
174 elems
175 );
176 })
177 );
178 }), choices]
179 );
180 actionArray.push(
181 h('button',
182 {
183 on: { click: this.resign },
184 attrs: { "aria-label": 'Resign' },
0706ea91
BA
185 'class': {
186 "tooltip":true,
187 "bottom": true,
188 },
1d184b4c
BA
189 },
190 [h('i', { 'class': { "material-icons": true } }, "flag")])
191 );
192 elementArray.push(gameDiv);
193 // if (!!vr.reserve)
194 // {
195 // let reserve = h('div',
196 // {'class':{'game':true}}, [
197 // h('div',
198 // { 'class': { 'row': true }},
199 // [
200 // h('div',
201 // {'class':{'board':true}},
202 // [h('img',{'class':{"piece":true},attrs:{"src":"/images/pieces/wb.svg"}})]
203 // )
204 // ]
205 // )
206 // ],
207 // );
208 // elementArray.push(reserve);
209 // }
f3802fcd 210 const eogMessage = this.getEndgameMessage(this.score);
1d184b4c
BA
211 const modalEog = [
212 h('input',
213 {
ecf44502 214 attrs: { "id": "modal-eog", type: "checkbox" },
1d184b4c
BA
215 "class": { "modal": true },
216 }),
217 h('div',
218 {
219 attrs: { "role": "dialog", "aria-labelledby": "dialog-title" },
220 },
221 [
222 h('div',
223 {
224 "class": { "card": true, "smallpad": true },
225 },
01a135e2
BA
226 [
227 h('label',
228 {
229 attrs: { "for": "modal-eog" },
230 "class": { "modal-close": true },
231 }
232 ),
233 h('h3',
234 {
235 "class": { "section": true },
236 domProps: { innerHTML: eogMessage },
237 }
238 )
239 ]
1d184b4c
BA
240 )
241 ]
242 )
243 ];
244 elementArray = elementArray.concat(modalEog);
245 }
246 const modalNewgame = [
247 h('input',
248 {
ecf44502 249 attrs: { "id": "modal-newgame", type: "checkbox" },
1d184b4c
BA
250 "class": { "modal": true },
251 }),
252 h('div',
253 {
254 attrs: { "role": "dialog", "aria-labelledby": "dialog-title" },
255 },
256 [
257 h('div',
258 {
259 "class": { "card": true, "smallpad": true },
260 },
261 [
262 h('label',
263 {
ecf44502 264 attrs: { "id": "close-newgame", "for": "modal-newgame" },
1d184b4c
BA
265 "class": { "modal-close": true },
266 }
267 ),
268 h('h3',
269 {
270 "class": { "section": true },
271 domProps: { innerHTML: "New game" },
272 }
273 ),
274 h('p',
275 {
276 "class": { "section": true },
277 domProps: { innerHTML: "Waiting for opponent..." },
278 }
279 )
280 ]
281 )
282 ]
283 )
284 ];
285 elementArray = elementArray.concat(modalNewgame);
286 const actions = h('div',
287 {
288 attrs: { "id": "actions" },
289 'class': { 'text-center': true },
290 },
291 actionArray
292 );
293 elementArray.push(actions);
01a135e2
BA
294 if (this.score != "*")
295 {
296 elementArray.push(
297 h('div',
298 { },
299 [
300 h('p',
301 {
302 domProps: {
303 innerHTML: this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode)
304 }
305 }
306 )
307 ]
308 )
309 );
310 }
1d184b4c
BA
311 return h(
312 'div',
313 {
314 'class': {
315 "col-sm-12":true,
316 "col-md-8":true,
317 "col-md-offset-2":true,
318 "col-lg-6":true,
319 "col-lg-offset-3":true,
320 },
321 // NOTE: click = mousedown + mouseup --> what about smartphone?!
322 on: {
323 mousedown: this.mousedown,
324 mousemove: this.mousemove,
325 mouseup: this.mouseup,
326 touchdown: this.mousedown,
327 touchmove: this.mousemove,
328 touchup: this.mouseup,
329 },
330 },
331 elementArray
332 );
333 },
334 created: function() {
335 const url = socketUrl;
336 const continuation = (localStorage.getItem("variant") === variant);
337 this.myid = continuation
338 ? localStorage.getItem("myid")
339 // random enough (TODO: function)
340 : (Date.now().toString(36) + Math.random().toString(36).substr(2, 7)).toUpperCase();
341 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
d35f20e4 342 const socketOpenListener = () => {
1d184b4c
BA
343 if (continuation)
344 {
dfb4afc1
BA
345 const fen = localStorage.getItem("fen");
346 const mycolor = localStorage.getItem("mycolor");
347 const oppid = localStorage.getItem("oppid");
348 const moves = JSON.parse(localStorage.getItem("moves"));
349 this.newGame("human", fen, mycolor, oppid, moves, true);
a29d9d6b 350 // Send ping to server (answer pong if opponent is connected)
ecf44502 351 this.conn.send(JSON.stringify({code:"ping",oppid:this.oppid}));
1d184b4c 352 }
30ff6e04
BA
353 else if (localStorage.getItem("newgame") === variant)
354 {
355 // New game request has been cancelled on disconnect
186516b8 356 this.seek = true;
001344b9 357 this.newGame("human", undefined, undefined, undefined, undefined, "reconnect");
30ff6e04 358 }
1d184b4c 359 };
d35f20e4 360 const socketMessageListener = msg => {
1d184b4c 361 const data = JSON.parse(msg.data);
f3802fcd 362 console.log("Receive message: " + data.code);
1d184b4c
BA
363 switch (data.code)
364 {
365 case "newgame": //opponent found
366 this.newGame("human", data.fen, data.color, data.oppid); //oppid: opponent socket ID
367 break;
368 case "newmove": //..he played!
369 this.play(data.move, "animate");
370 break;
f3802fcd 371 case "pong": //received if we sent a ping (game still alive on our side)
1d184b4c 372 this.oppConnected = true;
a29d9d6b 373 const L = this.vr.moves.length;
f3802fcd 374 // Send our "last state" informations to opponent
a29d9d6b
BA
375 this.conn.send(JSON.stringify({
376 code:"lastate",
f3802fcd 377 oppid:this.oppid,
a29d9d6b
BA
378 lastMove:L>0?this.vr.moves[L-1]:undefined,
379 movesCount:L,
380 }));
1d184b4c 381 break;
a29d9d6b
BA
382 case "lastate": //got opponent infos about last move (we might have resigned)
383 if (this.mode!="human" || this.oppid!=data.oppid)
384 {
385 // OK, we resigned
386 this.conn.send(JSON.stringify({
387 code:"lastate",
f3802fcd 388 oppid:this.oppid,
a29d9d6b
BA
389 lastMove:undefined,
390 movesCount:-1,
391 }));
392 }
393 else if (data.movesCount < 0)
394 {
395 // OK, he resigned
396 this.endGame(this.mycolor=="w"?"1-0":"0-1");
397 }
398 else if (data.movesCount < this.vr.moves.length)
399 {
400 // We must tell last move to opponent
401 const L = this.vr.moves.length;
402 this.conn.send(JSON.stringify({
403 code:"lastate",
f3802fcd 404 oppid:this.oppid,
a29d9d6b
BA
405 lastMove:this.vr.moves[L-1],
406 movesCount:L,
407 }));
408 }
409 else if (data.movesCount > this.vr.moves.length) //just got last move from him
410 this.play(data.lastMove, "animate");
ecf44502 411 break;
1d184b4c 412 case "resign": //..you won!
dfb4afc1 413 this.endGame(this.mycolor=="w"?"1-0":"0-1");
1d184b4c 414 break;
f3802fcd 415 // TODO: also use (dis)connect info to count online players?
1d184b4c
BA
416 case "connect":
417 case "disconnect":
418 if (this.mode == "human" && this.oppid == data.id)
419 this.oppConnected = (data.code == "connect");
420 break;
421 }
422 };
d35f20e4 423 const socketCloseListener = () => {
d35f20e4
BA
424 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
425 this.conn.addEventListener('open', socketOpenListener);
426 this.conn.addEventListener('message', socketMessageListener);
427 this.conn.addEventListener('close', socketCloseListener);
428 };
429 this.conn.onopen = socketOpenListener;
430 this.conn.onmessage = socketMessageListener;
431 this.conn.onclose = socketCloseListener;
1d184b4c
BA
432 },
433 methods: {
dfb4afc1
BA
434 endGame: function(score) {
435 this.score = score;
ecf44502 436 let modalBox = document.getElementById("modal-eog");
186516b8 437 modalBox.checked = true;
01a135e2 438 setTimeout(() => { modalBox.checked = false; }, 2000);
1d184b4c
BA
439 if (this.mode == "human")
440 this.clearStorage();
441 this.mode = "idle";
442 this.oppid = "";
443 },
f3802fcd
BA
444 getEndgameMessage: function(score) {
445 let eogMessage = "Unfinished";
446 switch (this.score)
447 {
448 case "1-0":
449 eogMessage = "White win";
450 break;
451 case "0-1":
452 eogMessage = "Black win";
453 break;
454 case "1/2":
455 eogMessage = "Draw";
456 break;
457 }
458 return eogMessage;
459 },
1d184b4c
BA
460 resign: function() {
461 if (this.mode == "human" && this.oppConnected)
d35f20e4
BA
462 {
463 try {
464 this.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
465 } catch (INVALID_STATE_ERR) {
a29d9d6b 466 return; //socket is not ready (and not yet reconnected)
d35f20e4
BA
467 }
468 }
dfb4afc1 469 this.endGame(this.mycolor=="w"?"0-1":"1-0");
1d184b4c 470 },
762b7c9c
BA
471 setStorage: function() {
472 localStorage.setItem("myid", this.myid);
473 localStorage.setItem("variant", variant);
474 localStorage.setItem("mycolor", this.mycolor);
475 localStorage.setItem("oppid", this.oppid);
476 localStorage.setItem("fenStart", this.fenStart);
477 localStorage.setItem("moves", JSON.stringify(this.vr.moves));
1d184b4c 478 localStorage.setItem("fen", this.vr.getFen());
762b7c9c
BA
479 },
480 updateStorage: function() {
dfb4afc1 481 localStorage.setItem("moves", JSON.stringify(this.vr.moves));
762b7c9c 482 localStorage.setItem("fen", this.vr.getFen());
1d184b4c
BA
483 },
484 clearStorage: function() {
485 delete localStorage["variant"];
486 delete localStorage["myid"];
487 delete localStorage["mycolor"];
488 delete localStorage["oppid"];
762b7c9c 489 delete localStorage["fenStart"];
1d184b4c 490 delete localStorage["fen"];
dfb4afc1 491 delete localStorage["moves"];
1d184b4c 492 },
f3802fcd
BA
493 clickGameSeek: function() {
494 if (this.mode == "human")
495 return; //no newgame while playing
496 if (this.seek)
01a135e2 497 {
f3802fcd 498 delete localStorage["newgame"]; //cancel game seek
01a135e2
BA
499 this.seek = false;
500 }
f3802fcd 501 else
f3802fcd 502 this.newGame("human");
f3802fcd
BA
503 },
504 clickComputerGame: function() {
505 if (this.mode == "human")
506 return; //no newgame while playing
507 this.newGame("computer");
508 },
dfb4afc1 509 newGame: function(mode, fenInit, color, oppId, moves, continuation) {
1d184b4c
BA
510 const fen = fenInit || VariantRules.GenRandInitFen();
511 console.log(fen); //DEBUG
dfb4afc1 512 this.score = "*";
1d184b4c
BA
513 if (mode=="human" && !oppId)
514 {
cd3174c5
BA
515 const storageVariant = localStorage.getItem("variant");
516 if (!!storageVariant && storageVariant !== variant)
517 {
cd3174c5
BA
518 alert("Finish your " + storageVariant + " game first!");
519 return;
520 }
1d184b4c 521 // Send game request and wait..
01a135e2
BA
522 localStorage["newgame"] = variant;
523 this.seek = true;
1d184b4c 524 this.clearStorage(); //in case of
d35f20e4
BA
525 try {
526 this.conn.send(JSON.stringify({code:"newgame", fen:fen}));
527 } catch (INVALID_STATE_ERR) {
528 return; //nothing achieved
529 }
f3802fcd 530 if (continuation !== "reconnect") //TODO: bad HACK...
a68d899d 531 {
ecf44502 532 let modalBox = document.getElementById("modal-newgame");
a68d899d
BA
533 modalBox.checked = true;
534 setTimeout(() => { modalBox.checked = false; }, 2000);
535 }
1d184b4c
BA
536 return;
537 }
dfb4afc1 538 this.vr = new VariantRules(fen, moves || []);
1d184b4c 539 this.mode = mode;
1fcaa356 540 this.incheck = []; //in case of
762b7c9c
BA
541 this.fenStart = continuation
542 ? localStorage.getItem("fenStart")
543 : fen.split(" ")[0]; //Only the position matters
1d184b4c
BA
544 if (mode=="human")
545 {
546 // Opponent found!
547 if (!continuation)
548 {
ecf44502 549 // Not playing sound on game continuation:
ea8417ff 550 new Audio("/sounds/newgame.mp3").play().then(() => {}).catch(err => {});
ecf44502 551 document.getElementById("modal-newgame").checked = false;
1d184b4c
BA
552 }
553 this.oppid = oppId;
554 this.oppConnected = true;
555 this.mycolor = color;
186516b8 556 this.seek = false;
e64a4eff
BA
557 if (!!moves && moves.length > 0) //imply continuation
558 {
559 const oppCol = this.vr.turn;
560 const lastMove = moves[moves.length-1];
cd4cad04 561 this.vr.undo(lastMove);
4b5fe306 562 this.incheck = this.vr.getCheckSquares(lastMove, oppCol);
e64a4eff
BA
563 this.vr.play(lastMove, "ingame");
564 }
186516b8 565 delete localStorage["newgame"];
762b7c9c 566 this.setStorage(); //in case of interruptions
1d184b4c
BA
567 }
568 else //against computer
569 {
570 this.mycolor = Math.random() < 0.5 ? 'w' : 'b';
571 if (this.mycolor == 'b')
572 setTimeout(this.playComputerMove, 500);
573 }
574 },
575 playComputerMove: function() {
576 const compColor = this.mycolor=='w' ? 'b' : 'w';
577 const compMove = this.vr.getComputerMove(compColor);
578 // HACK: avoid selecting elements before they appear on page:
579 setTimeout(() => this.play(compMove, "animate"), 500);
580 },
581 // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
582 getSquareId: function(o) {
583 // NOTE: a separator is required to allow any size of board
584 return "sq-" + o.x + "-" + o.y;
585 },
586 // Inverse function
587 getSquareFromId: function(id) {
588 let idParts = id.split('-');
589 return [parseInt(idParts[1]), parseInt(idParts[2])];
590 },
591 mousedown: function(e) {
592 e = e || window.event;
593 e.preventDefault(); //disable native drag & drop
594 if (!this.selectedPiece && e.target.classList.contains("piece"))
595 {
596 // Next few lines to center the piece on mouse cursor
597 let rect = e.target.parentNode.getBoundingClientRect();
598 this.start = {
599 x: rect.x + rect.width/2,
600 y: rect.y + rect.width/2,
601 id: e.target.parentNode.id
602 };
603 this.selectedPiece = e.target.cloneNode();
604 this.selectedPiece.style.position = "absolute";
605 this.selectedPiece.style.top = 0;
606 this.selectedPiece.style.display = "inline-block";
607 this.selectedPiece.style.zIndex = 3000;
608 let startSquare = this.getSquareFromId(e.target.parentNode.id);
609 this.possibleMoves = this.vr.canIplay(this.mycolor,startSquare)
610 ? this.vr.getPossibleMovesFrom(startSquare)
611 : [];
612 e.target.parentNode.appendChild(this.selectedPiece);
613 }
614 },
615 mousemove: function(e) {
616 if (!this.selectedPiece)
617 return;
618 e = e || window.event;
619 // If there is an active element, move it around
620 if (!!this.selectedPiece)
621 {
622 this.selectedPiece.style.left = (e.clientX-this.start.x) + "px";
623 this.selectedPiece.style.top = (e.clientY-this.start.y) + "px";
624 }
625 },
626 mouseup: function(e) {
627 if (!this.selectedPiece)
628 return;
629 e = e || window.event;
630 // Read drop target (or parentElement, parentNode... if type == "img")
631 this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coordinates
632 let landing = document.elementFromPoint(e.clientX, e.clientY);
633 this.selectedPiece.style.zIndex = 3000;
634 while (landing.tagName == "IMG") //classList.contains(piece) fails because of mark/highlight
635 landing = landing.parentNode;
636 if (this.start.id == landing.id) //a click: selectedPiece and possibleMoves already filled
637 return;
638 // OK: process move attempt
639 let endSquare = this.getSquareFromId(landing.id);
640 let moves = this.findMatchingMoves(endSquare);
641 this.possibleMoves = [];
642 if (moves.length > 1)
643 this.choices = moves;
644 else if (moves.length==1)
645 this.play(moves[0]);
646 // Else: impossible move
647 this.selectedPiece.parentNode.removeChild(this.selectedPiece);
648 delete this.selectedPiece;
649 this.selectedPiece = null;
650 },
651 findMatchingMoves: function(endSquare) {
652 // Run through moves list and return the matching set (if promotions...)
653 let moves = [];
654 this.possibleMoves.forEach(function(m) {
655 if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
656 moves.push(m);
657 });
658 return moves;
659 },
660 animateMove: function(move) {
661 let startSquare = document.getElementById(this.getSquareId(move.start));
662 let endSquare = document.getElementById(this.getSquareId(move.end));
663 let rectStart = startSquare.getBoundingClientRect();
664 let rectEnd = endSquare.getBoundingClientRect();
665 let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
666 let movingPiece = document.querySelector("#" + this.getSquareId(move.start) + " > img.piece");
667 // HACK for animation (otherwise with positive translate, image slides "under background"...)
668 // Possible improvement: just alter squares on the piece's way...
669 squares = document.getElementsByClassName("board");
670 for (let i=0; i<squares.length; i++)
671 {
672 let square = squares.item(i);
673 if (square.id != this.getSquareId(move.start))
674 square.style.zIndex = "-1";
675 }
676 movingPiece.style.transform = "translate(" + translation.x + "px," + translation.y + "px)";
677 movingPiece.style.transitionDuration = "0.2s";
678 movingPiece.style.zIndex = "3000";
679 setTimeout( () => {
680 for (let i=0; i<squares.length; i++)
681 squares.item(i).style.zIndex = "auto";
682 movingPiece.style = {}; //required e.g. for 0-0 with KR swap
683 this.play(move);
684 }, 200);
685 },
686 play: function(move, programmatic) {
687 if (!!programmatic) //computer or human opponent
688 {
689 this.animateMove(move);
690 return;
691 }
e64a4eff 692 const oppCol = this.vr.getOppCol(this.vr.turn);
4b5fe306 693 this.incheck = this.vr.getCheckSquares(move, oppCol); //is opponent in check?
1d184b4c
BA
694 // Not programmatic, or animation is over
695 if (this.mode == "human" && this.vr.turn == this.mycolor)
a29d9d6b 696 this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
ea8417ff 697 new Audio("/sounds/chessmove1.mp3").play().then(() => {}).catch(err => {});
1d184b4c
BA
698 this.vr.play(move, "ingame");
699 if (this.mode == "human")
700 this.updateStorage(); //after our moves and opponent moves
701 const eog = this.vr.checkGameOver(this.vr.turn);
702 if (eog != "*")
703 this.endGame(eog);
704 else if (this.mode == "computer" && this.vr.turn != this.mycolor)
705 setTimeout(this.playComputerMove, 500);
706 },
707 },
708})