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