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