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