Fix some typos and forgotten things
[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 this.score = score;
1171 if (["human","computer"].includes(this.mode))
1172 {
1173 const prefix = (this.mode=="computer" ? "comp-" : "");
1174 localStorage.setItem(prefix+"score", score);
1175 }
1176 this.showScoreMsg();
1177 // Variants may have special PGN structure (so next function isn't defined here)
1178 this.pgnTxt = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode);
1179 if (this.mode == "human" && this.oppConnected)
1180 {
1181 // Send our nickname to opponent
1182 this.conn.send(JSON.stringify({
1183 code:"myname", name:this.myname, oppid:this.oppid}));
1184 }
1185 this.mode = (this.mode=="human" ? "chat" : "idle");
1186 this.cursor = this.vr.moves.length; //to navigate in finished game
1187 },
1188 setStorage: function() {
1189 if (this.mode=="human")
1190 {
1191 localStorage.setItem("myid", this.myid);
1192 localStorage.setItem("oppid", this.oppid);
1193 localStorage.setItem("gameId", this.gameId);
1194 }
1195 // 'prefix' = "comp-" to resume games vs. computer
1196 const prefix = (this.mode=="computer" ? "comp-" : "");
1197 localStorage.setItem(prefix+"variant", variant);
1198 localStorage.setItem(prefix+"mycolor", this.mycolor);
1199 localStorage.setItem(prefix+"fenStart", this.fenStart);
1200 localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
1201 localStorage.setItem(prefix+"fen", this.vr.getFen());
1202 localStorage.setItem(prefix+"score", "*");
1203 },
1204 updateStorage: function() {
1205 const prefix = (this.mode=="computer" ? "comp-" : "");
1206 localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
1207 localStorage.setItem(prefix+"fen", this.vr.getFen());
1208 if (this.score != "*")
1209 localStorage.setItem(prefix+"score", this.score);
1210 },
1211 // "computer mode" clearing is done through the menu
1212 clearStorage: function() {
1213 if (["human","chat"].includes(this.mode))
1214 {
1215 delete localStorage["myid"];
1216 delete localStorage["oppid"];
1217 delete localStorage["gameId"];
1218 }
1219 const prefix = (this.mode=="computer" ? "comp-" : "");
1220 delete localStorage[prefix+"variant"];
1221 delete localStorage[prefix+"mycolor"];
1222 delete localStorage[prefix+"fenStart"];
1223 delete localStorage[prefix+"moves"];
1224 delete localStorage[prefix+"fen"];
1225 delete localStorage[prefix+"score"];
1226 },
1227 // HACK because mini-css tooltips are persistent after click...
1228 getRidOfTooltip: function(elt) {
1229 elt.style.visibility = "hidden";
1230 setTimeout(() => { elt.style.visibility="visible"; }, 100);
1231 },
1232 startChat: function(e) {
1233 this.getRidOfTooltip(e.currentTarget);
1234 document.getElementById("modal-chat").checked = true;
1235 },
1236 clearComputerGame: function(e) {
1237 this.getRidOfTooltip(e.currentTarget);
1238 this.clearStorage(); //this.mode=="computer" (already checked)
1239 location.reload(); //to see clearing effects
1240 },
1241 showSettings: function(e) {
1242 this.getRidOfTooltip(e.currentTarget);
1243 document.getElementById("modal-settings").checked = true;
1244 },
1245 toggleHints: function() {
1246 this.hints = !this.hints;
1247 setCookie("hints", this.hints ? "1" : "0");
1248 },
1249 setColor: function(e) {
1250 this.color = e.target.options[e.target.selectedIndex].value;
1251 setCookie("color", this.color);
1252 },
1253 setSound: function(e) {
1254 this.sound = parseInt(e.target.options[e.target.selectedIndex].value);
1255 setCookie("sound", this.sound);
1256 },
1257 clickGameSeek: function(e) {
1258 this.getRidOfTooltip(e.currentTarget);
1259 if (this.mode == "human")
1260 return; //no newgame while playing
1261 if (this.seek)
1262 {
1263 this.conn.send(JSON.stringify({code:"cancelnewgame"}));
1264 this.seek = false;
1265 }
1266 else
1267 this.newGame("human");
1268 },
1269 clickComputerGame: function(e) {
1270 this.getRidOfTooltip(e.currentTarget);
1271 this.newGame("computer");
1272 },
1273 clickFriendGame: function(e) {
1274 this.getRidOfTooltip(e.currentTarget);
1275 document.getElementById("modal-fenedit").checked = true;
1276 },
1277 resign: function(e) {
1278 this.getRidOfTooltip(e.currentTarget);
1279 if (this.mode == "human" && this.oppConnected)
1280 {
1281 try {
1282 this.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
1283 } catch (INVALID_STATE_ERR) {
1284 return; //socket is not ready (and not yet reconnected)
1285 }
1286 }
1287 this.endGame(this.mycolor=="w"?"0-1":"1-0");
1288 },
1289 newGame: function(mode, fenInit, color, oppId) {
1290 let fen = fenInit || VariantRules.GenRandInitFen();
1291 console.log(fen); //DEBUG
1292 if (mode=="human" && !oppId)
1293 {
1294 const storageVariant = localStorage.getItem("variant");
1295 if (!!storageVariant && storageVariant !== variant)
1296 return alert("Finish your " + storageVariant + " game first!");
1297 // Send game request and wait..
1298 try {
1299 this.conn.send(JSON.stringify({code:"newgame", fen:fen}));
1300 } catch (INVALID_STATE_ERR) {
1301 return; //nothing achieved
1302 }
1303 this.seek = true;
1304 let modalBox = document.getElementById("modal-newgame");
1305 modalBox.checked = true;
1306 setTimeout(() => { modalBox.checked = false; }, 2000);
1307 return;
1308 }
1309 if (["human","chat"].includes(this.mode))
1310 {
1311 // Start a new game vs. another human (or...) => forget about current one
1312 this.clearStorage();
1313 }
1314 if (mode == "computer")
1315 {
1316 const storageVariant = localStorage.getItem("comp-variant");
1317 if (!!storageVariant)
1318 {
1319 const score = localStorage.getItem("comp-score");
1320 if (storageVariant !== variant && score == "*")
1321 {
1322 if (!confirm("Unfinished " + storageVariant +
1323 " computer game will be erased"))
1324 {
1325 return;
1326 }
1327 }
1328 else if (score == "*")
1329 return this.continueGame("computer");
1330 }
1331 }
1332 this.vr = new VariantRules(fen, []);
1333 this.score = "*";
1334 this.pgnTxt = ""; //redundant with this.score = "*", but cleaner
1335 this.mode = mode;
1336 this.incheck = [];
1337 this.fenStart = V.ParseFen(fen).position; //this is enough
1338 if (mode=="human")
1339 {
1340 // Opponent found!
1341 this.gameId = getRandString();
1342 this.oppid = oppId;
1343 this.oppConnected = true;
1344 this.mycolor = color;
1345 this.seek = false;
1346 if (this.sound >= 1)
1347 new Audio("/sounds/newgame.mp3").play().catch(err => {});
1348 document.getElementById("modal-newgame").checked = false;
1349 this.setStorage(); //in case of interruptions
1350 }
1351 else if (mode == "computer")
1352 {
1353 this.compWorker.postMessage(["init",this.vr.getFen()]);
1354 this.mycolor = (Math.random() < 0.5 ? 'w' : 'b');
1355 this.setStorage(); //store game state
1356 if (this.mycolor != this.vr.turn)
1357 this.playComputerMove();
1358 }
1359 //else: against a (IRL) friend or problem solving: nothing more to do
1360 },
1361 continueGame: function(mode) {
1362 this.mode = mode;
1363 this.oppid = (mode=="human" ? localStorage.getItem("oppid") : undefined);
1364 const prefix = (mode=="computer" ? "comp-" : "");
1365 this.mycolor = localStorage.getItem(prefix+"mycolor");
1366 const moves = JSON.parse(localStorage.getItem(prefix+"moves"));
1367 const fen = localStorage.getItem(prefix+"fen");
1368 const score = localStorage.getItem(prefix+"score"); //set in "endGame()"
1369 this.fenStart = localStorage.getItem(prefix+"fenStart");
1370 if (mode == "human")
1371 {
1372 this.gameId = localStorage.getItem("gameId");
1373 // Send ping to server (answer pong if opponent is connected)
1374 this.conn.send(JSON.stringify({
1375 code:"ping",oppid:this.oppid,gameId:this.gameId}));
1376 }
1377 else
1378 this.compWorker.postMessage(["init",fen]);
1379 this.vr = new VariantRules(fen, moves);
1380 if (moves.length > 0)
1381 {
1382 const lastMove = moves[moves.length-1];
1383 this.vr.undo(lastMove);
1384 this.incheck = this.vr.getCheckSquares(lastMove);
1385 this.vr.play(lastMove, "ingame");
1386 }
1387 if (score != "*")
1388 {
1389 // Small delay required when continuation run faster than drawing page
1390 setTimeout(() => this.endGame(score), 100);
1391 }
1392 },
1393 playComputerMove: function() {
1394 this.timeStart = Date.now();
1395 this.compWorker.postMessage(["askmove"]);
1396 },
1397 // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
1398 getSquareId: function(o) {
1399 // NOTE: a separator is required to allow any size of board
1400 return "sq-" + o.x + "-" + o.y;
1401 },
1402 // Inverse function
1403 getSquareFromId: function(id) {
1404 let idParts = id.split('-');
1405 return [parseInt(idParts[1]), parseInt(idParts[2])];
1406 },
1407 mousedown: function(e) {
1408 e = e || window.event;
1409 let ingame = false;
1410 let elem = e.target;
1411 while (!ingame && elem !== null)
1412 {
1413 if (elem.classList.contains("game"))
1414 {
1415 ingame = true;
1416 break;
1417 }
1418 elem = elem.parentElement;
1419 }
1420 if (!ingame) //let default behavior (click on button...)
1421 return;
1422 e.preventDefault(); //disable native drag & drop
1423 if (!this.selectedPiece && e.target.classList.contains("piece"))
1424 {
1425 // Next few lines to center the piece on mouse cursor
1426 let rect = e.target.parentNode.getBoundingClientRect();
1427 this.start = {
1428 x: rect.x + rect.width/2,
1429 y: rect.y + rect.width/2,
1430 id: e.target.parentNode.id
1431 };
1432 this.selectedPiece = e.target.cloneNode();
1433 this.selectedPiece.style.position = "absolute";
1434 this.selectedPiece.style.top = 0;
1435 this.selectedPiece.style.display = "inline-block";
1436 this.selectedPiece.style.zIndex = 3000;
1437 const startSquare = this.getSquareFromId(e.target.parentNode.id);
1438 this.possibleMoves = [];
1439 if (!["idle","chat"].includes(this.mode))
1440 {
1441 const color = ["friend","problem"].includes(this.mode)
1442 ? this.vr.turn
1443 : this.mycolor;
1444 if (this.vr.canIplay(color,startSquare))
1445 this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
1446 }
1447 // Next line add moving piece just after current image
1448 // (required for Crazyhouse reserve)
1449 e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling);
1450 }
1451 },
1452 mousemove: function(e) {
1453 if (!this.selectedPiece)
1454 return;
1455 e = e || window.event;
1456 // If there is an active element, move it around
1457 if (!!this.selectedPiece)
1458 {
1459 const [offsetX,offsetY] = !!e.clientX
1460 ? [e.clientX,e.clientY] //desktop browser
1461 : [e.changedTouches[0].pageX, e.changedTouches[0].pageY]; //smartphone
1462 this.selectedPiece.style.left = (offsetX-this.start.x) + "px";
1463 this.selectedPiece.style.top = (offsetY-this.start.y) + "px";
1464 }
1465 },
1466 mouseup: function(e) {
1467 if (!this.selectedPiece)
1468 return;
1469 e = e || window.event;
1470 // Read drop target (or parentElement, parentNode... if type == "img")
1471 this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords
1472 const [offsetX,offsetY] = !!e.clientX
1473 ? [e.clientX,e.clientY]
1474 : [e.changedTouches[0].pageX, e.changedTouches[0].pageY];
1475 let landing = document.elementFromPoint(offsetX, offsetY);
1476 this.selectedPiece.style.zIndex = 3000;
1477 // Next condition: classList.contains(piece) fails because of marks
1478 while (landing.tagName == "IMG")
1479 landing = landing.parentNode;
1480 if (this.start.id == landing.id)
1481 {
1482 // A click: selectedPiece and possibleMoves are already filled
1483 return;
1484 }
1485 // OK: process move attempt
1486 let endSquare = this.getSquareFromId(landing.id);
1487 let moves = this.findMatchingMoves(endSquare);
1488 this.possibleMoves = [];
1489 if (moves.length > 1)
1490 this.choices = moves;
1491 else if (moves.length==1)
1492 this.play(moves[0]);
1493 // Else: impossible move
1494 this.selectedPiece.parentNode.removeChild(this.selectedPiece);
1495 delete this.selectedPiece;
1496 this.selectedPiece = null;
1497 },
1498 findMatchingMoves: function(endSquare) {
1499 // Run through moves list and return the matching set (if promotions...)
1500 let moves = [];
1501 this.possibleMoves.forEach(function(m) {
1502 if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
1503 moves.push(m);
1504 });
1505 return moves;
1506 },
1507 animateMove: function(move) {
1508 let startSquare = document.getElementById(this.getSquareId(move.start));
1509 let endSquare = document.getElementById(this.getSquareId(move.end));
1510 let rectStart = startSquare.getBoundingClientRect();
1511 let rectEnd = endSquare.getBoundingClientRect();
1512 let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
1513 let movingPiece =
1514 document.querySelector("#" + this.getSquareId(move.start) + " > img.piece");
1515 // HACK for animation (with positive translate, image slides "under background")
1516 // Possible improvement: just alter squares on the piece's way...
1517 squares = document.getElementsByClassName("board");
1518 for (let i=0; i<squares.length; i++)
1519 {
1520 let square = squares.item(i);
1521 if (square.id != this.getSquareId(move.start))
1522 square.style.zIndex = "-1";
1523 }
1524 movingPiece.style.transform = "translate(" + translation.x + "px," +
1525 translation.y + "px)";
1526 movingPiece.style.transitionDuration = "0.2s";
1527 movingPiece.style.zIndex = "3000";
1528 setTimeout( () => {
1529 for (let i=0; i<squares.length; i++)
1530 squares.item(i).style.zIndex = "auto";
1531 movingPiece.style = {}; //required e.g. for 0-0 with KR swap
1532 this.play(move);
1533 }, 250);
1534 },
1535 play: function(move, programmatic) {
1536 if (!move)
1537 {
1538 // Navigate after game is over
1539 if (this.cursor >= this.vr.moves.length)
1540 return; //already at the end
1541 move = this.vr.moves[this.cursor++];
1542 }
1543 if (!!programmatic) //computer or human opponent
1544 {
1545 this.animateMove(move);
1546 return;
1547 }
1548 // Not programmatic, or animation is over
1549 if (this.mode == "human" && this.vr.turn == this.mycolor)
1550 this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
1551 if (this.sound == 2)
1552 new Audio("/sounds/chessmove1.mp3").play().catch(err => {});
1553 if (!["idle","chat"].includes(this.mode))
1554 {
1555 // Emergency check, if human game started "at the same time"
1556 // TODO: robustify this...
1557 if (this.mode == "human" && !!move.computer)
1558 return;
1559 this.incheck = this.vr.getCheckSquares(move); //is opponent in check?
1560 this.vr.play(move, "ingame");
1561 if (this.mode == "computer")
1562 {
1563 // Send the move to web worker (TODO: including his own moves?!)
1564 this.compWorker.postMessage(["newmove",move]);
1565 }
1566 }
1567 else
1568 {
1569 VariantRules.PlayOnBoard(this.vr.board, move);
1570 this.$forceUpdate(); //TODO: ?!
1571 }
1572 if (!["idle","chat"].includes(this.mode))
1573 {
1574 const eog = this.vr.checkGameOver();
1575 if (eog != "*")
1576 {
1577 if (["human","computer"].includes(this.mode))
1578 this.endGame(eog);
1579 else
1580 {
1581 // Just show score on screen (allow undo)
1582 this.score = eog;
1583 this.showScoreMsg();
1584 }
1585 }
1586 }
1587 if (["human","computer"].includes(this.mode))
1588 this.updateStorage(); //after our moves and opponent moves
1589 if (this.mode == "computer" && this.vr.turn != this.mycolor && this.score == "*")
1590 this.playComputerMove();
1591 },
1592 undo: function() {
1593 // Navigate after game is over
1594 if (this.cursor == 0)
1595 return; //already at the beginning
1596 if (this.cursor == this.vr.moves.length)
1597 this.incheck = []; //in case of...
1598 const move = this.vr.moves[--this.cursor];
1599 VariantRules.UndoOnBoard(this.vr.board, move);
1600 this.$forceUpdate(); //TODO: ?!
1601 },
1602 undoInGame: function() {
1603 const lm = this.vr.lastMove;
1604 if (!!lm)
1605 {
1606 this.vr.undo(lm);
1607 const lmBefore = this.vr.lastMove;
1608 if (!!lmBefore)
1609 {
1610 this.vr.undo(lmBefore);
1611 this.incheck = this.vr.getCheckSquares(lmBefore);
1612 this.vr.play(lmBefore, "ingame");
1613 }
1614 else
1615 this.incheck = [];
1616 }
1617 },
1618 },
1619 })