1 Vue
.component('my-board', {
2 // Last move cannot be guessed from here, and is required to highlight squares
3 // gotoMove : juste set FEN depuis FEN stocké dans le coup (TODO)
4 // send event after each move (or undo), to notify what was played
5 // also notify end of game (which returns here later through prop...)
6 props: ["fen","moveToPlay","moveToUndo",
7 "analyze","lastMove","orientation","userColor","gameOver"],
10 hints: (!localStorage
["hints"] ? true : localStorage
["hints"] === "1"),
11 bcolor: localStorage
["bcolor"] || "lichess", //lichess, chesscom or chesstempo
12 possibleMoves: [], //filled after each valid click/dragstart
13 choices: [], //promotion pieces, or checkered captures... (as moves)
14 selectedPiece: null, //moving piece (or clicked piece)
16 start: {}, //pixels coordinates + id of starting square (click or drag)
17 vr: null, //object to check moves, store them, FEN..
21 // NOTE: maybe next 3 should be encapsulated in object to be watched (?)
22 fen: function(newFen
) {
23 this.vr
= new VariantRules(newFen
);
25 moveToPlay: function(move) {
26 this.play(move, "animate");
28 moveToUndo: function(move) {
33 this.vr
= new VariantRules(this.fen
);
36 const [sizeX
,sizeY
] = [V
.size
.x
,V
.size
.y
];
37 // Precompute hints squares to facilitate rendering
38 let hintSquares
= doubleArray(sizeX
, sizeY
, false);
39 this.possibleMoves
.forEach(m
=> { hintSquares
[m
.end
.x
][m
.end
.y
] = true; });
40 // Also precompute in-check squares
41 let incheckSq
= doubleArray(sizeX
, sizeY
, false);
42 this.incheck
.forEach(sq
=> { incheckSq
[sq
[0]][sq
[1]] = true; });
46 attrs: { "id": "choices" },
47 'class': { 'row': true },
49 "display": this.choices
.length
>0?"block":"none",
50 "top": "-" + ((sizeY
/2)*squareWidth+squareWidth/2) + "px",
51 "width": (this.choices
.length
* squareWidth
) + "px",
52 "height": squareWidth
+ "px",
55 this.choices
.map(m
=> { //a "choice" is a move
60 ['board'+sizeY
]: true,
63 'width': (100/this.choices
.length
) + "%",
64 'padding-bottom': (100/this.choices
.length
) + "%",
69 attrs: { "src": '/images/pieces/' +
70 V
.getPpath(m
.appear
[0].c
+m
.appear
[0].p
) + '.svg' },
71 'class': { 'choice-piece': true },
73 "click": e
=> { this.play(m
); this.choices
=[]; },
74 // NOTE: add 'touchstart' event to fix a problem on smartphones
75 "touchstart": e
=> { this.play(m
); this.choices
=[]; },
82 // Create board element (+ reserves if needed by variant or mode)
83 const lm
= this.lastMove
;
84 const showLight
= this.hints
&& variant
.name
!= "Dark";
93 [_
.range(sizeX
).map(i
=> {
94 let ci
= (this.orientation
=='w' ? i : sizeX
-i
-1);
101 style: { 'opacity': this.choices
.length
>0?"0.5":"1" },
103 _
.range(sizeY
).map(j
=> {
104 let cj
= (this.orientation
=='w' ? j : sizeY
-j
-1);
106 if (this.vr
.board
[ci
][cj
] != V
.EMPTY
&& (variant
.name
!="Dark"
107 || this.gameOver
|| this.vr
.enlightened
[this.userColor
][ci
][cj
]))
115 'ghost': !!this.selectedPiece
116 && this.selectedPiece
.parentNode
.id
== "sq-"+ci
+"-"+cj
,
119 src: "/images/pieces/" +
120 V
.getPpath(this.vr
.board
[ci
][cj
]) + ".svg",
126 if (this.hints
&& hintSquares
[ci
][cj
])
136 src: "/images/mark.svg",
147 ['board'+sizeY
]: true,
148 'light-square': (i
+j
)%2==0,
149 'dark-square': (i
+j
)%2==1,
151 'in-shadow': variant
.name
=="Dark" && !this.gameOver
152 && !this.vr
.enlightened
[this.userColor
][ci
][cj
],
153 'highlight': showLight
&& !!lm
&& _
.isMatch(lm
.end
, {x:ci
,y:cj
}),
154 'incheck': showLight
&& incheckSq
[ci
][cj
],
157 id: this.getSquareId({x:ci
,y:cj
}),
166 let elementArray
= [choices
, gameDiv
];
167 if (!!this.vr
.reserve
)
169 const shiftIdx
= (this.userColor
=="w" ? 0 : 1);
170 let myReservePiecesArray
= [];
171 for (let i
=0; i
<V
.RESERVE_PIECES
.length
; i
++)
173 myReservePiecesArray
.push(h('div',
175 'class': {'board':true, ['board'+sizeY
]:true},
176 attrs: { id: this.getSquareId({x:sizeX
+shiftIdx
,y:i
}) }
181 'class': {"piece":true, "reserve":true},
183 "src": "/images/pieces/" +
184 this.vr
.getReservePpath(this.userColor
,i
) + ".svg",
188 {"class": { "reserve-count": true } },
189 [ this.vr
.reserve
[this.userColor
][V
.RESERVE_PIECES
[i
]] ]
193 let oppReservePiecesArray
= [];
194 const oppCol
= this.vr
.getOppCol(this.userColor
);
195 for (let i
=0; i
<V
.RESERVE_PIECES
.length
; i
++)
197 oppReservePiecesArray
.push(h('div',
199 'class': {'board':true, ['board'+sizeY
]:true},
200 attrs: { id: this.getSquareId({x:sizeX
+(1-shiftIdx
),y:i
}) }
205 'class': {"piece":true, "reserve":true},
207 "src": "/images/pieces/" +
208 this.vr
.getReservePpath(oppCol
,i
) + ".svg",
212 {"class": { "reserve-count": true } },
213 [ this.vr
.reserve
[oppCol
][V
.RESERVE_PIECES
[i
]] ]
217 let reserves
= h('div',
229 "reserve-row-1": true,
235 { 'class': { 'row': true }},
236 oppReservePiecesArray
240 elementArray
.push(reserves
);
248 "col-md-offset-1":true,
250 "col-lg-offset-2":true,
252 // NOTE: click = mousedown + mouseup
254 mousedown: this.mousedown
,
255 mousemove: this.mousemove
,
256 mouseup: this.mouseup
,
257 touchstart: this.mousedown
,
258 touchmove: this.mousemove
,
259 touchend: this.mouseup
,
266 // Get the identifier of a HTML square from its numeric coordinates o.x,o.y.
267 getSquareId: function(o
) {
268 // NOTE: a separator is required to allow any size of board
269 return "sq-" + o
.x
+ "-" + o
.y
;
272 getSquareFromId: function(id
) {
273 let idParts
= id
.split('-');
274 return [parseInt(idParts
[1]), parseInt(idParts
[2])];
276 mousedown: function(e
) {
277 e
= e
|| window
.event
;
280 while (!ingame
&& elem
!== null)
282 if (elem
.classList
.contains("game"))
287 elem
= elem
.parentElement
;
289 if (!ingame
) //let default behavior (click on button...)
291 e
.preventDefault(); //disable native drag & drop
292 if (!this.selectedPiece
&& e
.target
.classList
.contains("piece"))
294 // Next few lines to center the piece on mouse cursor
295 let rect
= e
.target
.parentNode
.getBoundingClientRect();
297 x: rect
.x
+ rect
.width
/2,
298 y: rect
.y
+ rect
.width
/2,
299 id: e
.target
.parentNode
.id
301 this.selectedPiece
= e
.target
.cloneNode();
302 this.selectedPiece
.style
.position
= "absolute";
303 this.selectedPiece
.style
.top
= 0;
304 this.selectedPiece
.style
.display
= "inline-block";
305 this.selectedPiece
.style
.zIndex
= 3000;
306 const startSquare
= this.getSquareFromId(e
.target
.parentNode
.id
);
307 this.possibleMoves
= [];
308 const color
= this.analyze
|| this.gameOver
311 if (this.vr
.canIplay(color
,startSquare
))
312 this.possibleMoves
= this.vr
.getPossibleMovesFrom(startSquare
);
313 // Next line add moving piece just after current image
314 // (required for Crazyhouse reserve)
315 e
.target
.parentNode
.insertBefore(this.selectedPiece
, e
.target
.nextSibling
);
318 mousemove: function(e
) {
319 if (!this.selectedPiece
)
321 e
= e
|| window
.event
;
322 // If there is an active element, move it around
323 if (!!this.selectedPiece
)
325 const [offsetX
,offsetY
] = !!e
.clientX
326 ? [e
.clientX
,e
.clientY
] //desktop browser
327 : [e
.changedTouches
[0].pageX
, e
.changedTouches
[0].pageY
]; //smartphone
328 this.selectedPiece
.style
.left
= (offsetX
-this.start
.x
) + "px";
329 this.selectedPiece
.style
.top
= (offsetY
-this.start
.y
) + "px";
332 mouseup: function(e
) {
333 if (!this.selectedPiece
)
335 e
= e
|| window
.event
;
336 // Read drop target (or parentElement, parentNode... if type == "img")
337 this.selectedPiece
.style
.zIndex
= -3000; //HACK to find square from final coords
338 const [offsetX
,offsetY
] = !!e
.clientX
339 ? [e
.clientX
,e
.clientY
]
340 : [e
.changedTouches
[0].pageX
, e
.changedTouches
[0].pageY
];
341 let landing
= document
.elementFromPoint(offsetX
, offsetY
);
342 this.selectedPiece
.style
.zIndex
= 3000;
343 // Next condition: classList.contains(piece) fails because of marks
344 while (landing
.tagName
== "IMG")
345 landing
= landing
.parentNode
;
346 if (this.start
.id
== landing
.id
)
348 // A click: selectedPiece and possibleMoves are already filled
351 // OK: process move attempt
352 let endSquare
= this.getSquareFromId(landing
.id
);
353 let moves
= this.findMatchingMoves(endSquare
);
354 this.possibleMoves
= [];
355 if (moves
.length
> 1)
356 this.choices
= moves
;
357 else if (moves
.length
==1)
359 // Else: impossible move
360 this.selectedPiece
.parentNode
.removeChild(this.selectedPiece
);
361 delete this.selectedPiece
;
362 this.selectedPiece
= null;
364 findMatchingMoves: function(endSquare
) {
365 // Run through moves list and return the matching set (if promotions...)
367 this.possibleMoves
.forEach(function(m
) {
368 if (endSquare
[0] == m
.end
.x
&& endSquare
[1] == m
.end
.y
)
373 animateMove: function(move) {
374 let startSquare
= document
.getElementById(this.getSquareId(move.start
));
375 let endSquare
= document
.getElementById(this.getSquareId(move.end
));
376 let rectStart
= startSquare
.getBoundingClientRect();
377 let rectEnd
= endSquare
.getBoundingClientRect();
378 let translation
= {x:rectEnd
.x
-rectStart
.x
, y:rectEnd
.y
-rectStart
.y
};
380 document
.querySelector("#" + this.getSquareId(move.start
) + " > img.piece");
381 // HACK for animation (with positive translate, image slides "under background")
382 // Possible improvement: just alter squares on the piece's way...
383 squares
= document
.getElementsByClassName("board");
384 for (let i
=0; i
<squares
.length
; i
++)
386 let square
= squares
.item(i
);
387 if (square
.id
!= this.getSquareId(move.start
))
388 square
.style
.zIndex
= "-1";
390 movingPiece
.style
.transform
= "translate(" + translation
.x
+ "px," +
391 translation
.y
+ "px)";
392 movingPiece
.style
.transitionDuration
= "0.2s";
393 movingPiece
.style
.zIndex
= "3000";
395 for (let i
=0; i
<squares
.length
; i
++)
396 squares
.item(i
).style
.zIndex
= "auto";
397 movingPiece
.style
= {}; //required e.g. for 0-0 with KR swap
401 play: function(move, programmatic
) {
402 if (!!programmatic
) //computer or human opponent
403 return this.animateMove(move);
404 // Not programmatic, or animation is over
407 new Audio("/sounds/move.mp3").play().catch(err
=> {});
408 // Is opponent in check?
409 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);
410 const eog
= this.vr
.getCurrentScore();
413 // TODO: notify end of game (give score)
416 undo: function(move) {
419 new Audio("/sounds/undo.mp3").play().catch(err
=> {});
420 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);