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