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