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