1 // Game logic on a variant page
2 Vue
.component('my-game', {
5 vr: null, //object to check moves, store them, FEN..
7 possibleMoves: [], //filled after each valid click/dragstart
8 choices: [], //promotion pieces, or checkered captures... (as moves)
9 start: {}, //pixels coordinates + id of starting square (click or drag)
10 selectedPiece: null, //moving piece (or clicked piece)
11 conn: null, //socket connection
12 score: "*", //'*' means 'unfinished'
13 mode: "idle", //human, friend, computer or idle (when not playing)
14 oppid: "", //opponent ID in case of HH game
20 expert: (getCookie("expert") === "1" ? true : false),
21 gameId: "", //used to limit computer moves' time
25 const [sizeX
,sizeY
] = VariantRules
.size
;
26 const smallScreen
= (screen
.width
<= 420);
27 // Precompute hints squares to facilitate rendering
28 let hintSquares
= doubleArray(sizeX
, sizeY
, false);
29 this.possibleMoves
.forEach(m
=> { hintSquares
[m
.end
.x
][m
.end
.y
] = true; });
30 // Also precompute in-check squares
31 let incheckSq
= doubleArray(sizeX
, sizeY
, false);
32 this.incheck
.forEach(sq
=> { incheckSq
[sq
[0]][sq
[1]] = true; });
33 let elementArray
= [];
38 on: { click: this.clickGameSeek
},
39 attrs: { "aria-label": 'New online game' },
42 "bottom": true, //display below
44 "playing": this.mode
== "human",
48 [h('i', { 'class': { "material-icons": true } }, "accessibility")])
50 if (["idle","computer"].includes(this.mode
))
55 on: { click: this.clickComputerGame
},
56 attrs: { "aria-label": 'New game VS computer' },
60 "playing": this.mode
== "computer",
64 [h('i', { 'class': { "material-icons": true } }, "computer")])
67 if (["idle","friend"].includes(this.mode
))
72 on: { click: this.clickFriendGame
},
73 attrs: { "aria-label": 'New IRL game' },
77 "playing": this.mode
== "friend",
81 [h('i', { 'class': { "material-icons": true } }, "people")])
86 const square00
= document
.getElementById("sq-0-0");
87 const squareWidth
= !!square00
88 ? parseFloat(window
.getComputedStyle(square00
).width
.slice(0,-2))
90 const indicWidth
= (squareWidth
>0 ? squareWidth
/2 : 20);
91 if (this.mode
== "human")
93 let connectedIndic
= h(
99 "connected": this.oppConnected
,
100 "disconnected": !this.oppConnected
,
103 "width": indicWidth
+ "px",
104 "height": indicWidth
+ "px",
108 elementArray
.push(connectedIndic
);
114 "topindicator": true,
116 "white-turn": this.vr
.turn
=="w",
117 "black-turn": this.vr
.turn
=="b",
120 "width": indicWidth
+ "px",
121 "height": indicWidth
+ "px",
125 elementArray
.push(turnIndic
);
126 let expertSwitch
= h(
129 on: { click: this.toggleExpertMode
},
130 attrs: { "aria-label": 'Toggle expert mode' },
133 "topindicator": true,
135 "expert-switch": true,
136 "expert-mode": this.expert
,
137 "small": smallScreen
,
140 [h('i', { 'class': { "material-icons": true } }, "visibility_off")]
142 elementArray
.push(expertSwitch
);
143 let choices
= h('div',
145 attrs: { "id": "choices" },
146 'class': { 'row': true },
148 "display": this.choices
.length
>0?"block":"none",
149 "top": "-" + ((sizeY
/2)*squareWidth+squareWidth/2) + "px",
150 "width": (this.choices
.length
* squareWidth
) + "px",
151 "height": squareWidth
+ "px",
154 this.choices
.map( m
=> { //a "choice" is a move
159 ['board'+sizeY
]: true,
162 'width': (100/this.choices
.length
) + "%",
163 'padding-bottom': (100/this.choices
.length
) + "%",
168 attrs: { "src": '/images/pieces/' +
169 VariantRules
.getPpath(m
.appear
[0].c
+m
.appear
[0].p
) + '.svg' },
170 'class': { 'choice-piece': true },
171 on: { "click": e
=> { this.play(m
); this.choices
=[]; } },
177 // Create board element (+ reserves if needed by variant or mode)
178 let gameDiv
= h('div',
180 'class': { 'game': true },
182 [_
.range(sizeX
).map(i
=> {
183 let ci
= this.mycolor
=='w' ? i : sizeX
-i
-1;
190 style: { 'opacity': this.choices
.length
>0?"0.5":"1" },
192 _
.range(sizeY
).map(j
=> {
193 let cj
= this.mycolor
=='w' ? j : sizeY
-j
-1;
195 if (this.vr
.board
[ci
][cj
] != VariantRules
.EMPTY
)
203 'ghost': !!this.selectedPiece
204 && this.selectedPiece
.parentNode
.id
== "sq-"+ci
+"-"+cj
,
207 src: "/images/pieces/" +
208 VariantRules
.getPpath(this.vr
.board
[ci
][cj
]) + ".svg",
214 if (!this.expert
&& hintSquares
[ci
][cj
])
224 src: "/images/mark.svg",
230 const lm
= this.vr
.lastMove
;
231 const showLight
= !this.expert
&&
232 (this.mode
!="idle" || this.cursor
==this.vr
.moves
.length
);
238 ['board'+sizeY
]: true,
239 'light-square': (i
+j
)%2==0,
240 'dark-square': (i
+j
)%2==1,
241 'highlight': showLight
&& !!lm
&& _
.isMatch(lm
.end
, {x:ci
,y:cj
}),
242 'incheck': showLight
&& incheckSq
[ci
][cj
],
245 id: this.getSquareId({x:ci
,y:cj
}),
254 if (this.mode
!= "idle")
259 on: { click: this.resign
},
260 attrs: { "aria-label": 'Resign' },
264 "small": smallScreen
,
267 [h('i', { 'class': { "material-icons": true } }, "flag")])
270 else if (this.vr
.moves
.length
> 0)
272 // A game finished, and another is not started yet: allow navigation
273 actionArray
= actionArray
.concat([
276 on: { click: e
=> this.undo() },
277 attrs: { "aria-label": 'Undo' },
279 "small": smallScreen
,
283 [h('i', { 'class': { "material-icons": true } }, "fast_rewind")]),
286 on: { click: e
=> this.play() },
287 attrs: { "aria-label": 'Play' },
288 "class": { "small": smallScreen
},
290 [h('i', { 'class': { "material-icons": true } }, "fast_forward")]),
294 if (this.mode
== "friend")
296 actionArray
= actionArray
.concat(
300 on: { click: this.undoInGame
},
301 attrs: { "aria-label": 'Undo' },
303 "small": smallScreen
,
307 [h('i', { 'class': { "material-icons": true } }, "undo")]
311 on: { click: () => { this.mycolor
= this.vr
.getOppCol(this.mycolor
) } },
312 attrs: { "aria-label": 'Flip' },
313 "class": { "small": smallScreen
},
315 [h('i', { 'class': { "material-icons": true } }, "cached")]
319 elementArray
.push(gameDiv
);
320 if (!!this.vr
.reserve
)
322 const shiftIdx
= (this.mycolor
=="w" ? 0 : 1);
323 let myReservePiecesArray
= [];
324 for (let i
=0; i
<VariantRules
.RESERVE_PIECES
.length
; i
++)
326 myReservePiecesArray
.push(h('div',
328 'class': {'board':true, ['board'+sizeY
]:true},
329 attrs: { id: this.getSquareId({x:sizeX
+shiftIdx
,y:i
}) }
334 'class': {"piece":true},
336 "src": "/images/pieces/" +
337 this.vr
.getReservePpath(this.mycolor
,i
) + ".svg",
341 {"class": { "reserve-count": true } },
342 [ this.vr
.reserve
[this.mycolor
][VariantRules
.RESERVE_PIECES
[i
]] ]
346 let oppReservePiecesArray
= [];
347 const oppCol
= this.vr
.getOppCol(this.mycolor
);
348 for (let i
=0; i
<VariantRules
.RESERVE_PIECES
.length
; i
++)
350 oppReservePiecesArray
.push(h('div',
352 'class': {'board':true, ['board'+sizeY
]:true},
353 attrs: { id: this.getSquareId({x:sizeX
+(1-shiftIdx
),y:i
}) }
358 'class': {"piece":true},
360 "src": "/images/pieces/" +
361 this.vr
.getReservePpath(oppCol
,i
) + ".svg",
365 {"class": { "reserve-count": true } },
366 [ this.vr
.reserve
[oppCol
][VariantRules
.RESERVE_PIECES
[i
]] ]
370 let reserves
= h('div',
382 "reserve-row-1": true,
388 { 'class': { 'row': true }},
389 oppReservePiecesArray
393 elementArray
.push(reserves
);
395 const eogMessage
= this.getEndgameMessage(this.score
);
399 attrs: { "id": "modal-eog", type: "checkbox" },
400 "class": { "modal": true },
404 attrs: { "role": "dialog", "aria-labelledby": "modal-eog" },
409 "class": { "card": true, "smallpad": true },
414 attrs: { "for": "modal-eog" },
415 "class": { "modal-close": true },
420 "class": { "section": true },
421 domProps: { innerHTML: eogMessage
},
429 elementArray
= elementArray
.concat(modalEog
);
431 const modalNewgame
= [
434 attrs: { "id": "modal-newgame", type: "checkbox" },
435 "class": { "modal": true },
439 attrs: { "role": "dialog", "aria-labelledby": "modal-newgame" },
444 "class": { "card": true, "smallpad": true },
449 attrs: { "id": "close-newgame", "for": "modal-newgame" },
450 "class": { "modal-close": true },
455 "class": { "section": true },
456 domProps: { innerHTML: "New game" },
461 "class": { "section": true },
462 domProps: { innerHTML: "Waiting for opponent..." },
470 elementArray
= elementArray
.concat(modalNewgame
);
471 const modalFenEdit
= [
474 attrs: { "id": "modal-fenedit", type: "checkbox" },
475 "class": { "modal": true },
479 attrs: { "role": "dialog", "aria-labelledby": "modal-fenedit" },
484 "class": { "card": true, "smallpad": true },
489 attrs: { "id": "close-fenedit", "for": "modal-fenedit" },
490 "class": { "modal-close": true },
495 "class": { "section": true },
496 domProps: { innerHTML: "Position + flags (FEN):" },
504 value: VariantRules
.GenRandInitFen(),
512 const fen
= document
.getElementById("input-fen").value
;
513 document
.getElementById("modal-fenedit").checked
= false;
514 this.newGame("friend", fen
);
517 domProps: { innerHTML: "Ok" },
524 document
.getElementById("input-fen").value
=
525 VariantRules
.GenRandInitFen();
528 domProps: { innerHTML: "Random" },
536 elementArray
= elementArray
.concat(modalFenEdit
);
537 const actions
= h('div',
539 attrs: { "id": "actions" },
540 'class': { 'text-center': true },
544 elementArray
.push(actions
);
545 if (this.score
!= "*")
549 { attrs: { id: "pgn-div" } },
561 attrs: { id: "pgn-game" },
562 on: { click: this.download
},
563 domProps: { innerHTML: this.pgnTxt
}
570 else if (this.mode
!= "idle")
575 { attrs: { id: "fen-div" } },
579 attrs: { id: "fen-string" },
580 domProps: { innerHTML: this.vr
.getFen() }
593 "col-md-offset-2":true,
595 "col-lg-offset-3":true,
597 // NOTE: click = mousedown + mouseup
599 mousedown: this.mousedown
,
600 mousemove: this.mousemove
,
601 mouseup: this.mouseup
,
602 touchstart: this.mousedown
,
603 touchmove: this.mousemove
,
604 touchend: this.mouseup
,
610 created: function() {
611 const url
= socketUrl
;
612 const continuation
= (localStorage
.getItem("variant") === variant
);
613 this.myid
= continuation
? localStorage
.getItem("myid") : getRandString();
616 // HACK: play a small silent sound to allow "new game" sound later
617 // if tab not focused (TODO: does it really work ?!)
618 new Audio("/sounds/silent.mp3").play().then(() => {}).catch(err
=> {});
620 this.conn
= new WebSocket(url
+ "/?sid=" + this.myid
+ "&page=" + variant
);
621 const socketOpenListener
= () => {
624 const fen
= localStorage
.getItem("fen");
625 const mycolor
= localStorage
.getItem("mycolor");
626 const oppid
= localStorage
.getItem("oppid");
627 const moves
= JSON
.parse(localStorage
.getItem("moves"));
628 this.newGame("human", fen
, mycolor
, oppid
, moves
, true);
629 // Send ping to server (answer pong if opponent is connected)
630 this.conn
.send(JSON
.stringify({code:"ping",oppid:this.oppid
}));
632 else if (localStorage
.getItem("newgame") === variant
)
634 // New game request has been cancelled on disconnect
635 this.newGame("human", undefined, undefined, undefined, undefined, "reconnect");
638 const socketMessageListener
= msg
=> {
639 const data
= JSON
.parse(msg
.data
);
642 case "newgame": //opponent found
643 // oppid: opponent socket ID
644 this.newGame("human", data
.fen
, data
.color
, data
.oppid
);
646 case "newmove": //..he played!
647 this.play(data
.move, "animate");
649 case "pong": //received if we sent a ping (game still alive on our side)
650 this.oppConnected
= true;
651 const L
= this.vr
.moves
.length
;
652 // Send our "last state" informations to opponent
653 this.conn
.send(JSON
.stringify({
656 lastMove:L
>0?this.vr
.moves
[L
-1]:undefined,
660 case "lastate": //got opponent infos about last move (we might have resigned)
661 if (this.mode
!="human" || this.oppid
!=data
.oppid
)
664 this.conn
.send(JSON
.stringify({
671 else if (data
.movesCount
< 0)
674 this.endGame(this.mycolor
=="w"?"1-0":"0-1");
676 else if (data
.movesCount
< this.vr
.moves
.length
)
678 // We must tell last move to opponent
679 const L
= this.vr
.moves
.length
;
680 this.conn
.send(JSON
.stringify({
683 lastMove:this.vr
.moves
[L
-1],
687 else if (data
.movesCount
> this.vr
.moves
.length
) //just got last move from him
688 this.play(data
.lastMove
, "animate");
690 case "resign": //..you won!
691 this.endGame(this.mycolor
=="w"?"1-0":"0-1");
693 // TODO: also use (dis)connect info to count online players?
696 if (this.mode
== "human" && this.oppid
== data
.id
)
697 this.oppConnected
= (data
.code
== "connect");
701 const socketCloseListener
= () => {
702 this.conn
= new WebSocket(url
+ "/?sid=" + this.myid
+ "&page=" + variant
);
703 this.conn
.addEventListener('open', socketOpenListener
);
704 this.conn
.addEventListener('message', socketMessageListener
);
705 this.conn
.addEventListener('close', socketCloseListener
);
707 this.conn
.onopen
= socketOpenListener
;
708 this.conn
.onmessage
= socketMessageListener
;
709 this.conn
.onclose
= socketCloseListener
;
710 // Listen to keyboard left/right to navigate in game
711 document
.onkeydown
= event
=> {
712 if (this.mode
== "idle" && !!this.vr
&& this.vr
.moves
.length
> 0
713 && [37,39].includes(event
.keyCode
))
715 event
.preventDefault();
716 if (event
.keyCode
== 37) //Back
724 download: function() {
725 let content
= document
.getElementById("pgn-game").innerHTML
;
726 content
= content
.replace(/<br>/g, "\n");
727 // Prepare and trigger download link
728 let downloadAnchor
= document
.getElementById("download");
729 downloadAnchor
.setAttribute("download", "game.pgn");
730 downloadAnchor
.href
= "data:text/plain;charset=utf-8," +
731 encodeURIComponent(content
);
732 downloadAnchor
.click();
734 endGame: function(score
) {
736 let modalBox
= document
.getElementById("modal-eog");
737 modalBox
.checked
= true;
738 // Variants may have special PGN structure (so next function isn't defined here)
739 this.pgnTxt
= this.vr
.getPGN(this.mycolor
, this.score
, this.fenStart
, this.mode
);
740 setTimeout(() => { modalBox
.checked
= false; }, 2000);
741 if (this.mode
== "human")
744 this.cursor
= this.vr
.moves
.length
; //to navigate in finished game
747 getEndgameMessage: function(score
) {
748 let eogMessage
= "Unfinished";
752 eogMessage
= "White win";
755 eogMessage
= "Black win";
763 setStorage: function() {
764 localStorage
.setItem("myid", this.myid
);
765 localStorage
.setItem("variant", variant
);
766 localStorage
.setItem("mycolor", this.mycolor
);
767 localStorage
.setItem("oppid", this.oppid
);
768 localStorage
.setItem("fenStart", this.fenStart
);
769 localStorage
.setItem("moves", JSON
.stringify(this.vr
.moves
));
770 localStorage
.setItem("fen", this.vr
.getFen());
772 updateStorage: function() {
773 localStorage
.setItem("moves", JSON
.stringify(this.vr
.moves
));
774 localStorage
.setItem("fen", this.vr
.getFen());
776 clearStorage: function() {
777 delete localStorage
["variant"];
778 delete localStorage
["myid"];
779 delete localStorage
["mycolor"];
780 delete localStorage
["oppid"];
781 delete localStorage
["fenStart"];
782 delete localStorage
["fen"];
783 delete localStorage
["moves"];
785 // HACK because mini-css tooltips are persistent after click...
786 getRidOfTooltip: function(elt
) {
787 elt
.style
.visibility
= "hidden";
788 setTimeout(() => { elt
.style
.visibility
="visible"; }, 100);
790 clickGameSeek: function(e
) {
791 this.getRidOfTooltip(e
.currentTarget
);
792 if (this.mode
== "human")
793 return; //no newgame while playing
796 this.conn
.send(JSON
.stringify({code:"cancelnewgame"}));
797 delete localStorage
["newgame"]; //cancel game seek
801 this.newGame("human");
803 clickComputerGame: function(e
) {
804 this.getRidOfTooltip(e
.currentTarget
);
805 if (this.mode
== "human")
806 return; //no newgame while playing
807 this.newGame("computer");
809 clickFriendGame: function(e
) {
810 this.getRidOfTooltip(e
.currentTarget
);
811 document
.getElementById("modal-fenedit").checked
= true;
813 toggleExpertMode: function(e
) {
814 this.getRidOfTooltip(e
.currentTarget
);
815 this.expert
= !this.expert
;
816 setCookie("expert", this.expert
? "1" : "0");
818 resign: function(e
) {
819 this.getRidOfTooltip(e
.currentTarget
);
820 if (this.mode
== "human" && this.oppConnected
)
823 this.conn
.send(JSON
.stringify({code: "resign", oppid: this.oppid
}));
824 } catch (INVALID_STATE_ERR
) {
825 return; //socket is not ready (and not yet reconnected)
828 this.endGame(this.mycolor
=="w"?"0-1":"1-0");
830 newGame: function(mode
, fenInit
, color
, oppId
, moves
, continuation
) {
831 const fen
= fenInit
|| VariantRules
.GenRandInitFen();
832 console
.log(fen
); //DEBUG
833 if (mode
=="human" && !oppId
)
835 const storageVariant
= localStorage
.getItem("variant");
836 if (!!storageVariant
&& storageVariant
!== variant
)
838 alert("Finish your " + storageVariant
+ " game first!");
841 // Send game request and wait..
842 localStorage
["newgame"] = variant
;
844 this.clearStorage(); //in case of
846 this.conn
.send(JSON
.stringify({code:"newgame", fen:fen
}));
847 } catch (INVALID_STATE_ERR
) {
848 return; //nothing achieved
850 if (continuation
!== "reconnect") //TODO: bad HACK...
852 let modalBox
= document
.getElementById("modal-newgame");
853 modalBox
.checked
= true;
854 setTimeout(() => { modalBox
.checked
= false; }, 2000);
858 this.gameId
= getRandString();
859 this.vr
= new VariantRules(fen
, moves
|| []);
861 this.pgnTxt
= ""; //redundant with this.score = "*", but cleaner
863 this.incheck
= []; //in case of
864 this.fenStart
= (continuation
? localStorage
.getItem("fenStart") : fen
);
870 // Not playing sound on game continuation:
871 new Audio("/sounds/newgame.mp3").play().then(() => {}).catch(err
=> {});
872 document
.getElementById("modal-newgame").checked
= false;
875 this.oppConnected
= true;
876 this.mycolor
= color
;
878 if (!!moves
&& moves
.length
> 0) //imply continuation
880 const lastMove
= moves
[moves
.length
-1];
881 this.vr
.undo(lastMove
);
882 this.incheck
= this.vr
.getCheckSquares(lastMove
);
883 this.vr
.play(lastMove
, "ingame");
885 delete localStorage
["newgame"];
886 this.setStorage(); //in case of interruptions
888 else if (mode
== "computer")
890 this.mycolor
= Math
.random() < 0.5 ? 'w' : 'b';
891 if (this.mycolor
== 'b')
892 setTimeout(this.playComputerMove
, 500);
894 //else: against a (IRL) friend: nothing more to do
896 playComputerMove: function() {
897 const timeStart
= Date
.now();
898 // We use moves' count to know if search finished:
899 const nbMoves
= this.vr
.moves
.length
;
900 const gameId
= this.gameId
; //to know if game was reset before timer end
903 if (gameId
!= this.gameId
)
904 return; //game stopped
905 const L
= this.vr
.moves
.length
;
906 if (nbMoves
== L
|| !this.vr
.moves
[L
-1].notation
) //move search didn't finish
907 this.vr
.shouldReturn
= true;
909 const compMove
= this.vr
.getComputerMove();
910 // (first move) HACK: avoid selecting elements before they appear on page:
911 const delay
= Math
.max(500-(Date
.now()-timeStart
), 0);
912 setTimeout(() => this.play(compMove
, "animate"), delay
);
914 // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
915 getSquareId: function(o
) {
916 // NOTE: a separator is required to allow any size of board
917 return "sq-" + o
.x
+ "-" + o
.y
;
920 getSquareFromId: function(id
) {
921 let idParts
= id
.split('-');
922 return [parseInt(idParts
[1]), parseInt(idParts
[2])];
924 mousedown: function(e
) {
925 e
= e
|| window
.event
;
928 while (!ingame
&& elem
!== null)
930 if (elem
.classList
.contains("game"))
935 elem
= elem
.parentElement
;
937 if (!ingame
) //let default behavior (click on button...)
939 e
.preventDefault(); //disable native drag & drop
940 if (!this.selectedPiece
&& e
.target
.classList
.contains("piece"))
942 // Next few lines to center the piece on mouse cursor
943 let rect
= e
.target
.parentNode
.getBoundingClientRect();
945 x: rect
.x
+ rect
.width
/2,
946 y: rect
.y
+ rect
.width
/2,
947 id: e
.target
.parentNode
.id
949 this.selectedPiece
= e
.target
.cloneNode();
950 this.selectedPiece
.style
.position
= "absolute";
951 this.selectedPiece
.style
.top
= 0;
952 this.selectedPiece
.style
.display
= "inline-block";
953 this.selectedPiece
.style
.zIndex
= 3000;
954 let startSquare
= this.getSquareFromId(e
.target
.parentNode
.id
);
955 const iCanPlay
= this.mode
!="idle"
956 && (this.mode
=="friend" || this.vr
.canIplay(this.mycolor
,startSquare
));
957 this.possibleMoves
= iCanPlay
? this.vr
.getPossibleMovesFrom(startSquare
) : [];
958 // Next line add moving piece just after current image
959 // (required for Crazyhouse reserve)
960 e
.target
.parentNode
.insertBefore(this.selectedPiece
, e
.target
.nextSibling
);
963 mousemove: function(e
) {
964 if (!this.selectedPiece
)
966 e
= e
|| window
.event
;
967 // If there is an active element, move it around
968 if (!!this.selectedPiece
)
970 const [offsetX
,offsetY
] = !!e
.clientX
971 ? [e
.clientX
,e
.clientY
] //desktop browser
972 : [e
.changedTouches
[0].pageX
, e
.changedTouches
[0].pageY
]; //smartphone
973 this.selectedPiece
.style
.left
= (offsetX
-this.start
.x
) + "px";
974 this.selectedPiece
.style
.top
= (offsetY
-this.start
.y
) + "px";
977 mouseup: function(e
) {
978 if (!this.selectedPiece
)
980 e
= e
|| window
.event
;
981 // Read drop target (or parentElement, parentNode... if type == "img")
982 this.selectedPiece
.style
.zIndex
= -3000; //HACK to find square from final coords
983 const [offsetX
,offsetY
] = !!e
.clientX
984 ? [e
.clientX
,e
.clientY
]
985 : [e
.changedTouches
[0].pageX
, e
.changedTouches
[0].pageY
];
986 let landing
= document
.elementFromPoint(offsetX
, offsetY
);
987 this.selectedPiece
.style
.zIndex
= 3000;
988 // Next condition: classList.contains(piece) fails because of marks
989 while (landing
.tagName
== "IMG")
990 landing
= landing
.parentNode
;
991 if (this.start
.id
== landing
.id
)
993 // A click: selectedPiece and possibleMoves are already filled
996 // OK: process move attempt
997 let endSquare
= this.getSquareFromId(landing
.id
);
998 let moves
= this.findMatchingMoves(endSquare
);
999 this.possibleMoves
= [];
1000 if (moves
.length
> 1)
1001 this.choices
= moves
;
1002 else if (moves
.length
==1)
1003 this.play(moves
[0]);
1004 // Else: impossible move
1005 this.selectedPiece
.parentNode
.removeChild(this.selectedPiece
);
1006 delete this.selectedPiece
;
1007 this.selectedPiece
= null;
1009 findMatchingMoves: function(endSquare
) {
1010 // Run through moves list and return the matching set (if promotions...)
1012 this.possibleMoves
.forEach(function(m
) {
1013 if (endSquare
[0] == m
.end
.x
&& endSquare
[1] == m
.end
.y
)
1018 animateMove: function(move) {
1019 let startSquare
= document
.getElementById(this.getSquareId(move.start
));
1020 let endSquare
= document
.getElementById(this.getSquareId(move.end
));
1021 let rectStart
= startSquare
.getBoundingClientRect();
1022 let rectEnd
= endSquare
.getBoundingClientRect();
1023 let translation
= {x:rectEnd
.x
-rectStart
.x
, y:rectEnd
.y
-rectStart
.y
};
1025 document
.querySelector("#" + this.getSquareId(move.start
) + " > img.piece");
1026 // HACK for animation (with positive translate, image slides "under background")
1027 // Possible improvement: just alter squares on the piece's way...
1028 squares
= document
.getElementsByClassName("board");
1029 for (let i
=0; i
<squares
.length
; i
++)
1031 let square
= squares
.item(i
);
1032 if (square
.id
!= this.getSquareId(move.start
))
1033 square
.style
.zIndex
= "-1";
1035 movingPiece
.style
.transform
= "translate(" + translation
.x
+ "px," +
1036 translation
.y
+ "px)";
1037 movingPiece
.style
.transitionDuration
= "0.2s";
1038 movingPiece
.style
.zIndex
= "3000";
1040 for (let i
=0; i
<squares
.length
; i
++)
1041 squares
.item(i
).style
.zIndex
= "auto";
1042 movingPiece
.style
= {}; //required e.g. for 0-0 with KR swap
1046 play: function(move, programmatic
) {
1049 // Navigate after game is over
1050 if (this.cursor
>= this.vr
.moves
.length
)
1051 return; //already at the end
1052 move = this.vr
.moves
[this.cursor
++];
1054 if (!!programmatic
) //computer or human opponent
1056 this.animateMove(move);
1059 // Not programmatic, or animation is over
1060 if (this.mode
== "human" && this.vr
.turn
== this.mycolor
)
1061 this.conn
.send(JSON
.stringify({code:"newmove", move:move, oppid:this.oppid
}));
1062 new Audio("/sounds/chessmove1.mp3").play().then(() => {}).catch(err
=> {});
1063 if (this.mode
!= "idle")
1065 this.incheck
= this.vr
.getCheckSquares(move); //is opponent in check?
1066 this.vr
.play(move, "ingame");
1070 VariantRules
.PlayOnBoard(this.vr
.board
, move);
1071 this.$forceUpdate(); //TODO: ?!
1073 if (this.mode
== "human")
1074 this.updateStorage(); //after our moves and opponent moves
1075 if (this.mode
!= "idle")
1077 const eog
= this.vr
.checkGameOver();
1081 if (this.mode
== "computer" && this.vr
.turn
!= this.mycolor
)
1082 setTimeout(this.playComputerMove
, 500);
1085 // Navigate after game is over
1086 if (this.cursor
== 0)
1087 return; //already at the beginning
1088 if (this.cursor
== this.vr
.moves
.length
)
1089 this.incheck
= []; //in case of...
1090 const move = this.vr
.moves
[--this.cursor
];
1091 VariantRules
.UndoOnBoard(this.vr
.board
, move);
1092 this.$forceUpdate(); //TODO: ?!
1094 undoInGame: function() {
1095 const lm
= this.vr
.lastMove
;