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