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