Almost finished: just translations TODO
[vchess.git] / public / javascripts / components / game.js
1 // Game logic on a variant page
2 Vue.component('my-game', {
3 props: ["problem"],
4 data: function() {
5 return {
6 vr: null, //object to check moves, store them, FEN..
7 mycolor: "w",
8 possibleMoves: [], //filled after each valid click/dragstart
9 choices: [], //promotion pieces, or checkered captures... (as moves)
10 start: {}, //pixels coordinates + id of starting square (click or drag)
11 selectedPiece: null, //moving piece (or clicked piece)
12 conn: null, //socket connection
13 score: "*", //'*' means 'unfinished'
14 mode: "idle", //human, chat, friend, problem, computer or idle (if not playing)
15 myid: "", //our ID, always set
16 oppid: "", //opponent ID in case of HH game
17 gameId: "", //useful if opponent started other human games after we disconnected
18 myname: localStorage["username"] || "anonymous",
19 oppName: "anonymous", //opponent name, revealed after a game (if provided)
20 chats: [], //chat messages after human game
21 oppConnected: false,
22 seek: false,
23 fenStart: "",
24 incheck: [],
25 pgnTxt: "",
26 hints: (!localStorage["hints"] ? true : localStorage["hints"] === "1"),
27 color: localStorage["color"] || "lichess", //lichess, chesscom or chesstempo
28 // sound level: 0 = no sound, 1 = sound only on newgame, 2 = always
29 sound: parseInt(localStorage["sound"] || "2"),
30 // Web worker to play computer moves without freezing interface:
31 compWorker: new Worker('/javascripts/playCompMove.js'),
32 timeStart: undefined, //time when computer starts thinking
33 };
34 },
35 watch: {
36 problem: function(p) {
37 // 'problem' prop changed: update board state
38 this.newGame("problem", p.fen, V.ParseFen(p.fen).turn);
39 },
40 },
41 render(h) {
42 const [sizeX,sizeY] = [V.size.x,V.size.y];
43 // Precompute hints squares to facilitate rendering
44 let hintSquares = doubleArray(sizeX, sizeY, false);
45 this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
46 // Also precompute in-check squares
47 let incheckSq = doubleArray(sizeX, sizeY, false);
48 this.incheck.forEach(sq => { incheckSq[sq[0]][sq[1]] = true; });
49 let elementArray = [];
50 let actionArray = [];
51 actionArray.push(
52 h('button',
53 {
54 on: { click: this.clickGameSeek },
55 attrs: { "aria-label": 'New online game' },
56 'class': {
57 "tooltip": true,
58 "play": true,
59 "bottom": true, //display below
60 "seek": this.seek,
61 "playing": this.mode == "human",
62 "spaceright": true,
63 },
64 },
65 [h('i', { 'class': { "material-icons": true } }, "accessibility")])
66 );
67 if (["idle","chat","computer"].includes(this.mode))
68 {
69 actionArray.push(
70 h('button',
71 {
72 on: { click: this.clickComputerGame },
73 attrs: { "aria-label": 'New game VS computer' },
74 'class': {
75 "tooltip":true,
76 "play": true,
77 "bottom": true,
78 "playing": this.mode == "computer",
79 "spaceright": true,
80 },
81 },
82 [h('i', { 'class': { "material-icons": true } }, "computer")])
83 );
84 }
85 if (["idle","chat","friend"].includes(this.mode))
86 {
87 actionArray.push(
88 h('button',
89 {
90 on: { click: this.clickFriendGame },
91 attrs: { "aria-label": 'New IRL game' },
92 'class': {
93 "tooltip":true,
94 "play": true,
95 "bottom": true,
96 "playing": this.mode == "friend",
97 "spaceright": true,
98 },
99 },
100 [h('i', { 'class': { "material-icons": true } }, "people")])
101 );
102 }
103 if (!!this.vr)
104 {
105 const square00 = document.getElementById("sq-0-0");
106 const squareWidth = !!square00
107 ? parseFloat(window.getComputedStyle(square00).width.slice(0,-2))
108 : 0;
109 const settingsBtnElt = document.getElementById("settingsBtn");
110 const settingsStyle = !!settingsBtnElt
111 ? window.getComputedStyle(settingsBtnElt)
112 : {width:"46px", height:"26px"};
113 const [indicWidth,indicHeight] = //[44,24];
114 [
115 // NOTE: -2 for border
116 parseFloat(settingsStyle.width.slice(0,-2)) - 2,
117 parseFloat(settingsStyle.height.slice(0,-2)) - 2
118 ];
119 let aboveBoardElts = [];
120 if (["chat","human"].includes(this.mode))
121 {
122 const connectedIndic = h(
123 'div',
124 {
125 "class": {
126 "indic-left": true,
127 "connected": this.oppConnected,
128 "disconnected": !this.oppConnected,
129 },
130 style: {
131 "width": indicWidth + "px",
132 "height": indicHeight + "px",
133 },
134 }
135 );
136 aboveBoardElts.push(connectedIndic);
137 }
138 if (this.mode == "chat")
139 {
140 const chatButton = h(
141 'button',
142 {
143 on: { click: this.startChat },
144 attrs: {
145 "aria-label": 'Start chat',
146 "id": "chatBtn",
147 },
148 'class': {
149 "tooltip": true,
150 "play": true,
151 "above-board": true,
152 "indic-left": true,
153 },
154 },
155 [h('i', { 'class': { "material-icons": true } }, "chat")]
156 );
157 aboveBoardElts.push(chatButton);
158 }
159 else if (this.mode == "computer")
160 {
161 const clearButton = h(
162 'button',
163 {
164 on: { click: this.clearComputerGame },
165 attrs: {
166 "aria-label": 'Clear computer game',
167 "id": "clearBtn",
168 },
169 'class': {
170 "tooltip": true,
171 "play": true,
172 "above-board": true,
173 "indic-left": true,
174 },
175 },
176 [h('i', { 'class': { "material-icons": true } }, "clear")]
177 );
178 aboveBoardElts.push(clearButton);
179 }
180 const turnIndic = h(
181 'div',
182 {
183 "class": {
184 "indic-right": true,
185 "white-turn": this.vr.turn=="w",
186 "black-turn": this.vr.turn=="b",
187 },
188 style: {
189 "width": indicWidth + "px",
190 "height": indicHeight + "px",
191 },
192 }
193 );
194 aboveBoardElts.push(turnIndic);
195 const settingsBtn = h(
196 'button',
197 {
198 on: { click: this.showSettings },
199 attrs: {
200 "aria-label": 'Settings',
201 "id": "settingsBtn",
202 },
203 'class': {
204 "tooltip": true,
205 "play": true,
206 "above-board": true,
207 "indic-right": true,
208 },
209 },
210 [h('i', { 'class': { "material-icons": true } }, "settings")]
211 );
212 aboveBoardElts.push(settingsBtn);
213 elementArray.push(
214 h('div',
215 { "class": { "aboveboard-wrapper": true } },
216 aboveBoardElts
217 )
218 );
219 if (this.mode == "problem")
220 {
221 // Show problem instructions
222 elementArray.push(
223 h('div',
224 {
225 attrs: { id: "instructions-div" },
226 "class": {
227 "clearer": true,
228 "section-content": true,
229 },
230 },
231 [
232 h('p',
233 {
234 attrs: { id: "problem-instructions" },
235 domProps: { innerHTML: this.problem.instructions }
236 }
237 )
238 ]
239 )
240 );
241 }
242 const choices = h('div',
243 {
244 attrs: { "id": "choices" },
245 'class': { 'row': true },
246 style: {
247 "display": this.choices.length>0?"block":"none",
248 "top": "-" + ((sizeY/2)*squareWidth+squareWidth/2) + "px",
249 "width": (this.choices.length * squareWidth) + "px",
250 "height": squareWidth + "px",
251 },
252 },
253 this.choices.map( m => { //a "choice" is a move
254 return h('div',
255 {
256 'class': {
257 'board': true,
258 ['board'+sizeY]: true,
259 },
260 style: {
261 'width': (100/this.choices.length) + "%",
262 'padding-bottom': (100/this.choices.length) + "%",
263 },
264 },
265 [h('img',
266 {
267 attrs: { "src": '/images/pieces/' +
268 VariantRules.getPpath(m.appear[0].c+m.appear[0].p) + '.svg' },
269 'class': { 'choice-piece': true },
270 on: {
271 "click": e => { this.play(m); this.choices=[]; },
272 // NOTE: add 'touchstart' event to fix a problem on smartphones
273 "touchstart": e => { this.play(m); this.choices=[]; },
274 },
275 })
276 ]
277 );
278 })
279 );
280 // Create board element (+ reserves if needed by variant or mode)
281 const lm = this.vr.lastMove;
282 const showLight = this.hints &&
283 (!["idle","chat"].includes(this.mode) || this.cursor==this.vr.moves.length);
284 const gameDiv = h('div',
285 {
286 'class': {
287 'game': true,
288 'clearer': true,
289 },
290 },
291 [_.range(sizeX).map(i => {
292 let ci = (this.mycolor=='w' ? i : sizeX-i-1);
293 return h(
294 'div',
295 {
296 'class': {
297 'row': true,
298 },
299 style: { 'opacity': this.choices.length>0?"0.5":"1" },
300 },
301 _.range(sizeY).map(j => {
302 let cj = (this.mycolor=='w' ? j : sizeY-j-1);
303 let elems = [];
304 if (this.vr.board[ci][cj] != VariantRules.EMPTY)
305 {
306 elems.push(
307 h(
308 'img',
309 {
310 'class': {
311 'piece': true,
312 'ghost': !!this.selectedPiece
313 && this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
314 },
315 attrs: {
316 src: "/images/pieces/" +
317 VariantRules.getPpath(this.vr.board[ci][cj]) + ".svg",
318 },
319 }
320 )
321 );
322 }
323 if (this.hints && hintSquares[ci][cj])
324 {
325 elems.push(
326 h(
327 'img',
328 {
329 'class': {
330 'mark-square': true,
331 },
332 attrs: {
333 src: "/images/mark.svg",
334 },
335 }
336 )
337 );
338 }
339 return h(
340 'div',
341 {
342 'class': {
343 'board': true,
344 ['board'+sizeY]: true,
345 'light-square': (i+j)%2==0,
346 'dark-square': (i+j)%2==1,
347 [this.color]: true,
348 'highlight': showLight && !!lm && _.isMatch(lm.end, {x:ci,y:cj}),
349 'incheck': showLight && incheckSq[ci][cj],
350 },
351 attrs: {
352 id: this.getSquareId({x:ci,y:cj}),
353 },
354 },
355 elems
356 );
357 })
358 );
359 }), choices]
360 );
361 if (!["idle","chat"].includes(this.mode))
362 {
363 actionArray.push(
364 h('button',
365 {
366 on: { click: this.resign },
367 attrs: { "aria-label": 'Resign' },
368 'class': {
369 "tooltip":true,
370 "play": true,
371 "bottom": true,
372 },
373 },
374 [h('i', { 'class': { "material-icons": true } }, "flag")])
375 );
376 }
377 else if (this.vr.moves.length > 0)
378 {
379 // A game finished, and another is not started yet: allow navigation
380 actionArray = actionArray.concat([
381 h('button',
382 {
383 on: { click: e => this.undo() },
384 attrs: { "aria-label": 'Undo' },
385 "class": {
386 "play": true,
387 "spaceleft": true,
388 },
389 },
390 [h('i', { 'class': { "material-icons": true } }, "fast_rewind")]),
391 h('button',
392 {
393 on: { click: e => this.play() },
394 attrs: { "aria-label": 'Play' },
395 "class": {
396 "play": true,
397 "spaceleft": true,
398 },
399 },
400 [h('i', { 'class': { "material-icons": true } }, "fast_forward")]),
401 ]
402 );
403 }
404 if (["friend","problem"].includes(this.mode))
405 {
406 actionArray = actionArray.concat(
407 [
408 h('button',
409 {
410 on: { click: this.undoInGame },
411 attrs: { "aria-label": 'Undo' },
412 "class": {
413 "play": true,
414 "spaceleft": true,
415 },
416 },
417 [h('i', { 'class': { "material-icons": true } }, "undo")]
418 ),
419 h('button',
420 {
421 on: { click: () => { this.mycolor = this.vr.getOppCol(this.mycolor) } },
422 attrs: { "aria-label": 'Flip' },
423 "class": {
424 "play": true,
425 "spaceleft": true,
426 },
427 },
428 [h('i', { 'class': { "material-icons": true } }, "cached")]
429 ),
430 ]);
431 }
432 elementArray.push(gameDiv);
433 if (!!this.vr.reserve)
434 {
435 const shiftIdx = (this.mycolor=="w" ? 0 : 1);
436 let myReservePiecesArray = [];
437 for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
438 {
439 myReservePiecesArray.push(h('div',
440 {
441 'class': {'board':true, ['board'+sizeY+'-reserve']:true},
442 attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
443 },
444 [
445 h('img',
446 {
447 'class': {"piece":true, "reserve":true},
448 attrs: {
449 "src": "/images/pieces/" +
450 this.vr.getReservePpath(this.mycolor,i) + ".svg",
451 }
452 }),
453 h('sup',
454 {"class": { "reserve-count": true } },
455 [ this.vr.reserve[this.mycolor][VariantRules.RESERVE_PIECES[i]] ]
456 )
457 ]));
458 }
459 let oppReservePiecesArray = [];
460 const oppCol = this.vr.getOppCol(this.mycolor);
461 for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
462 {
463 oppReservePiecesArray.push(h('div',
464 {
465 'class': {'board':true, ['board'+sizeY+'-reserve']:true},
466 attrs: { id: this.getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
467 },
468 [
469 h('img',
470 {
471 'class': {"piece":true, "reserve":true},
472 attrs: {
473 "src": "/images/pieces/" +
474 this.vr.getReservePpath(oppCol,i) + ".svg",
475 }
476 }),
477 h('sup',
478 {"class": { "reserve-count": true } },
479 [ this.vr.reserve[oppCol][VariantRules.RESERVE_PIECES[i]] ]
480 )
481 ]));
482 }
483 let reserves = h('div',
484 {
485 'class':{
486 'game': true,
487 "reserve-div": true,
488 },
489 },
490 [
491 h('div',
492 {
493 'class': {
494 'row': true,
495 "reserve-row-1": true,
496 },
497 },
498 myReservePiecesArray
499 ),
500 h('div',
501 { 'class': { 'row': true }},
502 oppReservePiecesArray
503 )
504 ]
505 );
506 elementArray.push(reserves);
507 }
508 const modalEog = [
509 h('input',
510 {
511 attrs: { "id": "modal-eog", type: "checkbox" },
512 "class": { "modal": true },
513 }),
514 h('div',
515 {
516 attrs: { "role": "dialog", "aria-labelledby": "eogMessage" },
517 },
518 [
519 h('div',
520 {
521 "class": { "card": true, "smallpad": true },
522 },
523 [
524 h('label',
525 {
526 attrs: { "for": "modal-eog" },
527 "class": { "modal-close": true },
528 }
529 ),
530 h('h3',
531 {
532 attrs: { "id": "eogMessage" },
533 "class": { "section": true },
534 domProps: { innerHTML: this.endgameMessage },
535 }
536 )
537 ]
538 )
539 ]
540 )
541 ];
542 elementArray = elementArray.concat(modalEog);
543 }
544 const modalFenEdit = [
545 h('input',
546 {
547 attrs: { "id": "modal-fenedit", type: "checkbox" },
548 "class": { "modal": true },
549 }),
550 h('div',
551 {
552 attrs: { "role": "dialog", "aria-labelledby": "titleFenedit" },
553 },
554 [
555 h('div',
556 {
557 "class": { "card": true, "smallpad": true },
558 },
559 [
560 h('label',
561 {
562 attrs: { "id": "close-fenedit", "for": "modal-fenedit" },
563 "class": { "modal-close": true },
564 }
565 ),
566 h('h3',
567 {
568 attrs: { "id": "titleFenedit" },
569 "class": { "section": true },
570 domProps: { innerHTML: "Position + flags (FEN):" },
571 }
572 ),
573 h('input',
574 {
575 attrs: {
576 "id": "input-fen",
577 type: "text",
578 value: VariantRules.GenRandInitFen(),
579 },
580 }
581 ),
582 h('button',
583 {
584 on: { click:
585 () => {
586 const fen = document.getElementById("input-fen").value;
587 document.getElementById("modal-fenedit").checked = false;
588 this.newGame("friend", fen);
589 }
590 },
591 domProps: { innerHTML: "Ok" },
592 }
593 ),
594 h('button',
595 {
596 on: { click:
597 () => {
598 document.getElementById("input-fen").value =
599 VariantRules.GenRandInitFen();
600 }
601 },
602 domProps: { innerHTML: "Random" },
603 }
604 ),
605 ]
606 )
607 ]
608 )
609 ];
610 elementArray = elementArray.concat(modalFenEdit);
611 const modalSettings = [
612 h('input',
613 {
614 attrs: { "id": "modal-settings", type: "checkbox" },
615 "class": { "modal": true },
616 }),
617 h('div',
618 {
619 attrs: { "role": "dialog", "aria-labelledby": "settingsTitle" },
620 },
621 [
622 h('div',
623 {
624 "class": { "card": true, "smallpad": true },
625 },
626 [
627 h('label',
628 {
629 attrs: { "id": "close-settings", "for": "modal-settings" },
630 "class": { "modal-close": true },
631 }
632 ),
633 h('h3',
634 {
635 attrs: { "id": "settingsTitle" },
636 "class": { "section": true },
637 domProps: { innerHTML: "Preferences" },
638 }
639 ),
640 h('fieldset',
641 { },
642 [
643 h('label',
644 {
645 attrs: { for: "nameSetter" },
646 domProps: { innerHTML: "My name is..." },
647 },
648 ),
649 h('input',
650 {
651 attrs: {
652 "id": "nameSetter",
653 type: "text",
654 value: this.myname,
655 },
656 on: { "change": this.setMyname },
657 }
658 ),
659 ]
660 ),
661 h('fieldset',
662 { },
663 [
664 h('label',
665 {
666 attrs: { for: "setHints" },
667 domProps: { innerHTML: "Show hints?" },
668 },
669 ),
670 h('input',
671 {
672 attrs: {
673 "id": "setHints",
674 type: "checkbox",
675 checked: this.hints,
676 },
677 on: { "change": this.toggleHints },
678 }
679 ),
680 ]
681 ),
682 h('fieldset',
683 { },
684 [
685 h('label',
686 {
687 attrs: { for: "selectColor" },
688 domProps: { innerHTML: "Board colors" },
689 },
690 ),
691 h("select",
692 {
693 attrs: { "id": "selectColor" },
694 on: { "change": this.setColor },
695 },
696 [
697 h("option",
698 {
699 domProps: {
700 "value": "lichess",
701 innerHTML: "brown"
702 },
703 attrs: { "selected": this.color=="lichess" },
704 }
705 ),
706 h("option",
707 {
708 domProps: {
709 "value": "chesscom",
710 innerHTML: "green"
711 },
712 attrs: { "selected": this.color=="chesscom" },
713 }
714 ),
715 h("option",
716 {
717 domProps: {
718 "value": "chesstempo",
719 innerHTML: "blue"
720 },
721 attrs: { "selected": this.color=="chesstempo" },
722 }
723 ),
724 ],
725 ),
726 ]
727 ),
728 h('fieldset',
729 { },
730 [
731 h('label',
732 {
733 attrs: { for: "selectSound" },
734 domProps: { innerHTML: "Play sounds?" },
735 },
736 ),
737 h("select",
738 {
739 attrs: { "id": "selectSound" },
740 on: { "change": this.setSound },
741 },
742 [
743 h("option",
744 {
745 domProps: {
746 "value": "0",
747 innerHTML: "None"
748 },
749 attrs: { "selected": this.sound==0 },
750 }
751 ),
752 h("option",
753 {
754 domProps: {
755 "value": "1",
756 innerHTML: "Newgame"
757 },
758 attrs: { "selected": this.sound==1 },
759 }
760 ),
761 h("option",
762 {
763 domProps: {
764 "value": "2",
765 innerHTML: "All"
766 },
767 attrs: { "selected": this.sound==2 },
768 }
769 ),
770 ],
771 ),
772 ]
773 ),
774 ]
775 )
776 ]
777 )
778 ];
779 elementArray = elementArray.concat(modalSettings);
780 let chatEltsArray =
781 [
782 h('label',
783 {
784 attrs: { "id": "close-chat", "for": "modal-chat" },
785 "class": { "modal-close": true },
786 }
787 ),
788 h('h3',
789 {
790 attrs: { "id": "titleChat" },
791 "class": { "section": true },
792 domProps: { innerHTML: "Chat with " + this.oppName },
793 }
794 )
795 ];
796 for (let chat of this.chats)
797 {
798 chatEltsArray.push(
799 h('p',
800 {
801 "class": {
802 "my-chatmsg": chat.author==this.myid,
803 "opp-chatmsg": chat.author==this.oppid,
804 },
805 domProps: { innerHTML: chat.msg }
806 }
807 )
808 );
809 }
810 chatEltsArray = chatEltsArray.concat([
811 h('input',
812 {
813 attrs: {
814 "id": "input-chat",
815 type: "text",
816 placeholder: "Type here",
817 },
818 on: { keyup: this.trySendChat }, //if key is 'enter'
819 }
820 ),
821 h('button',
822 {
823 attrs: { id: "sendChatBtn"},
824 on: { click: this.sendChat },
825 domProps: { innerHTML: "Send" },
826 }
827 )
828 ]);
829 const modalChat = [
830 h('input',
831 {
832 attrs: { "id": "modal-chat", type: "checkbox" },
833 "class": { "modal": true },
834 }),
835 h('div',
836 {
837 attrs: { "role": "dialog", "aria-labelledby": "titleChat" },
838 },
839 [
840 h('div',
841 {
842 "class": { "card": true, "smallpad": true },
843 },
844 chatEltsArray
845 )
846 ]
847 )
848 ];
849 elementArray = elementArray.concat(modalChat);
850 const actions = h('div',
851 {
852 attrs: { "id": "actions" },
853 'class': { 'text-center': true },
854 },
855 actionArray
856 );
857 elementArray.push(actions);
858 if (this.score != "*" && this.pgnTxt.length > 0)
859 {
860 elementArray.push(
861 h('div',
862 {
863 attrs: { id: "pgn-div" },
864 "class": { "section-content": true },
865 },
866 [
867 h('a',
868 {
869 attrs: {
870 id: "download",
871 href: "#",
872 }
873 }
874 ),
875 h('p',
876 {
877 attrs: { id: "pgn-game" },
878 domProps: { innerHTML: this.pgnTxt }
879 }
880 ),
881 h('button',
882 {
883 attrs: { "id": "downloadBtn" },
884 on: { click: this.download },
885 domProps: { innerHTML: "Download game" },
886 }
887 ),
888 ]
889 )
890 );
891 }
892 else if (this.mode != "idle")
893 {
894 if (this.mode == "problem")
895 {
896 // Show problem solution (on click)
897 elementArray.push(
898 h('div',
899 {
900 attrs: { id: "solution-div" },
901 "class": { "section-content": true },
902 },
903 [
904 h('h3',
905 {
906 "class": { clickable: true },
907 domProps: { innerHTML: "Show solution" },
908 on: { click: this.toggleShowSolution },
909 }
910 ),
911 h('p',
912 {
913 attrs: { id: "problem-solution" },
914 domProps: { innerHTML: this.problem.solution }
915 }
916 )
917 ]
918 )
919 );
920 }
921 // Show current FEN
922 elementArray.push(
923 h('div',
924 {
925 attrs: { id: "fen-div" },
926 "class": { "section-content": true },
927 },
928 [
929 h('p',
930 {
931 attrs: { id: "fen-string" },
932 domProps: { innerHTML: this.vr.getBaseFen() },
933 "class": { "text-center": true },
934 }
935 )
936 ]
937 )
938 );
939 }
940 return h(
941 'div',
942 {
943 'class': {
944 "col-sm-12":true,
945 "col-md-10":true,
946 "col-md-offset-1":true,
947 "col-lg-8":true,
948 "col-lg-offset-2":true,
949 },
950 // NOTE: click = mousedown + mouseup
951 on: {
952 mousedown: this.mousedown,
953 mousemove: this.mousemove,
954 mouseup: this.mouseup,
955 touchstart: this.mousedown,
956 touchmove: this.mousemove,
957 touchend: this.mouseup,
958 },
959 },
960 elementArray
961 );
962 },
963 computed: {
964 endgameMessage: function() {
965 let eogMessage = "Unfinished";
966 switch (this.score)
967 {
968 case "1-0":
969 eogMessage = "White win";
970 break;
971 case "0-1":
972 eogMessage = "Black win";
973 break;
974 case "1/2":
975 eogMessage = "Draw";
976 break;
977 }
978 return eogMessage;
979 },
980 },
981 created: function() {
982 const url = socketUrl;
983 const humanContinuation = (localStorage.getItem("variant") === variant);
984 const computerContinuation = (localStorage.getItem("comp-variant") === variant);
985 this.myid = (humanContinuation ? localStorage.getItem("myid") : getRandString());
986 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
987 const socketOpenListener = () => {
988 if (humanContinuation) //game VS human has priority
989 this.continueGame("human");
990 else if (computerContinuation)
991 this.continueGame("computer");
992 };
993 const socketMessageListener = msg => {
994 const data = JSON.parse(msg.data);
995 const L = (!!this.vr ? this.vr.moves.length : 0);
996 switch (data.code)
997 {
998 case "oppname":
999 // Receive opponent's name
1000 this.oppName = data.name;
1001 break;
1002 case "newchat":
1003 // Receive new chat
1004 this.chats.push({msg:data.msg, author:this.oppid});
1005 break;
1006 case "duplicate":
1007 // We opened another tab on the same game
1008 this.mode = "idle";
1009 this.vr = null;
1010 alert("Already playing a game in this variant on another tab!");
1011 break;
1012 case "newgame": //opponent found
1013 // oppid: opponent socket ID
1014 this.newGame("human", data.fen, data.color, data.oppid);
1015 break;
1016 case "newmove": //..he played!
1017 this.play(data.move, "animate");
1018 break;
1019 case "pong": //received if we sent a ping (game still alive on our side)
1020 if (this.gameId != data.gameId)
1021 break; //games IDs don't match: definitely over...
1022 this.oppConnected = true;
1023 // Send our "last state" informations to opponent
1024 this.conn.send(JSON.stringify({
1025 code: "lastate",
1026 oppid: this.oppid,
1027 gameId: this.gameId,
1028 lastMove: (L>0?this.vr.moves[L-1]:undefined),
1029 movesCount: L,
1030 }));
1031 break;
1032 case "lastate": //got opponent infos about last move
1033 if (this.gameId != data.gameId)
1034 break; //games IDs don't match: nothing we can do...
1035 // OK, opponent still in game (which might be over)
1036 if (this.mode != "human")
1037 {
1038 // We finished the game (any result possible)
1039 this.conn.send(JSON.stringify({
1040 code: "lastate",
1041 oppid: data.oppid,
1042 gameId: this.gameId,
1043 score: this.score,
1044 }));
1045 }
1046 else if (!!data.score) //opponent finished the game
1047 this.endGame(data.score);
1048 else if (data.movesCount < L)
1049 {
1050 // We must tell last move to opponent
1051 this.conn.send(JSON.stringify({
1052 code: "lastate",
1053 oppid: this.oppid,
1054 lastMove: this.vr.moves[L-1],
1055 movesCount: L,
1056 }));
1057 }
1058 else if (data.movesCount > L) //just got last move from him
1059 this.play(data.lastMove, "animate");
1060 break;
1061 case "resign": //..you won!
1062 this.endGame(this.mycolor=="w"?"1-0":"0-1");
1063 break;
1064 // TODO: also use (dis)connect info to count online players?
1065 case "connect":
1066 case "disconnect":
1067 if (["human","chat"].includes(this.mode) && this.oppid == data.id)
1068 this.oppConnected = (data.code == "connect");
1069 if (this.oppConnected && this.mode == "chat")
1070 {
1071 // Send our name to the opponent, in case of he hasn't it
1072 this.conn.send(JSON.stringify({
1073 code:"myname", name:this.myname, oppid: this.oppid}));
1074 }
1075 break;
1076 }
1077 };
1078 const socketCloseListener = () => {
1079 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
1080 this.conn.addEventListener('open', socketOpenListener);
1081 this.conn.addEventListener('message', socketMessageListener);
1082 this.conn.addEventListener('close', socketCloseListener);
1083 };
1084 this.conn.onopen = socketOpenListener;
1085 this.conn.onmessage = socketMessageListener;
1086 this.conn.onclose = socketCloseListener;
1087 // Listen to keyboard left/right to navigate in game
1088 document.onkeydown = event => {
1089 if (["idle","chat"].includes(this.mode) &&
1090 !!this.vr && this.vr.moves.length > 0 && [37,39].includes(event.keyCode))
1091 {
1092 event.preventDefault();
1093 if (event.keyCode == 37) //Back
1094 this.undo();
1095 else //Forward (39)
1096 this.play();
1097 }
1098 };
1099 // Computer moves web worker logic:
1100 this.compWorker.postMessage(["scripts",variant]);
1101 const self = this;
1102 this.compWorker.onmessage = function(e) {
1103 let compMove = e.data;
1104 compMove.computer = true; //TODO: imperfect attempt to avoid ghost move
1105 // (first move) HACK: small delay to avoid selecting elements
1106 // before they appear on page:
1107 const delay = Math.max(500-(Date.now()-self.timeStart), 0);
1108 setTimeout(() => {
1109 if (self.mode == "computer") //warning: mode could have changed!
1110 self.play(compMove, "animate")
1111 }, delay);
1112 }
1113 },
1114 methods: {
1115 setMyname: function(e) {
1116 this.myname = e.target.value;
1117 localStorage["username"] = this.myname;
1118 },
1119 trySendChat: function(e) {
1120 if (e.keyCode == 13) //'enter' key
1121 this.sendChat();
1122 },
1123 sendChat: function() {
1124 let chatInput = document.getElementById("input-chat");
1125 const chatTxt = chatInput.value;
1126 chatInput.value = "";
1127 this.chats.push({msg:chatTxt, author:this.myid});
1128 this.conn.send(JSON.stringify({
1129 code:"newchat", oppid: this.oppid, msg: chatTxt}));
1130 },
1131 toggleShowSolution: function() {
1132 let problemSolution = document.getElementById("problem-solution");
1133 problemSolution.style.display =
1134 !problemSolution.style.display || problemSolution.style.display == "none"
1135 ? "block"
1136 : "none";
1137 },
1138 download: function() {
1139 let content = document.getElementById("pgn-game").innerHTML;
1140 content = content.replace(/<br>/g, "\n");
1141 // Prepare and trigger download link
1142 let downloadAnchor = document.getElementById("download");
1143 downloadAnchor.setAttribute("download", "game.pgn");
1144 downloadAnchor.href = "data:text/plain;charset=utf-8," +
1145 encodeURIComponent(content);
1146 downloadAnchor.click();
1147 },
1148 showScoreMsg: function() {
1149 let modalBox = document.getElementById("modal-eog");
1150 modalBox.checked = true;
1151 setTimeout(() => { modalBox.checked = false; }, 2000);
1152 },
1153 endGame: function(score) {
1154 this.score = score;
1155 if (["human","computer"].includes(this.mode))
1156 {
1157 const prefix = (this.mode=="computer" ? "comp-" : "");
1158 localStorage.setItem(prefix+"score", score);
1159 }
1160 this.showScoreMsg();
1161 // Variants may have special PGN structure (so next function isn't defined here)
1162 this.pgnTxt = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode);
1163 if (this.mode == "human" && this.oppConnected)
1164 {
1165 // Send our nickname to opponent
1166 this.conn.send(JSON.stringify({
1167 code:"myname", name:this.myname, oppid:this.oppid}));
1168 }
1169 this.mode = (this.mode=="human" ? "chat" : "idle");
1170 this.cursor = this.vr.moves.length; //to navigate in finished game
1171 },
1172 setStorage: function() {
1173 if (this.mode=="human")
1174 {
1175 localStorage.setItem("myid", this.myid);
1176 localStorage.setItem("oppid", this.oppid);
1177 localStorage.setItem("gameId", this.gameId);
1178 }
1179 // 'prefix' = "comp-" to resume games vs. computer
1180 const prefix = (this.mode=="computer" ? "comp-" : "");
1181 localStorage.setItem(prefix+"variant", variant);
1182 localStorage.setItem(prefix+"mycolor", this.mycolor);
1183 localStorage.setItem(prefix+"fenStart", this.fenStart);
1184 localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
1185 localStorage.setItem(prefix+"fen", this.vr.getFen());
1186 localStorage.setItem(prefix+"score", "*");
1187 },
1188 updateStorage: function() {
1189 const prefix = (this.mode=="computer" ? "comp-" : "");
1190 localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
1191 localStorage.setItem(prefix+"fen", this.vr.getFen());
1192 if (this.score != "*")
1193 localStorage.setItem(prefix+"score", this.score);
1194 },
1195 // "computer mode" clearing is done through the menu
1196 clearStorage: function() {
1197 if (["human","chat"].includes(this.mode))
1198 {
1199 delete localStorage["myid"];
1200 delete localStorage["oppid"];
1201 delete localStorage["gameId"];
1202 }
1203 const prefix = (this.mode=="computer" ? "comp-" : "");
1204 delete localStorage[prefix+"variant"];
1205 delete localStorage[prefix+"mycolor"];
1206 delete localStorage[prefix+"fenStart"];
1207 delete localStorage[prefix+"moves"];
1208 delete localStorage[prefix+"fen"];
1209 delete localStorage[prefix+"score"];
1210 },
1211 // HACK because mini-css tooltips are persistent after click...
1212 getRidOfTooltip: function(elt) {
1213 elt.style.visibility = "hidden";
1214 setTimeout(() => { elt.style.visibility="visible"; }, 100);
1215 },
1216 startChat: function(e) {
1217 this.getRidOfTooltip(e.currentTarget);
1218 document.getElementById("modal-chat").checked = true;
1219 },
1220 clearComputerGame: function(e) {
1221 this.getRidOfTooltip(e.currentTarget);
1222 this.clearStorage(); //this.mode=="computer" (already checked)
1223 location.reload(); //to see clearing effects
1224 },
1225 showSettings: function(e) {
1226 this.getRidOfTooltip(e.currentTarget);
1227 document.getElementById("modal-settings").checked = true;
1228 },
1229 toggleHints: function() {
1230 this.hints = !this.hints;
1231 localStorage["hints"] = (this.hints ? "1" : "0");
1232 },
1233 setColor: function(e) {
1234 this.color = e.target.options[e.target.selectedIndex].value;
1235 localStorage["color"] = this.color;
1236 },
1237 setSound: function(e) {
1238 this.sound = parseInt(e.target.options[e.target.selectedIndex].value);
1239 localStorage["sound"] = this.sound;
1240 },
1241 clickGameSeek: function(e) {
1242 this.getRidOfTooltip(e.currentTarget);
1243 if (this.mode == "human")
1244 return; //no newgame while playing
1245 if (this.seek)
1246 {
1247 this.conn.send(JSON.stringify({code:"cancelnewgame"}));
1248 this.seek = false;
1249 }
1250 else
1251 this.newGame("human");
1252 },
1253 clickComputerGame: function(e) {
1254 this.getRidOfTooltip(e.currentTarget);
1255 this.newGame("computer");
1256 },
1257 clickFriendGame: function(e) {
1258 this.getRidOfTooltip(e.currentTarget);
1259 document.getElementById("modal-fenedit").checked = true;
1260 },
1261 resign: function(e) {
1262 this.getRidOfTooltip(e.currentTarget);
1263 if (this.mode == "human" && this.oppConnected)
1264 {
1265 try {
1266 this.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
1267 } catch (INVALID_STATE_ERR) {
1268 return; //socket is not ready (and not yet reconnected)
1269 }
1270 }
1271 this.endGame(this.mycolor=="w"?"0-1":"1-0");
1272 },
1273 newGame: function(mode, fenInit, color, oppId) {
1274 let fen = fenInit || VariantRules.GenRandInitFen();
1275 console.log(fen); //DEBUG
1276 if (mode=="human" && !oppId)
1277 {
1278 const storageVariant = localStorage.getItem("variant");
1279 if (!!storageVariant && storageVariant !== variant)
1280 return alert("Finish your " + storageVariant + " game first!");
1281 // Send game request and wait..
1282 try {
1283 this.conn.send(JSON.stringify({code:"newgame", fen:fen}));
1284 } catch (INVALID_STATE_ERR) {
1285 return; //nothing achieved
1286 }
1287 this.seek = true;
1288 let modalBox = document.getElementById("modal-newgame");
1289 modalBox.checked = true;
1290 setTimeout(() => { modalBox.checked = false; }, 2000);
1291 return;
1292 }
1293 if (["human","chat"].includes(this.mode))
1294 {
1295 // Start a new game vs. another human (or...) => forget about current one
1296 this.clearStorage();
1297 }
1298 if (mode == "computer")
1299 {
1300 const storageVariant = localStorage.getItem("comp-variant");
1301 if (!!storageVariant)
1302 {
1303 const score = localStorage.getItem("comp-score");
1304 if (storageVariant !== variant && score == "*")
1305 {
1306 if (!confirm("Unfinished " + storageVariant +
1307 " computer game will be erased"))
1308 {
1309 return;
1310 }
1311 }
1312 else if (score == "*")
1313 return this.continueGame("computer");
1314 }
1315 }
1316 this.vr = new VariantRules(fen, []);
1317 this.score = "*";
1318 this.pgnTxt = ""; //redundant with this.score = "*", but cleaner
1319 this.mode = mode;
1320 this.incheck = [];
1321 this.fenStart = V.ParseFen(fen).position; //this is enough
1322 if (mode=="human")
1323 {
1324 // Opponent found!
1325 this.gameId = getRandString();
1326 this.oppid = oppId;
1327 this.oppConnected = true;
1328 this.mycolor = color;
1329 this.seek = false;
1330 if (this.sound >= 1)
1331 new Audio("/sounds/newgame.mp3").play().catch(err => {});
1332 document.getElementById("modal-newgame").checked = false;
1333 this.setStorage(); //in case of interruptions
1334 }
1335 else if (mode == "computer")
1336 {
1337 this.compWorker.postMessage(["init",this.vr.getFen()]);
1338 this.mycolor = (Math.random() < 0.5 ? 'w' : 'b');
1339 this.setStorage(); //store game state
1340 if (this.mycolor != this.vr.turn)
1341 this.playComputerMove();
1342 }
1343 //else: against a (IRL) friend or problem solving: nothing more to do
1344 },
1345 continueGame: function(mode) {
1346 this.mode = mode;
1347 this.oppid = (mode=="human" ? localStorage.getItem("oppid") : undefined);
1348 const prefix = (mode=="computer" ? "comp-" : "");
1349 this.mycolor = localStorage.getItem(prefix+"mycolor");
1350 const moves = JSON.parse(localStorage.getItem(prefix+"moves"));
1351 const fen = localStorage.getItem(prefix+"fen");
1352 const score = localStorage.getItem(prefix+"score"); //set in "endGame()"
1353 this.fenStart = localStorage.getItem(prefix+"fenStart");
1354 if (mode == "human")
1355 {
1356 this.gameId = localStorage.getItem("gameId");
1357 // Send ping to server (answer pong if opponent is connected)
1358 this.conn.send(JSON.stringify({
1359 code:"ping",oppid:this.oppid,gameId:this.gameId}));
1360 }
1361 else
1362 this.compWorker.postMessage(["init",fen]);
1363 this.vr = new VariantRules(fen, moves);
1364 if (moves.length > 0)
1365 {
1366 const lastMove = moves[moves.length-1];
1367 this.vr.undo(lastMove);
1368 this.incheck = this.vr.getCheckSquares(lastMove);
1369 this.vr.play(lastMove, "ingame");
1370 }
1371 if (score != "*")
1372 {
1373 // Small delay required when continuation run faster than drawing page
1374 setTimeout(() => this.endGame(score), 100);
1375 }
1376 },
1377 playComputerMove: function() {
1378 this.timeStart = Date.now();
1379 this.compWorker.postMessage(["askmove"]);
1380 },
1381 // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
1382 getSquareId: function(o) {
1383 // NOTE: a separator is required to allow any size of board
1384 return "sq-" + o.x + "-" + o.y;
1385 },
1386 // Inverse function
1387 getSquareFromId: function(id) {
1388 let idParts = id.split('-');
1389 return [parseInt(idParts[1]), parseInt(idParts[2])];
1390 },
1391 mousedown: function(e) {
1392 e = e || window.event;
1393 let ingame = false;
1394 let elem = e.target;
1395 while (!ingame && elem !== null)
1396 {
1397 if (elem.classList.contains("game"))
1398 {
1399 ingame = true;
1400 break;
1401 }
1402 elem = elem.parentElement;
1403 }
1404 if (!ingame) //let default behavior (click on button...)
1405 return;
1406 e.preventDefault(); //disable native drag & drop
1407 if (!this.selectedPiece && e.target.classList.contains("piece"))
1408 {
1409 // Next few lines to center the piece on mouse cursor
1410 let rect = e.target.parentNode.getBoundingClientRect();
1411 this.start = {
1412 x: rect.x + rect.width/2,
1413 y: rect.y + rect.width/2,
1414 id: e.target.parentNode.id
1415 };
1416 this.selectedPiece = e.target.cloneNode();
1417 this.selectedPiece.style.position = "absolute";
1418 this.selectedPiece.style.top = 0;
1419 this.selectedPiece.style.display = "inline-block";
1420 this.selectedPiece.style.zIndex = 3000;
1421 const startSquare = this.getSquareFromId(e.target.parentNode.id);
1422 this.possibleMoves = [];
1423 if (!["idle","chat"].includes(this.mode))
1424 {
1425 const color = ["friend","problem"].includes(this.mode)
1426 ? this.vr.turn
1427 : this.mycolor;
1428 if (this.vr.canIplay(color,startSquare))
1429 this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
1430 }
1431 // Next line add moving piece just after current image
1432 // (required for Crazyhouse reserve)
1433 e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling);
1434 }
1435 },
1436 mousemove: function(e) {
1437 if (!this.selectedPiece)
1438 return;
1439 e = e || window.event;
1440 // If there is an active element, move it around
1441 if (!!this.selectedPiece)
1442 {
1443 const [offsetX,offsetY] = !!e.clientX
1444 ? [e.clientX,e.clientY] //desktop browser
1445 : [e.changedTouches[0].pageX, e.changedTouches[0].pageY]; //smartphone
1446 this.selectedPiece.style.left = (offsetX-this.start.x) + "px";
1447 this.selectedPiece.style.top = (offsetY-this.start.y) + "px";
1448 }
1449 },
1450 mouseup: function(e) {
1451 if (!this.selectedPiece)
1452 return;
1453 e = e || window.event;
1454 // Read drop target (or parentElement, parentNode... if type == "img")
1455 this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords
1456 const [offsetX,offsetY] = !!e.clientX
1457 ? [e.clientX,e.clientY]
1458 : [e.changedTouches[0].pageX, e.changedTouches[0].pageY];
1459 let landing = document.elementFromPoint(offsetX, offsetY);
1460 this.selectedPiece.style.zIndex = 3000;
1461 // Next condition: classList.contains(piece) fails because of marks
1462 while (landing.tagName == "IMG")
1463 landing = landing.parentNode;
1464 if (this.start.id == landing.id)
1465 {
1466 // A click: selectedPiece and possibleMoves are already filled
1467 return;
1468 }
1469 // OK: process move attempt
1470 let endSquare = this.getSquareFromId(landing.id);
1471 let moves = this.findMatchingMoves(endSquare);
1472 this.possibleMoves = [];
1473 if (moves.length > 1)
1474 this.choices = moves;
1475 else if (moves.length==1)
1476 this.play(moves[0]);
1477 // Else: impossible move
1478 this.selectedPiece.parentNode.removeChild(this.selectedPiece);
1479 delete this.selectedPiece;
1480 this.selectedPiece = null;
1481 },
1482 findMatchingMoves: function(endSquare) {
1483 // Run through moves list and return the matching set (if promotions...)
1484 let moves = [];
1485 this.possibleMoves.forEach(function(m) {
1486 if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
1487 moves.push(m);
1488 });
1489 return moves;
1490 },
1491 animateMove: function(move) {
1492 let startSquare = document.getElementById(this.getSquareId(move.start));
1493 let endSquare = document.getElementById(this.getSquareId(move.end));
1494 let rectStart = startSquare.getBoundingClientRect();
1495 let rectEnd = endSquare.getBoundingClientRect();
1496 let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
1497 let movingPiece =
1498 document.querySelector("#" + this.getSquareId(move.start) + " > img.piece");
1499 // HACK for animation (with positive translate, image slides "under background")
1500 // Possible improvement: just alter squares on the piece's way...
1501 squares = document.getElementsByClassName("board");
1502 for (let i=0; i<squares.length; i++)
1503 {
1504 let square = squares.item(i);
1505 if (square.id != this.getSquareId(move.start))
1506 square.style.zIndex = "-1";
1507 }
1508 movingPiece.style.transform = "translate(" + translation.x + "px," +
1509 translation.y + "px)";
1510 movingPiece.style.transitionDuration = "0.2s";
1511 movingPiece.style.zIndex = "3000";
1512 setTimeout( () => {
1513 for (let i=0; i<squares.length; i++)
1514 squares.item(i).style.zIndex = "auto";
1515 movingPiece.style = {}; //required e.g. for 0-0 with KR swap
1516 this.play(move);
1517 }, 250);
1518 },
1519 play: function(move, programmatic) {
1520 if (!move)
1521 {
1522 // Navigate after game is over
1523 if (this.cursor >= this.vr.moves.length)
1524 return; //already at the end
1525 move = this.vr.moves[this.cursor++];
1526 }
1527 if (!!programmatic) //computer or human opponent
1528 {
1529 this.animateMove(move);
1530 return;
1531 }
1532 // Not programmatic, or animation is over
1533 if (this.mode == "human" && this.vr.turn == this.mycolor)
1534 this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
1535 if (!["idle","chat"].includes(this.mode))
1536 {
1537 // Emergency check, if human game started "at the same time"
1538 // TODO: robustify this...
1539 if (this.mode == "human" && !!move.computer)
1540 return;
1541 this.incheck = this.vr.getCheckSquares(move); //is opponent in check?
1542 this.vr.play(move, "ingame");
1543 if (this.sound == 2)
1544 new Audio("/sounds/move.mp3").play().catch(err => {});
1545 if (this.mode == "computer")
1546 {
1547 // Send the move to web worker (TODO: including his own moves?!)
1548 this.compWorker.postMessage(["newmove",move]);
1549 }
1550 }
1551 else
1552 {
1553 VariantRules.PlayOnBoard(this.vr.board, move);
1554 this.$forceUpdate(); //TODO: ?!
1555 }
1556 if (!["idle","chat"].includes(this.mode))
1557 {
1558 const eog = this.vr.checkGameOver();
1559 if (eog != "*")
1560 {
1561 if (["human","computer"].includes(this.mode))
1562 this.endGame(eog);
1563 else
1564 {
1565 // Just show score on screen (allow undo)
1566 this.score = eog;
1567 this.showScoreMsg();
1568 }
1569 }
1570 }
1571 if (["human","computer"].includes(this.mode))
1572 this.updateStorage(); //after our moves and opponent moves
1573 if (this.mode == "computer" && this.vr.turn != this.mycolor && this.score == "*")
1574 this.playComputerMove();
1575 },
1576 undo: function() {
1577 // Navigate after game is over
1578 if (this.cursor == 0)
1579 return; //already at the beginning
1580 if (this.cursor == this.vr.moves.length)
1581 this.incheck = []; //in case of...
1582 const move = this.vr.moves[--this.cursor];
1583 VariantRules.UndoOnBoard(this.vr.board, move);
1584 this.$forceUpdate(); //TODO: ?!
1585 },
1586 undoInGame: function() {
1587 const lm = this.vr.lastMove;
1588 if (!!lm)
1589 {
1590 this.vr.undo(lm);
1591 if (this.sound == 2)
1592 new Audio("/sounds/undo.mp3").play().catch(err => {});
1593 const lmBefore = this.vr.lastMove;
1594 if (!!lmBefore)
1595 {
1596 this.vr.undo(lmBefore);
1597 this.incheck = this.vr.getCheckSquares(lmBefore);
1598 this.vr.play(lmBefore, "ingame");
1599 }
1600 else
1601 this.incheck = [];
1602 }
1603 },
1604 },
1605 })