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