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