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