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