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