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