1 Vue
.component('my-game', {
4 vr: null, //object to check moves, store them, FEN..
6 possibleMoves: [], //filled after each valid click/dragstart
7 choices: [], //promotion pieces, or checkered captures... (contain possible pieces)
8 start: {}, //pixels coordinates + id of starting square (click or drag)
9 selectedPiece: null, //moving piece (or clicked piece)
10 conn: null, //socket messages
11 endofgame: "", //end of game message
12 mode: "idle", //human, computer or idle (when not playing)
13 oppid: "", //opponent ID in case of HH game
18 let [sizeX
,sizeY
] = VariantRules
.size
;
19 // Precompute hints squares to facilitate rendering
20 let hintSquares
= doubleArray(sizeX
, sizeY
, false);
21 this.possibleMoves
.forEach(m
=> { hintSquares
[m
.end
.x
][m
.end
.y
] = true; });
22 let elementArray
= [];
23 let square00
= document
.getElementById("sq-0-0");
24 let squareWidth
= !!square00
25 ? parseFloat(window
.getComputedStyle(square00
).width
.slice(0,-2))
30 on: { click: () => this.newGame("human") },
31 attrs: { "aria-label": 'New game VS human' },
32 'class': { "tooltip":true },
34 [h('i', { 'class': { "material-icons": true } }, "accessibility")]),
37 on: { click: () => this.newGame("computer") },
38 attrs: { "aria-label": 'New game VS computer' },
39 'class': { "tooltip":true },
41 [h('i', { 'class': { "material-icons": true } }, "computer")])
45 if (this.mode
== "human")
47 let connectedIndic
= h(
51 "connected": this.oppConnected
,
52 "disconnected": !this.oppConnected
,
56 elementArray
.push(connectedIndic
);
58 let choices
= h('div',
60 attrs: { "id": "choices" },
61 'class': { 'row': true },
63 //"position": "relative",
64 "display": this.choices
.length
>0?"block":"none",
65 "top": "-" + ((sizeY
/2)*squareWidth+squareWidth/2) + "px",
66 "width": (this.choices
.length
* squareWidth
) + "px",
67 "height": squareWidth
+ "px",
70 this.choices
.map( m
=> { //a "choice" is a move
73 'class': { 'board': true },
75 'width': (100/this.choices
.length
) + "%",
76 'padding-bottom': (100/this.choices
.length
) + "%",
81 attrs: { "src": '/images/pieces/' + VariantRules
.getPpath(m
.appear
[0].c
+m
.appear
[0].p
) + '.svg' },
82 'class': { 'choice-piece': true, 'board': true },
83 on: { "click": e
=> { this.play(m
); this.choices
=[]; } },
89 // Create board element (+ reserves if needed by variant or mode)
90 let gameDiv
= h('div',
92 'class': { 'game': true },
94 [_
.range(sizeX
).map(i
=> {
95 let ci
= this.mycolor
=='w' ? i : sizeX
-i
-1;
102 style: { 'opacity': this.choices
.length
>0?"0.5":"1" },
104 _
.range(sizeY
).map(j
=> {
105 let cj
= this.mycolor
=='w' ? j : sizeY
-j
-1;
107 if (this.vr
.board
[ci
][cj
] != VariantRules
.EMPTY
)
115 'ghost': !!this.selectedPiece
&& this.selectedPiece
.parentNode
.id
== "sq-"+ci
+"-"+cj
,
118 src: "/images/pieces/" + VariantRules
.getPpath(this.vr
.board
[ci
][cj
]) + ".svg",
124 if (hintSquares
[ci
][cj
])
134 src: "/images/mark.svg",
140 const lm
= this.vr
.lastMove
; //TODO: interruptions (FEN local storage..)
141 const highlight
= !!lm
&& _
.isMatch(lm
.end
, {x:ci
,y:cj
}); //&& _.isMatch(lm.start, {x:ci,y:cj})
147 'light-square': !highlight
&& (i
+j
)%2==0,
148 'dark-square': !highlight
&& (i
+j
)%2==1,
149 'highlight': highlight
,
152 id: this.getSquareId({x:ci
,y:cj
}),
164 on: { click: this.resign
},
165 attrs: { "aria-label": 'Resign' },
166 'class': { "tooltip":true },
168 [h('i', { 'class': { "material-icons": true } }, "flag")])
170 elementArray
.push(gameDiv
);
173 // let reserve = h('div',
174 // {'class':{'game':true}}, [
176 // { 'class': { 'row': true }},
179 // {'class':{'board':true}},
180 // [h('img',{'class':{"piece":true},attrs:{"src":"/images/pieces/wb.svg"}})]
186 // elementArray.push(reserve);
191 attrs: { "id": "modal-control", type: "checkbox" },
192 "class": { "modal": true },
196 attrs: { "role": "dialog", "aria-labelledby": "dialog-title" },
201 "class": { "card": true, "smallpad": true },
206 attrs: { "for": "modal-control" },
207 "class": { "modal-close": true },
212 "class": { "section": true },
213 domProps: { innerHTML: "End of game" },
218 "class": { "section": true },
219 domProps: { innerHTML: this.endofgame
},
227 elementArray
= elementArray
.concat(modalEog
);
229 const modalNewgame
= [
232 attrs: { "id": "modal-control2", type: "checkbox" },
233 "class": { "modal": true },
237 attrs: { "role": "dialog", "aria-labelledby": "dialog-title" },
242 "class": { "card": true, "smallpad": true },
247 attrs: { "id": "close-newgame", "for": "modal-control2" },
248 "class": { "modal-close": true },
253 "class": { "section": true },
254 domProps: { innerHTML: "New game" },
259 "class": { "section": true },
260 domProps: { innerHTML: "Waiting for opponent..." },
268 elementArray
= elementArray
.concat(modalNewgame
);
269 const actions
= h('div',
271 attrs: { "id": "actions" },
272 'class': { 'text-center': true },
276 elementArray
.push(actions
);
283 "col-md-offset-2":true,
285 "col-lg-offset-3":true,
287 // NOTE: click = mousedown + mouseup --> what about smartphone?!
289 mousedown: this.mousedown
,
290 mousemove: this.mousemove
,
291 mouseup: this.mouseup
,
292 touchdown: this.mousedown
,
293 touchmove: this.mousemove
,
294 touchup: this.mouseup
,
300 created: function() {
301 const url
= socketUrl
;
302 const continuation
= (localStorage
.getItem("variant") === variant
);
303 this.myid
= continuation
304 ? localStorage
.getItem("myid")
305 // random enough (TODO: function)
306 : (Date
.now().toString(36) + Math
.random().toString(36).substr(2, 7)).toUpperCase();
307 this.conn
= new WebSocket(url
+ "/?sid=" + this.myid
+ "&page=" + variant
);
308 this.conn
.onopen
= () => {
311 // TODO: check FEN integrity with opponent
312 this.newGame("human", localStorage
.getItem("fen"),
313 localStorage
.getItem("mycolor"), localStorage
.getItem("oppid"), true);
314 // Send ping to server, which answers pong if opponent is connected
315 this.conn
.send(JSON
.stringify({code:"ping", oppid:this.oppId
}));
318 this.conn
.onmessage
= msg
=> {
319 const data
= JSON
.parse(msg
.data
);
322 case "newgame": //opponent found
323 this.newGame("human", data
.fen
, data
.color
, data
.oppid
); //oppid: opponent socket ID
325 case "newmove": //..he played!
326 this.play(data
.move, "animate");
328 case "pong": //sent when opponent stayed online after we disconnected
329 this.oppConnected
= true;
331 case "resign": //..you won!
332 this.endGame("Victory!");
334 // TODO: also use (dis)connect info to count online players
337 if (this.mode
== "human" && this.oppid
== data
.id
)
338 this.oppConnected
= (data
.code
== "connect");
344 endGame: function(message
) {
345 this.endofgame
= message
;
346 document
.getElementById("modal-control").checked
= true;
347 if (this.mode
== "human")
353 if (this.mode
== "human" && this.oppConnected
)
354 this.conn
.send(JSON
.stringify({code: "resign", oppid: this.oppid
}));
355 this.endGame("Try again!");
357 updateStorage: function() {
358 if (!localStorage
.getItem("myid"))
360 localStorage
.setItem("myid", this.myid
);
361 localStorage
.setItem("variant", variant
);
362 localStorage
.setItem("mycolor", this.mycolor
);
363 localStorage
.setItem("oppid", this.oppid
);
365 localStorage
.setItem("fen", this.vr
.getFen());
367 clearStorage: function() {
368 delete localStorage
["variant"];
369 delete localStorage
["myid"];
370 delete localStorage
["mycolor"];
371 delete localStorage
["oppid"];
372 delete localStorage
["fen"];
374 newGame: function(mode
, fenInit
, color
, oppId
, continuation
) {
375 const fen
= fenInit
|| VariantRules
.GenRandInitFen();
376 console
.log(fen
); //DEBUG
377 if (mode
=="human" && !oppId
)
379 // Send game request and wait..
380 this.clearStorage(); //in case of
381 this.conn
.send(JSON
.stringify({code:"newgame", fen:fen
}));
382 document
.getElementById("modal-control2").checked
= true;
385 this.vr
= new VariantRules(fen
);
392 // Playing sound fails on game continuation:
393 new Audio("/sounds/newgame.mp3").play();
394 document
.getElementById("modal-control2").checked
= false;
397 this.oppConnected
= true;
398 this.mycolor
= color
;
400 else //against computer
402 this.mycolor
= Math
.random() < 0.5 ? 'w' : 'b';
403 if (this.mycolor
== 'b')
404 setTimeout(this.playComputerMove
, 500);
407 playComputerMove: function() {
408 const compColor
= this.mycolor
=='w' ? 'b' : 'w';
409 const compMove
= this.vr
.getComputerMove(compColor
);
410 // HACK: avoid selecting elements before they appear on page:
411 setTimeout(() => this.play(compMove
, "animate"), 500);
413 // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
414 getSquareId: function(o
) {
415 // NOTE: a separator is required to allow any size of board
416 return "sq-" + o
.x
+ "-" + o
.y
;
419 getSquareFromId: function(id
) {
420 let idParts
= id
.split('-');
421 return [parseInt(idParts
[1]), parseInt(idParts
[2])];
423 mousedown: function(e
) {
424 e
= e
|| window
.event
;
425 e
.preventDefault(); //disable native drag & drop
426 if (!this.selectedPiece
&& e
.target
.classList
.contains("piece"))
428 // Next few lines to center the piece on mouse cursor
429 let rect
= e
.target
.parentNode
.getBoundingClientRect();
431 x: rect
.x
+ rect
.width
/2,
432 y: rect
.y
+ rect
.width
/2,
433 id: e
.target
.parentNode
.id
435 this.selectedPiece
= e
.target
.cloneNode();
436 this.selectedPiece
.style
.position
= "absolute";
437 this.selectedPiece
.style
.top
= 0;
438 this.selectedPiece
.style
.display
= "inline-block";
439 this.selectedPiece
.style
.zIndex
= 3000;
440 let startSquare
= this.getSquareFromId(e
.target
.parentNode
.id
);
441 this.possibleMoves
= this.vr
.canIplay(this.mycolor
,startSquare
)
442 ? this.vr
.getPossibleMovesFrom(startSquare
)
444 e
.target
.parentNode
.appendChild(this.selectedPiece
);
447 mousemove: function(e
) {
448 if (!this.selectedPiece
)
450 e
= e
|| window
.event
;
451 // If there is an active element, move it around
452 if (!!this.selectedPiece
)
454 this.selectedPiece
.style
.left
= (e
.clientX
-this.start
.x
) + "px";
455 this.selectedPiece
.style
.top
= (e
.clientY
-this.start
.y
) + "px";
458 mouseup: function(e
) {
459 if (!this.selectedPiece
)
461 e
= e
|| window
.event
;
462 // Read drop target (or parentElement, parentNode... if type == "img")
463 this.selectedPiece
.style
.zIndex
= -3000; //HACK to find square from final coordinates
464 let landing
= document
.elementFromPoint(e
.clientX
, e
.clientY
);
465 this.selectedPiece
.style
.zIndex
= 3000;
466 while (landing
.tagName
== "IMG") //classList.contains(piece) fails because of mark/highlight
467 landing
= landing
.parentNode
;
468 if (this.start
.id
== landing
.id
) //a click: selectedPiece and possibleMoves already filled
470 // OK: process move attempt
471 let endSquare
= this.getSquareFromId(landing
.id
);
472 let moves
= this.findMatchingMoves(endSquare
);
473 this.possibleMoves
= [];
474 if (moves
.length
> 1)
475 this.choices
= moves
;
476 else if (moves
.length
==1)
478 // Else: impossible move
479 this.selectedPiece
.parentNode
.removeChild(this.selectedPiece
);
480 delete this.selectedPiece
;
481 this.selectedPiece
= null;
483 findMatchingMoves: function(endSquare
) {
484 // Run through moves list and return the matching set (if promotions...)
486 this.possibleMoves
.forEach(function(m
) {
487 if (endSquare
[0] == m
.end
.x
&& endSquare
[1] == m
.end
.y
)
492 animateMove: function(move) {
493 let startSquare
= document
.getElementById(this.getSquareId(move.start
));
494 let endSquare
= document
.getElementById(this.getSquareId(move.end
));
495 let rectStart
= startSquare
.getBoundingClientRect();
496 let rectEnd
= endSquare
.getBoundingClientRect();
497 let translation
= {x:rectEnd
.x
-rectStart
.x
, y:rectEnd
.y
-rectStart
.y
};
498 let movingPiece
= document
.querySelector("#" + this.getSquareId(move.start
) + " > img.piece");
499 // HACK for animation (otherwise with positive translate, image slides "under background"...)
500 // Possible improvement: just alter squares on the piece's way...
501 squares
= document
.getElementsByClassName("board");
502 for (let i
=0; i
<squares
.length
; i
++)
504 let square
= squares
.item(i
);
505 if (square
.id
!= this.getSquareId(move.start
))
506 square
.style
.zIndex
= "-1";
508 movingPiece
.style
.transform
= "translate(" + translation
.x
+ "px," + translation
.y
+ "px)";
509 movingPiece
.style
.transitionDuration
= "0.2s";
510 movingPiece
.style
.zIndex
= "3000";
512 for (let i
=0; i
<squares
.length
; i
++)
513 squares
.item(i
).style
.zIndex
= "auto";
514 movingPiece
.style
= {}; //required e.g. for 0-0 with KR swap
518 play: function(move, programmatic
) {
519 if (!!programmatic
) //computer or human opponent
521 this.animateMove(move);
524 // Not programmatic, or animation is over
525 if (this.mode
== "human" && this.vr
.turn
== this.mycolor
)
527 if (!this.oppConnected
)
528 return; //abort move if opponent is gone
530 this.conn
.send(JSON
.stringify({code:"newmove", move:move, oppid:this.oppid
}));
531 } catch(INVALID_STATE_ERR
) {
532 return; //abort also if we lost connection
535 new Audio("/sounds/chessmove1.mp3").play();
536 this.vr
.play(move, "ingame");
537 if (this.mode
== "human")
538 this.updateStorage(); //after our moves and opponent moves
539 const eog
= this.vr
.checkGameOver(this.vr
.turn
);
542 else if (this.mode
== "computer" && this.vr
.turn
!= this.mycolor
)
543 setTimeout(this.playComputerMove
, 500);