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