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