Reorganize and completed board component. Now need to finish game component
[vchess.git] / public / javascripts / components / board.js
CommitLineData
e5dc87e0
BA
1Vue.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"],
8 data: function () {
9 return {
81da2786
BA
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)
15 incheck: [],
16 start: {}, //pixels coordinates + id of starting square (click or drag)
17 vr: null, //object to check moves, store them, FEN..
e5dc87e0
BA
18 };
19 },
20 watch: {
21 // NOTE: maybe next 3 should be encapsulated in object to be watched (?)
22 fen: function(newFen) {
23 this.vr = new VariantRules(newFen);
24 },
25 moveToPlay: function(move) {
26 this.play(move, "animate");
27 },
28 moveToUndo: function(move) {
29 this.undo(move);
30 },
31 },
32 created: function() {
33 this.vr = new VariantRules(this.fen);
34 },
35 render(h) {
36 const [sizeX,sizeY] = [V.size.x,V.size.y];
81da2786
BA
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; });
e5dc87e0
BA
43 const choices = h(
44 'div',
45 {
46 attrs: { "id": "choices" },
47 'class': { 'row': true },
48 style: {
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",
81da2786 53 },
e5dc87e0
BA
54 },
55 this.choices.map(m => { //a "choice" is a move
56 return h('div',
57 {
58 'class': {
59 'board': true,
60 ['board'+sizeY]: true,
61 },
62 style: {
63 'width': (100/this.choices.length) + "%",
64 'padding-bottom': (100/this.choices.length) + "%",
81da2786 65 },
81da2786 66 },
e5dc87e0 67 [h('img',
81da2786 68 {
e5dc87e0
BA
69 attrs: { "src": '/images/pieces/' +
70 V.getPpath(m.appear[0].c+m.appear[0].p) + '.svg' },
71 'class': { 'choice-piece': true },
72 on: {
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=[]; },
81da2786 76 },
81da2786 77 })
e5dc87e0
BA
78 ]
79 );
80 })
81 );
82 // Create board element (+ reserves if needed by variant or mode)
83 const lm = this.lastMove;
84 const showLight = this.hints && variant.name != "Dark";
85 const gameDiv = h(
86 'div',
81da2786 87 {
e5dc87e0
BA
88 'class': {
89 'game': true,
90 'clearer': true,
91 },
92 },
93 [_.range(sizeX).map(i => {
94 let ci = (this.orientation=='w' ? i : sizeX-i-1);
95 return h(
96 'div',
81da2786 97 {
e5dc87e0
BA
98 'class': {
99 'row': true,
100 },
101 style: { 'opacity': this.choices.length>0?"0.5":"1" },
81da2786 102 },
e5dc87e0
BA
103 _.range(sizeY).map(j => {
104 let cj = (this.orientation=='w' ? j : sizeY-j-1);
105 let elems = [];
106 if (this.vr.board[ci][cj] != V.EMPTY && (variant.name!="Dark"
107 || this.gameOver || this.vr.enlightened[this.userColor][ci][cj]))
81da2786 108 {
e5dc87e0
BA
109 elems.push(
110 h(
111 'img',
112 {
113 'class': {
114 'piece': true,
115 'ghost': !!this.selectedPiece
116 && this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
117 },
118 attrs: {
119 src: "/images/pieces/" +
120 V.getPpath(this.vr.board[ci][cj]) + ".svg",
121 },
122 }
123 )
124 );
125 }
126 if (this.hints && hintSquares[ci][cj])
81da2786 127 {
e5dc87e0
BA
128 elems.push(
129 h(
130 'img',
131 {
132 'class': {
133 'mark-square': true,
134 },
135 attrs: {
136 src: "/images/mark.svg",
137 },
138 }
139 )
140 );
141 }
142 return h(
143 'div',
81da2786
BA
144 {
145 'class': {
e5dc87e0
BA
146 'board': true,
147 ['board'+sizeY]: true,
148 'light-square': (i+j)%2==0,
149 'dark-square': (i+j)%2==1,
150 [this.bcolor]: true,
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],
155 },
156 attrs: {
157 id: this.getSquareId({x:ci,y:cj}),
81da2786
BA
158 },
159 },
e5dc87e0
BA
160 elems
161 );
162 })
81da2786 163 );
e5dc87e0
BA
164 }), choices]
165 );
166 let elementArray = [choices, gameDiv];
167 if (!!this.vr.reserve)
168 {
169 const shiftIdx = (this.userColor=="w" ? 0 : 1);
170 let myReservePiecesArray = [];
171 for (let i=0; i<V.RESERVE_PIECES.length; i++)
172 {
173 myReservePiecesArray.push(h('div',
174 {
175 'class': {'board':true, ['board'+sizeY]:true},
176 attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
177 },
178 [
179 h('img',
180 {
181 'class': {"piece":true, "reserve":true},
182 attrs: {
183 "src": "/images/pieces/" +
184 this.vr.getReservePpath(this.userColor,i) + ".svg",
185 }
186 }),
187 h('sup',
188 {"class": { "reserve-count": true } },
189 [ this.vr.reserve[this.userColor][V.RESERVE_PIECES[i]] ]
190 )
191 ]));
192 }
193 let oppReservePiecesArray = [];
194 const oppCol = this.vr.getOppCol(this.userColor);
195 for (let i=0; i<V.RESERVE_PIECES.length; i++)
196 {
197 oppReservePiecesArray.push(h('div',
198 {
199 'class': {'board':true, ['board'+sizeY]:true},
200 attrs: { id: this.getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
201 },
202 [
203 h('img',
204 {
205 'class': {"piece":true, "reserve":true},
206 attrs: {
207 "src": "/images/pieces/" +
208 this.vr.getReservePpath(oppCol,i) + ".svg",
209 }
210 }),
211 h('sup',
212 {"class": { "reserve-count": true } },
213 [ this.vr.reserve[oppCol][V.RESERVE_PIECES[i]] ]
214 )
215 ]));
81da2786 216 }
e5dc87e0
BA
217 let reserves = h('div',
218 {
219 'class':{
220 'game': true,
221 "reserve-div": true,
222 },
223 },
224 [
81da2786
BA
225 h('div',
226 {
e5dc87e0
BA
227 'class': {
228 'row': true,
229 "reserve-row-1": true,
230 },
81da2786 231 },
e5dc87e0
BA
232 myReservePiecesArray
233 ),
234 h('div',
235 { 'class': { 'row': true }},
236 oppReservePiecesArray
81da2786 237 )
e5dc87e0
BA
238 ]
239 );
240 elementArray.push(reserves);
241 }
242 return h(
243 'div',
244 {
245 'class': {
246 "col-sm-12":true,
247 "col-md-10":true,
248 "col-md-offset-1":true,
249 "col-lg-8":true,
250 "col-lg-offset-2":true,
251 },
252 // NOTE: click = mousedown + mouseup
81da2786
BA
253 on: {
254 mousedown: this.mousedown,
255 mousemove: this.mousemove,
256 mouseup: this.mouseup,
257 touchstart: this.mousedown,
258 touchmove: this.mousemove,
259 touchend: this.mouseup,
260 },
e5dc87e0
BA
261 },
262 elementArray
263 );
264 },
265 methods: {
266 // Get the identifier of a HTML square from its numeric coordinates o.x,o.y.
81da2786
BA
267 getSquareId: function(o) {
268 // NOTE: a separator is required to allow any size of board
269 return "sq-" + o.x + "-" + o.y;
270 },
271 // Inverse function
272 getSquareFromId: function(id) {
273 let idParts = id.split('-');
274 return [parseInt(idParts[1]), parseInt(idParts[2])];
275 },
276 mousedown: function(e) {
277 e = e || window.event;
278 let ingame = false;
279 let elem = e.target;
280 while (!ingame && elem !== null)
281 {
282 if (elem.classList.contains("game"))
283 {
284 ingame = true;
285 break;
286 }
287 elem = elem.parentElement;
288 }
289 if (!ingame) //let default behavior (click on button...)
290 return;
291 e.preventDefault(); //disable native drag & drop
292 if (!this.selectedPiece && e.target.classList.contains("piece"))
293 {
294 // Next few lines to center the piece on mouse cursor
295 let rect = e.target.parentNode.getBoundingClientRect();
296 this.start = {
297 x: rect.x + rect.width/2,
298 y: rect.y + rect.width/2,
299 id: e.target.parentNode.id
300 };
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 = [];
e5dc87e0
BA
308 const color = this.analyze || this.gameOver
309 ? this.vr.turn
310 : this.userColor;
311 if (this.vr.canIplay(color,startSquare))
312 this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
81da2786
BA
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);
316 }
317 },
318 mousemove: function(e) {
319 if (!this.selectedPiece)
320 return;
321 e = e || window.event;
322 // If there is an active element, move it around
323 if (!!this.selectedPiece)
324 {
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";
330 }
331 },
332 mouseup: function(e) {
333 if (!this.selectedPiece)
334 return;
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)
347 {
348 // A click: selectedPiece and possibleMoves are already filled
349 return;
350 }
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)
358 this.play(moves[0]);
359 // Else: impossible move
360 this.selectedPiece.parentNode.removeChild(this.selectedPiece);
361 delete this.selectedPiece;
362 this.selectedPiece = null;
363 },
364 findMatchingMoves: function(endSquare) {
365 // Run through moves list and return the matching set (if promotions...)
366 let moves = [];
367 this.possibleMoves.forEach(function(m) {
368 if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
369 moves.push(m);
370 });
371 return moves;
372 },
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};
379 let movingPiece =
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++)
385 {
386 let square = squares.item(i);
387 if (square.id != this.getSquareId(move.start))
388 square.style.zIndex = "-1";
389 }
390 movingPiece.style.transform = "translate(" + translation.x + "px," +
391 translation.y + "px)";
392 movingPiece.style.transitionDuration = "0.2s";
393 movingPiece.style.zIndex = "3000";
394 setTimeout( () => {
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
398 this.play(move);
399 }, 250);
400 },
e5dc87e0
BA
401 play: function(move, programmatic) {
402 if (!!programmatic) //computer or human opponent
403 return this.animateMove(move);
404 // Not programmatic, or animation is over
405 this.vr.play(move);
406 if (this.sound == 2)
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();
411 if (eog != "*")
412 {
413 // TODO: notify end of game (give score)
414 }
415 },
416 undo: function(move) {
417 this.vr.undo(move);
418 if (this.sound == 2)
419 new Audio("/sounds/undo.mp3").play().catch(err => {});
420 this.incheck = this.vr.getCheckSquares(this.vr.turn);
421 },
422 },
423})