Fix promotion bug on smartphones
[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: {
261 "click": e => { this.play(m); this.choices=[]; },
262 // NOTE: add 'touchstart' event to fix a problem on smartphones
263 "touchstart": e => { this.play(m); this.choices=[]; },
264 },
265 })
266 ]
267 );
268 })
269 );
270 // Create board element (+ reserves if needed by variant or mode)
271 const lm = this.vr.lastMove;
272 const showLight = this.hints &&
273 (!["idle","chat"].includes(this.mode) || this.cursor==this.vr.moves.length);
274 const gameDiv = h('div',
275 {
276 'class': { 'game': true },
277 },
278 [_.range(sizeX).map(i => {
279 let ci = (this.mycolor=='w' ? i : sizeX-i-1);
280 return h(
281 'div',
282 {
283 'class': {
284 'row': true,
285 },
286 style: { 'opacity': this.choices.length>0?"0.5":"1" },
287 },
288 _.range(sizeY).map(j => {
289 let cj = (this.mycolor=='w' ? j : sizeY-j-1);
290 let elems = [];
291 if (this.vr.board[ci][cj] != VariantRules.EMPTY)
292 {
293 elems.push(
294 h(
295 'img',
296 {
297 'class': {
298 'piece': true,
299 'ghost': !!this.selectedPiece
300 && this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
301 },
302 attrs: {
303 src: "/images/pieces/" +
304 VariantRules.getPpath(this.vr.board[ci][cj]) + ".svg",
305 },
306 }
307 )
308 );
309 }
310 if (this.hints && hintSquares[ci][cj])
311 {
312 elems.push(
313 h(
314 'img',
315 {
316 'class': {
317 'mark-square': true,
318 },
319 attrs: {
320 src: "/images/mark.svg",
321 },
322 }
323 )
324 );
325 }
326 return h(
327 'div',
328 {
329 'class': {
330 'board': true,
331 ['board'+sizeY]: true,
332 'light-square': (i+j)%2==0,
333 'dark-square': (i+j)%2==1,
334 [this.color]: true,
335 'highlight': showLight && !!lm && _.isMatch(lm.end, {x:ci,y:cj}),
336 'incheck': showLight && incheckSq[ci][cj],
337 },
338 attrs: {
339 id: this.getSquareId({x:ci,y:cj}),
340 },
341 },
342 elems
343 );
344 })
345 );
346 }), choices]
347 );
348 if (!["idle","chat"].includes(this.mode))
349 {
350 actionArray.push(
351 h('button',
352 {
353 on: { click: this.resign },
354 attrs: { "aria-label": 'Resign' },
355 'class': {
356 "tooltip":true,
357 "bottom": true,
358 "small": smallScreen,
359 },
360 },
361 [h('i', { 'class': { "material-icons": true } }, "flag")])
362 );
363 }
364 else if (this.vr.moves.length > 0)
365 {
366 // A game finished, and another is not started yet: allow navigation
367 actionArray = actionArray.concat([
368 h('button',
369 {
370 on: { click: e => this.undo() },
371 attrs: { "aria-label": 'Undo' },
372 "class": {
373 "small": smallScreen,
374 "marginleft": true,
375 },
376 },
377 [h('i', { 'class': { "material-icons": true } }, "fast_rewind")]),
378 h('button',
379 {
380 on: { click: e => this.play() },
381 attrs: { "aria-label": 'Play' },
382 "class": { "small": smallScreen },
383 },
384 [h('i', { 'class': { "material-icons": true } }, "fast_forward")]),
385 ]
386 );
387 }
388 if (["friend","problem"].includes(this.mode))
389 {
390 actionArray = actionArray.concat(
391 [
392 h('button',
393 {
394 on: { click: this.undoInGame },
395 attrs: { "aria-label": 'Undo' },
396 "class": {
397 "small": smallScreen,
398 "marginleft": true,
399 },
400 },
401 [h('i', { 'class': { "material-icons": true } }, "undo")]
402 ),
403 h('button',
404 {
405 on: { click: () => { this.mycolor = this.vr.getOppCol(this.mycolor) } },
406 attrs: { "aria-label": 'Flip' },
407 "class": { "small": smallScreen },
408 },
409 [h('i', { 'class': { "material-icons": true } }, "cached")]
410 ),
411 ]);
412 }
413 elementArray.push(gameDiv);
414 if (!!this.vr.reserve)
415 {
416 const shiftIdx = (this.mycolor=="w" ? 0 : 1);
417 let myReservePiecesArray = [];
418 for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
419 {
420 myReservePiecesArray.push(h('div',
421 {
422 'class': {'board':true, ['board'+sizeY]:true},
423 attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
424 },
425 [
426 h('img',
427 {
428 'class': {"piece":true},
429 attrs: {
430 "src": "/images/pieces/" +
431 this.vr.getReservePpath(this.mycolor,i) + ".svg",
432 }
433 }),
434 h('sup',
435 {"class": { "reserve-count": true } },
436 [ this.vr.reserve[this.mycolor][VariantRules.RESERVE_PIECES[i]] ]
437 )
438 ]));
439 }
440 let oppReservePiecesArray = [];
441 const oppCol = this.vr.getOppCol(this.mycolor);
442 for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
443 {
444 oppReservePiecesArray.push(h('div',
445 {
446 'class': {'board':true, ['board'+sizeY]:true},
447 attrs: { id: this.getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
448 },
449 [
450 h('img',
451 {
452 'class': {"piece":true},
453 attrs: {
454 "src": "/images/pieces/" +
455 this.vr.getReservePpath(oppCol,i) + ".svg",
456 }
457 }),
458 h('sup',
459 {"class": { "reserve-count": true } },
460 [ this.vr.reserve[oppCol][VariantRules.RESERVE_PIECES[i]] ]
461 )
462 ]));
463 }
464 let reserves = h('div',
465 {
466 'class':{
467 'game': true,
468 "reserve-div": true,
469 },
470 },
471 [
472 h('div',
473 {
474 'class': {
475 'row': true,
476 "reserve-row-1": true,
477 },
478 },
479 myReservePiecesArray
480 ),
481 h('div',
482 { 'class': { 'row': true }},
483 oppReservePiecesArray
484 )
485 ]
486 );
487 elementArray.push(reserves);
488 }
489 const modalEog = [
490 h('input',
491 {
492 attrs: { "id": "modal-eog", type: "checkbox" },
493 "class": { "modal": true },
494 }),
495 h('div',
496 {
497 attrs: { "role": "dialog", "aria-labelledby": "eogMessage" },
498 },
499 [
500 h('div',
501 {
502 "class": { "card": true, "smallpad": true },
503 },
504 [
505 h('label',
506 {
507 attrs: { "for": "modal-eog" },
508 "class": { "modal-close": true },
509 }
510 ),
511 h('h3',
512 {
513 attrs: { "id": "eogMessage" },
514 "class": { "section": true },
515 domProps: { innerHTML: this.endgameMessage },
516 }
517 )
518 ]
519 )
520 ]
521 )
522 ];
523 elementArray = elementArray.concat(modalEog);
524 }
525 // NOTE: this modal could be in Pug view (no usage of Vue functions or variables)
526 const modalNewgame = [
527 h('input',
528 {
529 attrs: { "id": "modal-newgame", type: "checkbox" },
530 "class": { "modal": true },
531 }),
532 h('div',
533 {
534 attrs: { "role": "dialog", "aria-labelledby": "newGameTxt" },
535 },
536 [
537 h('div',
538 {
539 "class": { "card": true, "smallpad": true },
540 },
541 [
542 h('label',
543 {
544 attrs: { "id": "close-newgame", "for": "modal-newgame" },
545 "class": { "modal-close": true },
546 }
547 ),
548 h('h3',
549 {
550 attrs: { "id": "newGameTxt" },
551 "class": { "section": true },
552 domProps: { innerHTML: "New game" },
553 }
554 ),
555 h('p',
556 {
557 "class": { "section": true },
558 domProps: { innerHTML: "Waiting for opponent..." },
559 }
560 )
561 ]
562 )
563 ]
564 )
565 ];
566 elementArray = elementArray.concat(modalNewgame);
567 const modalFenEdit = [
568 h('input',
569 {
570 attrs: { "id": "modal-fenedit", type: "checkbox" },
571 "class": { "modal": true },
572 }),
573 h('div',
574 {
575 attrs: { "role": "dialog", "aria-labelledby": "titleFenedit" },
576 },
577 [
578 h('div',
579 {
580 "class": { "card": true, "smallpad": true },
581 },
582 [
583 h('label',
584 {
585 attrs: { "id": "close-fenedit", "for": "modal-fenedit" },
586 "class": { "modal-close": true },
587 }
588 ),
589 h('h3',
590 {
591 attrs: { "id": "titleFenedit" },
592 "class": { "section": true },
593 domProps: { innerHTML: "Position + flags (FEN):" },
594 }
595 ),
596 h('input',
597 {
598 attrs: {
599 "id": "input-fen",
600 type: "text",
601 value: VariantRules.GenRandInitFen(),
602 },
603 }
604 ),
605 h('button',
606 {
607 on: { click:
608 () => {
609 const fen = document.getElementById("input-fen").value;
610 document.getElementById("modal-fenedit").checked = false;
611 this.newGame("friend", fen);
612 }
613 },
614 domProps: { innerHTML: "Ok" },
615 }
616 ),
617 h('button',
618 {
619 on: { click:
620 () => {
621 document.getElementById("input-fen").value =
622 VariantRules.GenRandInitFen();
623 }
624 },
625 domProps: { innerHTML: "Random" },
626 }
627 ),
628 ]
629 )
630 ]
631 )
632 ];
633 elementArray = elementArray.concat(modalFenEdit);
634 const modalSettings = [
635 h('input',
636 {
637 attrs: { "id": "modal-settings", type: "checkbox" },
638 "class": { "modal": true },
639 }),
640 h('div',
641 {
642 attrs: { "role": "dialog", "aria-labelledby": "settingsTitle" },
643 },
644 [
645 h('div',
646 {
647 "class": { "card": true, "smallpad": true },
648 },
649 [
650 h('label',
651 {
652 attrs: { "id": "close-settings", "for": "modal-settings" },
653 "class": { "modal-close": true },
654 }
655 ),
656 h('h3',
657 {
658 attrs: { "id": "settingsTitle" },
659 "class": { "section": true },
660 domProps: { innerHTML: "Preferences" },
661 }
662 ),
663 h('fieldset',
664 { },
665 [
666 h('label',
667 {
668 attrs: { for: "nameSetter" },
669 domProps: { innerHTML: "My name is..." },
670 },
671 ),
672 h('input',
673 {
674 attrs: {
675 "id": "nameSetter",
676 type: "text",
677 value: this.myname,
678 },
679 on: { "change": this.setMyname },
680 }
681 ),
682 ]
683 ),
684 h('fieldset',
685 { },
686 [
687 h('label',
688 {
689 attrs: { for: "setHints" },
690 domProps: { innerHTML: "Show hints?" },
691 },
692 ),
693 h('input',
694 {
695 attrs: {
696 "id": "setHints",
697 type: "checkbox",
698 checked: this.hints,
699 },
700 on: { "change": this.toggleHints },
701 }
702 ),
703 ]
704 ),
705 h('fieldset',
706 { },
707 [
708 h('label',
709 {
710 attrs: { for: "selectColor" },
711 domProps: { innerHTML: "Board colors" },
712 },
713 ),
714 h("select",
715 {
716 attrs: { "id": "selectColor" },
717 on: { "change": this.setColor },
718 },
719 [
720 h("option",
721 {
722 domProps: {
723 "value": "lichess",
724 innerHTML: "brown"
725 },
726 attrs: { "selected": this.color=="lichess" },
727 }
728 ),
729 h("option",
730 {
731 domProps: {
732 "value": "chesscom",
733 innerHTML: "green"
734 },
735 attrs: { "selected": this.color=="chesscom" },
736 }
737 ),
738 h("option",
739 {
740 domProps: {
741 "value": "chesstempo",
742 innerHTML: "blue"
743 },
744 attrs: { "selected": this.color=="chesstempo" },
745 }
746 ),
747 ],
748 ),
749 ]
750 ),
751 h('fieldset',
752 { },
753 [
754 h('label',
755 {
756 attrs: { for: "selectSound" },
757 domProps: { innerHTML: "Play sounds?" },
758 },
759 ),
760 h("select",
761 {
762 attrs: { "id": "selectSound" },
763 on: { "change": this.setSound },
764 },
765 [
766 h("option",
767 {
768 domProps: {
769 "value": "0",
770 innerHTML: "None"
771 },
772 attrs: { "selected": this.sound==0 },
773 }
774 ),
775 h("option",
776 {
777 domProps: {
778 "value": "1",
779 innerHTML: "Newgame"
780 },
781 attrs: { "selected": this.sound==1 },
782 }
783 ),
784 h("option",
785 {
786 domProps: {
787 "value": "2",
788 innerHTML: "All"
789 },
790 attrs: { "selected": this.sound==2 },
791 }
792 ),
793 ],
794 ),
795 ]
796 ),
797 ]
798 )
799 ]
800 )
801 ];
802 elementArray = elementArray.concat(modalSettings);
803 let chatEltsArray =
804 [
805 h('label',
806 {
807 attrs: { "id": "close-chat", "for": "modal-chat" },
808 "class": { "modal-close": true },
809 }
810 ),
811 h('h3',
812 {
813 attrs: { "id": "titleChat" },
814 "class": { "section": true },
815 domProps: { innerHTML: "Chat with " + this.oppName },
816 }
817 )
818 ];
819 for (let chat of this.chats)
820 {
821 chatEltsArray.push(
822 h('p',
823 {
824 "class": {
825 "my-chatmsg": chat.author==this.myid,
826 "opp-chatmsg": chat.author==this.oppid,
827 },
828 domProps: { innerHTML: chat.msg }
829 }
830 )
831 );
832 }
833 chatEltsArray = chatEltsArray.concat([
834 h('input',
835 {
836 attrs: {
837 "id": "input-chat",
838 type: "text",
839 placeholder: "Type here",
840 },
841 on: { keyup: this.trySendChat }, //if key is 'enter'
842 }
843 ),
844 h('button',
845 {
846 on: { click: this.sendChat },
847 domProps: { innerHTML: "Send" },
848 }
849 )
850 ]);
851 const modalChat = [
852 h('input',
853 {
854 attrs: { "id": "modal-chat", type: "checkbox" },
855 "class": { "modal": true },
856 }),
857 h('div',
858 {
859 attrs: { "role": "dialog", "aria-labelledby": "titleChat" },
860 },
861 [
862 h('div',
863 {
864 "class": { "card": true, "smallpad": true },
865 },
866 chatEltsArray
867 )
868 ]
869 )
870 ];
871 elementArray = elementArray.concat(modalChat);
872 const actions = h('div',
873 {
874 attrs: { "id": "actions" },
875 'class': { 'text-center': true },
876 },
877 actionArray
878 );
879 elementArray.push(actions);
880 if (this.score != "*" && this.pgnTxt.length > 0)
881 {
882 elementArray.push(
883 h('div',
884 {
885 attrs: { id: "pgn-div" },
886 "class": { "section-content": true },
887 },
888 [
889 h('a',
890 {
891 attrs: {
892 id: "download",
893 href: "#",
894 }
895 }
896 ),
897 h('p',
898 {
899 attrs: { id: "pgn-game" },
900 domProps: { innerHTML: this.pgnTxt }
901 }
902 ),
903 h('button',
904 {
905 attrs: { "id": "downloadBtn" },
906 on: { click: this.download },
907 domProps: { innerHTML: "Download game" },
908 }
909 ),
910 ]
911 )
912 );
913 }
914 else if (this.mode != "idle")
915 {
916 if (this.mode == "problem")
917 {
918 // Show problem solution (on click)
919 elementArray.push(
920 h('div',
921 {
922 attrs: { id: "solution-div" },
923 "class": { "section-content": true },
924 },
925 [
926 h('h3',
927 {
928 domProps: { innerHTML: "Show solution" },
929 on: { click: this.toggleShowSolution },
930 }
931 ),
932 h('p',
933 {
934 attrs: { id: "problem-solution" },
935 domProps: { innerHTML: this.problem.solution }
936 }
937 )
938 ]
939 )
940 );
941 }
942 // Show current FEN
943 elementArray.push(
944 h('div',
945 {
946 attrs: { id: "fen-div" },
947 "class": { "section-content": true },
948 },
949 [
950 h('p',
951 {
952 attrs: { id: "fen-string" },
953 domProps: { innerHTML: this.vr.getBaseFen() }
954 }
955 )
956 ]
957 )
958 );
959 }
960 return h(
961 'div',
962 {
963 'class': {
964 "col-sm-12":true,
965 "col-md-8":true,
966 "col-md-offset-2":true,
967 "col-lg-6":true,
968 "col-lg-offset-3":true,
969 },
970 // NOTE: click = mousedown + mouseup
971 on: {
972 mousedown: this.mousedown,
973 mousemove: this.mousemove,
974 mouseup: this.mouseup,
975 touchstart: this.mousedown,
976 touchmove: this.mousemove,
977 touchend: this.mouseup,
978 },
979 },
980 elementArray
981 );
982 },
983 computed: {
984 endgameMessage: function() {
985 let eogMessage = "Unfinished";
986 switch (this.score)
987 {
988 case "1-0":
989 eogMessage = "White win";
990 break;
991 case "0-1":
992 eogMessage = "Black win";
993 break;
994 case "1/2":
995 eogMessage = "Draw";
996 break;
997 }
998 return eogMessage;
999 },
1000 },
1001 created: function() {
1002 const url = socketUrl;
1003 const humanContinuation = (localStorage.getItem("variant") === variant);
1004 const computerContinuation = (localStorage.getItem("comp-variant") === variant);
1005 this.myid = (humanContinuation ? localStorage.getItem("myid") : getRandString());
1006 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
1007 const socketOpenListener = () => {
1008 if (humanContinuation) //game VS human has priority
1009 this.continueGame("human");
1010 else if (computerContinuation)
1011 this.continueGame("computer");
1012 };
1013 const socketMessageListener = msg => {
1014 const data = JSON.parse(msg.data);
1015 const L = (!!this.vr ? this.vr.moves.length : 0);
1016 switch (data.code)
1017 {
1018 case "oppname":
1019 // Receive opponent's name
1020 this.oppName = data.name;
1021 break;
1022 case "newchat":
1023 // Receive new chat
1024 this.chats.push({msg:data.msg, author:this.oppid});
1025 break;
1026 case "duplicate":
1027 // We opened another tab on the same game
1028 this.mode = "idle";
1029 this.vr = null;
1030 alert("Already playing a game in this variant on another tab!");
1031 break;
1032 case "newgame": //opponent found
1033 // oppid: opponent socket ID
1034 this.newGame("human", data.fen, data.color, data.oppid);
1035 break;
1036 case "newmove": //..he played!
1037 this.play(data.move, "animate");
1038 break;
1039 case "pong": //received if we sent a ping (game still alive on our side)
1040 if (this.gameId != data.gameId)
1041 break; //games IDs don't match: definitely over...
1042 this.oppConnected = true;
1043 // Send our "last state" informations to opponent
1044 this.conn.send(JSON.stringify({
1045 code: "lastate",
1046 oppid: this.oppid,
1047 gameId: this.gameId,
1048 lastMove: (L>0?this.vr.moves[L-1]:undefined),
1049 movesCount: L,
1050 }));
1051 break;
1052 case "lastate": //got opponent infos about last move
1053 if (this.gameId != data.gameId)
1054 break; //games IDs don't match: nothing we can do...
1055 // OK, opponent still in game (which might be over)
1056 if (this.mode != "human")
1057 {
1058 // We finished the game (any result possible)
1059 this.conn.send(JSON.stringify({
1060 code: "lastate",
1061 oppid: data.oppid,
1062 gameId: this.gameId,
1063 score: this.score,
1064 }));
1065 }
1066 else if (!!data.score) //opponent finished the game
1067 this.endGame(data.score);
1068 else if (data.movesCount < L)
1069 {
1070 // We must tell last move to opponent
1071 this.conn.send(JSON.stringify({
1072 code: "lastate",
1073 oppid: this.oppid,
1074 lastMove: this.vr.moves[L-1],
1075 movesCount: L,
1076 }));
1077 }
1078 else if (data.movesCount > L) //just got last move from him
1079 this.play(data.lastMove, "animate");
1080 break;
1081 case "resign": //..you won!
1082 this.endGame(this.mycolor=="w"?"1-0":"0-1");
1083 break;
1084 // TODO: also use (dis)connect info to count online players?
1085 case "connect":
1086 case "disconnect":
1087 if (["human","chat"].includes(this.mode) && this.oppid == data.id)
1088 this.oppConnected = (data.code == "connect");
1089 if (this.oppConnected && this.mode == "chat")
1090 {
1091 // Send our name to the opponent, in case of he hasn't it
1092 this.conn.send(JSON.stringify({
1093 code:"myname", name:this.myname, oppid: this.oppid}));
1094 }
1095 break;
1096 }
1097 };
1098 const socketCloseListener = () => {
1099 this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
1100 this.conn.addEventListener('open', socketOpenListener);
1101 this.conn.addEventListener('message', socketMessageListener);
1102 this.conn.addEventListener('close', socketCloseListener);
1103 };
1104 this.conn.onopen = socketOpenListener;
1105 this.conn.onmessage = socketMessageListener;
1106 this.conn.onclose = socketCloseListener;
1107 // Listen to keyboard left/right to navigate in game
1108 document.onkeydown = event => {
1109 if (["idle","chat"].includes(this.mode) &&
1110 !!this.vr && this.vr.moves.length > 0 && [37,39].includes(event.keyCode))
1111 {
1112 event.preventDefault();
1113 if (event.keyCode == 37) //Back
1114 this.undo();
1115 else //Forward (39)
1116 this.play();
1117 }
1118 };
1119 // Computer moves web worker logic:
1120 this.compWorker.postMessage(["scripts",variant]);
1121 const self = this;
1122 this.compWorker.onmessage = function(e) {
1123 let compMove = e.data;
1124 compMove.computer = true; //TODO: imperfect attempt to avoid ghost move
1125 // (first move) HACK: small delay to avoid selecting elements
1126 // before they appear on page:
1127 const delay = Math.max(500-(Date.now()-self.timeStart), 0);
1128 setTimeout(() => {
1129 if (self.mode == "computer") //warning: mode could have changed!
1130 self.play(compMove, "animate")
1131 }, delay);
1132 }
1133 },
1134 methods: {
1135 setMyname: function(e) {
1136 this.myname = e.target.value;
1137 setCookie("username",this.myname);
1138 },
1139 trySendChat: function(e) {
1140 if (e.keyCode == 13) //'enter' key
1141 this.sendChat();
1142 },
1143 sendChat: function() {
1144 let chatInput = document.getElementById("input-chat");
1145 const chatTxt = chatInput.value;
1146 chatInput.value = "";
1147 this.chats.push({msg:chatTxt, author:this.myid});
1148 this.conn.send(JSON.stringify({
1149 code:"newchat", oppid: this.oppid, msg: chatTxt}));
1150 },
1151 toggleShowSolution: function() {
1152 let problemSolution = document.getElementById("problem-solution");
1153 problemSolution.style.display =
1154 !problemSolution.style.display || problemSolution.style.display == "none"
1155 ? "block"
1156 : "none";
1157 },
1158 download: function() {
1159 let content = document.getElementById("pgn-game").innerHTML;
1160 content = content.replace(/<br>/g, "\n");
1161 // Prepare and trigger download link
1162 let downloadAnchor = document.getElementById("download");
1163 downloadAnchor.setAttribute("download", "game.pgn");
1164 downloadAnchor.href = "data:text/plain;charset=utf-8," +
1165 encodeURIComponent(content);
1166 downloadAnchor.click();
1167 },
1168 showScoreMsg: function() {
1169 let modalBox = document.getElementById("modal-eog");
1170 modalBox.checked = true;
1171 setTimeout(() => { modalBox.checked = false; }, 2000);
1172 },
1173 endGame: function(score) {
1174 this.score = score;
1175 if (["human","computer"].includes(this.mode))
1176 {
1177 const prefix = (this.mode=="computer" ? "comp-" : "");
1178 localStorage.setItem(prefix+"score", score);
1179 }
1180 this.showScoreMsg();
1181 // Variants may have special PGN structure (so next function isn't defined here)
1182 this.pgnTxt = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode);
1183 if (this.mode == "human" && this.oppConnected)
1184 {
1185 // Send our nickname to opponent
1186 this.conn.send(JSON.stringify({
1187 code:"myname", name:this.myname, oppid:this.oppid}));
1188 }
1189 this.mode = (this.mode=="human" ? "chat" : "idle");
1190 this.cursor = this.vr.moves.length; //to navigate in finished game
1191 },
1192 setStorage: function() {
1193 if (this.mode=="human")
1194 {
1195 localStorage.setItem("myid", this.myid);
1196 localStorage.setItem("oppid", this.oppid);
1197 localStorage.setItem("gameId", this.gameId);
1198 }
1199 // 'prefix' = "comp-" to resume games vs. computer
1200 const prefix = (this.mode=="computer" ? "comp-" : "");
1201 localStorage.setItem(prefix+"variant", variant);
1202 localStorage.setItem(prefix+"mycolor", this.mycolor);
1203 localStorage.setItem(prefix+"fenStart", this.fenStart);
1204 localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
1205 localStorage.setItem(prefix+"fen", this.vr.getFen());
1206 localStorage.setItem(prefix+"score", "*");
1207 },
1208 updateStorage: function() {
1209 const prefix = (this.mode=="computer" ? "comp-" : "");
1210 localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
1211 localStorage.setItem(prefix+"fen", this.vr.getFen());
1212 if (this.score != "*")
1213 localStorage.setItem(prefix+"score", this.score);
1214 },
1215 // "computer mode" clearing is done through the menu
1216 clearStorage: function() {
1217 if (["human","chat"].includes(this.mode))
1218 {
1219 delete localStorage["myid"];
1220 delete localStorage["oppid"];
1221 delete localStorage["gameId"];
1222 }
1223 const prefix = (this.mode=="computer" ? "comp-" : "");
1224 delete localStorage[prefix+"variant"];
1225 delete localStorage[prefix+"mycolor"];
1226 delete localStorage[prefix+"fenStart"];
1227 delete localStorage[prefix+"moves"];
1228 delete localStorage[prefix+"fen"];
1229 delete localStorage[prefix+"score"];
1230 },
1231 // HACK because mini-css tooltips are persistent after click...
1232 getRidOfTooltip: function(elt) {
1233 elt.style.visibility = "hidden";
1234 setTimeout(() => { elt.style.visibility="visible"; }, 100);
1235 },
1236 startChat: function(e) {
1237 this.getRidOfTooltip(e.currentTarget);
1238 document.getElementById("modal-chat").checked = true;
1239 },
1240 clearComputerGame: function(e) {
1241 this.getRidOfTooltip(e.currentTarget);
1242 this.clearStorage(); //this.mode=="computer" (already checked)
1243 location.reload(); //to see clearing effects
1244 },
1245 showSettings: function(e) {
1246 this.getRidOfTooltip(e.currentTarget);
1247 document.getElementById("modal-settings").checked = true;
1248 },
1249 toggleHints: function() {
1250 this.hints = !this.hints;
1251 setCookie("hints", this.hints ? "1" : "0");
1252 },
1253 setColor: function(e) {
1254 this.color = e.target.options[e.target.selectedIndex].value;
1255 setCookie("color", this.color);
1256 },
1257 setSound: function(e) {
1258 this.sound = parseInt(e.target.options[e.target.selectedIndex].value);
1259 setCookie("sound", this.sound);
1260 },
1261 clickGameSeek: function(e) {
1262 this.getRidOfTooltip(e.currentTarget);
1263 if (this.mode == "human")
1264 return; //no newgame while playing
1265 if (this.seek)
1266 {
1267 this.conn.send(JSON.stringify({code:"cancelnewgame"}));
1268 this.seek = false;
1269 }
1270 else
1271 this.newGame("human");
1272 },
1273 clickComputerGame: function(e) {
1274 this.getRidOfTooltip(e.currentTarget);
1275 this.newGame("computer");
1276 },
1277 clickFriendGame: function(e) {
1278 this.getRidOfTooltip(e.currentTarget);
1279 document.getElementById("modal-fenedit").checked = true;
1280 },
1281 resign: function(e) {
1282 this.getRidOfTooltip(e.currentTarget);
1283 if (this.mode == "human" && this.oppConnected)
1284 {
1285 try {
1286 this.conn.send(JSON.stringify({code: "resign", oppid: this.oppid}));
1287 } catch (INVALID_STATE_ERR) {
1288 return; //socket is not ready (and not yet reconnected)
1289 }
1290 }
1291 this.endGame(this.mycolor=="w"?"0-1":"1-0");
1292 },
1293 newGame: function(mode, fenInit, color, oppId) {
1294 let fen = fenInit || VariantRules.GenRandInitFen();
1295 console.log(fen); //DEBUG
1296 if (mode=="human" && !oppId)
1297 {
1298 const storageVariant = localStorage.getItem("variant");
1299 if (!!storageVariant && storageVariant !== variant)
1300 return alert("Finish your " + storageVariant + " game first!");
1301 // Send game request and wait..
1302 try {
1303 this.conn.send(JSON.stringify({code:"newgame", fen:fen}));
1304 } catch (INVALID_STATE_ERR) {
1305 return; //nothing achieved
1306 }
1307 this.seek = true;
1308 let modalBox = document.getElementById("modal-newgame");
1309 modalBox.checked = true;
1310 setTimeout(() => { modalBox.checked = false; }, 2000);
1311 return;
1312 }
1313 if (["human","chat"].includes(this.mode))
1314 {
1315 // Start a new game vs. another human (or...) => forget about current one
1316 this.clearStorage();
1317 }
1318 if (mode == "computer")
1319 {
1320 const storageVariant = localStorage.getItem("comp-variant");
1321 if (!!storageVariant)
1322 {
1323 const score = localStorage.getItem("comp-score");
1324 if (storageVariant !== variant && score == "*")
1325 {
1326 if (!confirm("Unfinished " + storageVariant +
1327 " computer game will be erased"))
1328 {
1329 return;
1330 }
1331 }
1332 else if (score == "*")
1333 return this.continueGame("computer");
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 })