4cadb0bd9ba75d68e8d2950aa302c3763564e301
[vchess.git] / public / javascripts / components / board.js
1 hints: (!localStorage["hints"] ? true : localStorage["hints"] === "1"),
2 bcolor: localStorage["bcolor"] || "lichess", //lichess, chesscom or chesstempo
3 possibleMoves: [], //filled after each valid click/dragstart
4 choices: [], //promotion pieces, or checkered captures... (as moves)
5 selectedPiece: null, //moving piece (or clicked piece)
6 incheck: [],
7 start: {}, //pixels coordinates + id of starting square (click or drag)
8 vr: null, //object to check moves, store them, FEN..
9 orientation: "w", //useful if click on "flip board"
10
11
12
13 const [sizeX,sizeY] = [V.size.x,V.size.y];
14 // Precompute hints squares to facilitate rendering
15 let hintSquares = doubleArray(sizeX, sizeY, false);
16 this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
17 // Also precompute in-check squares
18 let incheckSq = doubleArray(sizeX, sizeY, false);
19 this.incheck.forEach(sq => { incheckSq[sq[0]][sq[1]] = true; });
20 const choices = h('div',
21 {
22 attrs: { "id": "choices" },
23 'class': { 'row': true },
24 style: {
25 "display": this.choices.length>0?"block":"none",
26 "top": "-" + ((sizeY/2)*squareWidth+squareWidth/2) + "px",
27 "width": (this.choices.length * squareWidth) + "px",
28 "height": squareWidth + "px",
29 },
30 },
31 this.choices.map( m => { //a "choice" is a move
32 return h('div',
33 {
34 'class': {
35 'board': true,
36 ['board'+sizeY]: true,
37 },
38 style: {
39 'width': (100/this.choices.length) + "%",
40 'padding-bottom': (100/this.choices.length) + "%",
41 },
42 },
43 [h('img',
44 {
45 attrs: { "src": '/images/pieces/' +
46 VariantRules.getPpath(m.appear[0].c+m.appear[0].p) + '.svg' },
47 'class': { 'choice-piece': true },
48 on: {
49 "click": e => { this.play(m); this.choices=[]; },
50 // NOTE: add 'touchstart' event to fix a problem on smartphones
51 "touchstart": e => { this.play(m); this.choices=[]; },
52 },
53 })
54 ]
55 );
56 })
57 );
58 // Create board element (+ reserves if needed by variant or mode)
59 const lm = this.vr.lastMove;
60 const showLight = this.hints && variant!="Dark" &&
61 (this.mode != "idle" ||
62 (this.vr.moves.length > 0 && this.cursor==this.vr.moves.length));
63 const gameDiv = h('div',
64 {
65 'class': {
66 'game': true,
67 'clearer': true,
68 },
69 },
70 [_.range(sizeX).map(i => {
71 let ci = (this.mycolor=='w' ? i : sizeX-i-1);
72 return h(
73 'div',
74 {
75 'class': {
76 'row': true,
77 },
78 style: { 'opacity': this.choices.length>0?"0.5":"1" },
79 },
80 _.range(sizeY).map(j => {
81 let cj = (this.mycolor=='w' ? j : sizeY-j-1);
82 let elems = [];
83 if (this.vr.board[ci][cj] != VariantRules.EMPTY && (variant!="Dark"
84 || this.score!="*" || this.vr.enlightened[this.mycolor][ci][cj]))
85 {
86 elems.push(
87 h(
88 'img',
89 {
90 'class': {
91 'piece': true,
92 'ghost': !!this.selectedPiece
93 && this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
94 },
95 attrs: {
96 src: "/images/pieces/" +
97 VariantRules.getPpath(this.vr.board[ci][cj]) + ".svg",
98 },
99 }
100 )
101 );
102 }
103 if (this.hints && hintSquares[ci][cj])
104 {
105 elems.push(
106 h(
107 'img',
108 {
109 'class': {
110 'mark-square': true,
111 },
112 attrs: {
113 src: "/images/mark.svg",
114 },
115 }
116 )
117 );
118 }
119 return h(
120 'div',
121 {
122 'class': {
123 'board': true,
124 ['board'+sizeY]: true,
125 'light-square': (i+j)%2==0,
126 'dark-square': (i+j)%2==1,
127 [this.bcolor]: true,
128 'in-shadow': variant=="Dark" && this.score=="*"
129 && !this.vr.enlightened[this.mycolor][ci][cj],
130 'highlight': showLight && !!lm && _.isMatch(lm.end, {x:ci,y:cj}),
131 'incheck': showLight && incheckSq[ci][cj],
132 },
133 attrs: {
134 id: this.getSquareId({x:ci,y:cj}),
135 },
136 },
137 elems
138 );
139 })
140 );
141 }), choices]
142 );
143 if (!!this.vr.reserve)
144 {
145 const shiftIdx = (this.mycolor=="w" ? 0 : 1);
146 let myReservePiecesArray = [];
147 for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
148 {
149 myReservePiecesArray.push(h('div',
150 {
151 'class': {'board':true, ['board'+sizeY]:true},
152 attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
153 },
154 [
155 h('img',
156 {
157 'class': {"piece":true, "reserve":true},
158 attrs: {
159 "src": "/images/pieces/" +
160 this.vr.getReservePpath(this.mycolor,i) + ".svg",
161 }
162 }),
163 h('sup',
164 {"class": { "reserve-count": true } },
165 [ this.vr.reserve[this.mycolor][VariantRules.RESERVE_PIECES[i]] ]
166 )
167 ]));
168 }
169 let oppReservePiecesArray = [];
170 const oppCol = this.vr.getOppCol(this.mycolor);
171 for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
172 {
173 oppReservePiecesArray.push(h('div',
174 {
175 'class': {'board':true, ['board'+sizeY]:true},
176 attrs: { id: this.getSquareId({x:sizeX+(1-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(oppCol,i) + ".svg",
185 }
186 }),
187 h('sup',
188 {"class": { "reserve-count": true } },
189 [ this.vr.reserve[oppCol][VariantRules.RESERVE_PIECES[i]] ]
190 )
191 ]));
192 }
193 let reserves = h('div',
194 {
195 'class':{
196 'game': true,
197 "reserve-div": true,
198 },
199 },
200 [
201 h('div',
202 {
203 'class': {
204 'row': true,
205 "reserve-row-1": true,
206 },
207 },
208 myReservePiecesArray
209 ),
210 h('div',
211 { 'class': { 'row': true }},
212 oppReservePiecesArray
213 )
214 ]
215 );
216 elementArray.push(reserves);
217 }
218 // Show current FEN (just below board, lower right corner)
219 // (if mode != Dark ...)
220 elementArray.push(
221 h('div',
222 {
223 attrs: { id: "fen-div" },
224 "class": { "section-content": true },
225 },
226 [
227 h('p',
228 {
229 attrs: { id: "fen-string" },
230 domProps: { innerHTML: this.vr.getBaseFen() },
231 "class": { "text-center": true },
232 }
233 )
234 ]
235 )
236 );
237 on: {
238 mousedown: this.mousedown,
239 mousemove: this.mousemove,
240 mouseup: this.mouseup,
241 touchstart: this.mousedown,
242 touchmove: this.mousemove,
243 touchend: this.mouseup,
244 },
245
246
247 // TODO: "chessground-like" component
248 // Get the identifier of a HTML table cell from its numeric coordinates o.x,o.y.
249 getSquareId: function(o) {
250 // NOTE: a separator is required to allow any size of board
251 return "sq-" + o.x + "-" + o.y;
252 },
253 // Inverse function
254 getSquareFromId: function(id) {
255 let idParts = id.split('-');
256 return [parseInt(idParts[1]), parseInt(idParts[2])];
257 },
258 mousedown: function(e) {
259 e = e || window.event;
260 let ingame = false;
261 let elem = e.target;
262 while (!ingame && elem !== null)
263 {
264 if (elem.classList.contains("game"))
265 {
266 ingame = true;
267 break;
268 }
269 elem = elem.parentElement;
270 }
271 if (!ingame) //let default behavior (click on button...)
272 return;
273 e.preventDefault(); //disable native drag & drop
274 if (!this.selectedPiece && e.target.classList.contains("piece"))
275 {
276 // Next few lines to center the piece on mouse cursor
277 let rect = e.target.parentNode.getBoundingClientRect();
278 this.start = {
279 x: rect.x + rect.width/2,
280 y: rect.y + rect.width/2,
281 id: e.target.parentNode.id
282 };
283 this.selectedPiece = e.target.cloneNode();
284 this.selectedPiece.style.position = "absolute";
285 this.selectedPiece.style.top = 0;
286 this.selectedPiece.style.display = "inline-block";
287 this.selectedPiece.style.zIndex = 3000;
288 const startSquare = this.getSquareFromId(e.target.parentNode.id);
289 this.possibleMoves = [];
290 if (this.score == "*")
291 {
292 const color = ["friend","problem"].includes(this.mode)
293 ? this.vr.turn
294 : this.mycolor;
295 if (this.vr.canIplay(color,startSquare))
296 this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
297 }
298 // Next line add moving piece just after current image
299 // (required for Crazyhouse reserve)
300 e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling);
301 }
302 },
303 mousemove: function(e) {
304 if (!this.selectedPiece)
305 return;
306 e = e || window.event;
307 // If there is an active element, move it around
308 if (!!this.selectedPiece)
309 {
310 const [offsetX,offsetY] = !!e.clientX
311 ? [e.clientX,e.clientY] //desktop browser
312 : [e.changedTouches[0].pageX, e.changedTouches[0].pageY]; //smartphone
313 this.selectedPiece.style.left = (offsetX-this.start.x) + "px";
314 this.selectedPiece.style.top = (offsetY-this.start.y) + "px";
315 }
316 },
317 mouseup: function(e) {
318 if (!this.selectedPiece)
319 return;
320 e = e || window.event;
321 // Read drop target (or parentElement, parentNode... if type == "img")
322 this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords
323 const [offsetX,offsetY] = !!e.clientX
324 ? [e.clientX,e.clientY]
325 : [e.changedTouches[0].pageX, e.changedTouches[0].pageY];
326 let landing = document.elementFromPoint(offsetX, offsetY);
327 this.selectedPiece.style.zIndex = 3000;
328 // Next condition: classList.contains(piece) fails because of marks
329 while (landing.tagName == "IMG")
330 landing = landing.parentNode;
331 if (this.start.id == landing.id)
332 {
333 // A click: selectedPiece and possibleMoves are already filled
334 return;
335 }
336 // OK: process move attempt
337 let endSquare = this.getSquareFromId(landing.id);
338 let moves = this.findMatchingMoves(endSquare);
339 this.possibleMoves = [];
340 if (moves.length > 1)
341 this.choices = moves;
342 else if (moves.length==1)
343 this.play(moves[0]);
344 // Else: impossible move
345 this.selectedPiece.parentNode.removeChild(this.selectedPiece);
346 delete this.selectedPiece;
347 this.selectedPiece = null;
348 },
349 findMatchingMoves: function(endSquare) {
350 // Run through moves list and return the matching set (if promotions...)
351 let moves = [];
352 this.possibleMoves.forEach(function(m) {
353 if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
354 moves.push(m);
355 });
356 return moves;
357 },
358 animateMove: function(move) {
359 let startSquare = document.getElementById(this.getSquareId(move.start));
360 let endSquare = document.getElementById(this.getSquareId(move.end));
361 let rectStart = startSquare.getBoundingClientRect();
362 let rectEnd = endSquare.getBoundingClientRect();
363 let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
364 let movingPiece =
365 document.querySelector("#" + this.getSquareId(move.start) + " > img.piece");
366 // HACK for animation (with positive translate, image slides "under background")
367 // Possible improvement: just alter squares on the piece's way...
368 squares = document.getElementsByClassName("board");
369 for (let i=0; i<squares.length; i++)
370 {
371 let square = squares.item(i);
372 if (square.id != this.getSquareId(move.start))
373 square.style.zIndex = "-1";
374 }
375 movingPiece.style.transform = "translate(" + translation.x + "px," +
376 translation.y + "px)";
377 movingPiece.style.transitionDuration = "0.2s";
378 movingPiece.style.zIndex = "3000";
379 setTimeout( () => {
380 for (let i=0; i<squares.length; i++)
381 squares.item(i).style.zIndex = "auto";
382 movingPiece.style = {}; //required e.g. for 0-0 with KR swap
383 this.play(move);
384 }, 250);
385 },