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