First draft of Hex game
[xogo.git] / base_rules.js
CommitLineData
41534b92
BA
1import { Random } from "/utils/alea.js";
2import { ArrayFun } from "/utils/array.js";
3import PiPo from "/utils/PiPo.js";
4import Move from "/utils/Move.js";
5
6// NOTE: x coords: top to bottom (white perspective); y: left to right
cc2c7183 7// NOTE: ChessRules is aliased as window.C, and variants as window.V
41534b92
BA
8export default class ChessRules {
9
e5f93427 10 static get Aliases() {
3caec36f 11 return {'C': ChessRules};
e5f93427
BA
12 }
13
41534b92
BA
14 /////////////////////////
15 // VARIANT SPECIFICATIONS
16
17 // Some variants have specific options, like the number of pawns in Monster,
18 // or the board size for Pandemonium.
19 // Users can generally select a randomness level from 0 to 2.
20 static get Options() {
21 return {
41534b92
BA
22 select: [{
23 label: "Randomness",
24 variable: "randomness",
25 defaut: 0,
26 options: [
b4ae3ff6
BA
27 {label: "Deterministic", value: 0},
28 {label: "Symmetric random", value: 1},
29 {label: "Asymmetric random", value: 2}
41534b92
BA
30 ]
31 }],
f8b43ef7
BA
32 check: [
33 {
34 label: "Capture king",
35 defaut: false,
36 variable: "taking"
37 },
38 {
39 label: "Falling pawn",
40 defaut: false,
41 variable: "pawnfall"
42 }
43 ],
41534b92
BA
44 // Game modifiers (using "elementary variants"). Default: false
45 styles: [
46 "atomic",
47 "balance", //takes precedence over doublemove & progressive
48 "cannibal",
49 "capture",
50 "crazyhouse",
51 "cylinder", //ok with all
52 "dark",
53 "doublemove",
54 "madrasi",
55 "progressive", //(natural) priority over doublemove
56 "recycle",
57 "rifle",
58 "teleport",
59 "zen"
60 ]
61 };
62 }
63
c9ab0340
BA
64 get pawnPromotions() {
65 return ['q', 'r', 'n', 'b'];
41534b92
BA
66 }
67
68 // Some variants don't have flags:
69 get hasFlags() {
70 return true;
71 }
72 // Or castle
73 get hasCastle() {
74 return this.hasFlags;
75 }
76
77 // En-passant captures allowed?
78 get hasEnpassant() {
79 return true;
80 }
81
82 get hasReserve() {
83 return (
84 !!this.options["crazyhouse"] ||
85 (!!this.options["recycle"] && !this.options["teleport"])
86 );
87 }
88
89 get noAnimate() {
90 return !!this.options["dark"];
91 }
92
93 // Some variants use click infos:
15106e82
BA
94 doClick(coords) {
95 if (typeof coords.x != "number")
b4ae3ff6 96 return null; //click on reserves
41534b92 97 if (
cc2c7183 98 this.options["teleport"] && this.subTurnTeleport == 2 &&
15106e82 99 this.board[coords.x][coords.y] == ""
41534b92 100 ) {
1a7c0492 101 let res = new Move({
41534b92
BA
102 start: {x: this.captured.x, y: this.captured.y},
103 appear: [
104 new PiPo({
15106e82
BA
105 x: coords.x,
106 y: coords.y,
41534b92
BA
107 c: this.captured.c, //this.turn,
108 p: this.captured.p
109 })
110 ],
1a7c0492 111 vanish: []
41534b92 112 });
1a7c0492
BA
113 res.drag = {c: this.captured.c, p: this.captured.p};
114 return res;
41534b92
BA
115 }
116 return null;
117 }
118
119 ////////////////////
120 // COORDINATES UTILS
121
4bff03f5 122 // 3a --> {x:3, y:10}
41534b92 123 static SquareToCoords(sq) {
15106e82
BA
124 return ArrayFun.toObject(["x", "y"],
125 [0, 1].map(i => parseInt(sq[i], 36)));
41534b92
BA
126 }
127
4bff03f5 128 // {x:11, y:12} --> bc
15106e82
BA
129 static CoordsToSquare(cd) {
130 return Object.values(cd).map(c => c.toString(36)).join("");
41534b92
BA
131 }
132
15106e82
BA
133 coordsToId(cd) {
134 if (typeof cd.x == "number") {
135 return (
136 `${this.containerId}|sq-${cd.x.toString(36)}-${cd.y.toString(36)}`
137 );
138 }
41534b92 139 // Reserve :
15106e82 140 return `${this.containerId}|rsq-${cd.x}-${cd.y}`;
41534b92
BA
141 }
142
143 idToCoords(targetId) {
b4ae3ff6
BA
144 if (!targetId)
145 return null; //outside page, maybe...
41534b92
BA
146 const idParts = targetId.split('|'); //prefix|sq-2-3 (start at 0 => 3,4)
147 if (
148 idParts.length < 2 ||
149 idParts[0] != this.containerId ||
150 !idParts[1].match(/sq-[0-9a-zA-Z]-[0-9a-zA-Z]/)
151 ) {
152 return null;
153 }
154 const squares = idParts[1].split('-');
155 if (squares[0] == "sq")
15106e82
BA
156 return {x: parseInt(squares[1], 36), y: parseInt(squares[2], 36)};
157 // squares[0] == "rsq" : reserve, 'c' + 'p' (letters color & piece)
158 return {x: squares[1], y: squares[2]};
41534b92
BA
159 }
160
161 /////////////
162 // FEN UTILS
163
164 // Turn "wb" into "B" (for FEN)
165 board2fen(b) {
4bff03f5 166 return (b[0] == "w" ? b[1].toUpperCase() : b[1]);
41534b92
BA
167 }
168
169 // Turn "p" into "bp" (for board)
170 fen2board(f) {
4bff03f5 171 return (f.charCodeAt(0) <= 90 ? "w" + f.toLowerCase() : "b" + f);
41534b92
BA
172 }
173
174 // Setup the initial random-or-not (asymmetric-or-not) position
175 genRandInitFen(seed) {
176 Random.setSeed(seed);
177
178 let fen, flags = "0707";
cc2c7183 179 if (!this.options.randomness)
41534b92
BA
180 // Deterministic:
181 fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0";
182
183 else {
184 // Randomize
185 let pieces = { w: new Array(8), b: new Array(8) };
186 flags = "";
187 // Shuffle pieces on first (and last rank if randomness == 2)
188 for (let c of ["w", "b"]) {
189 if (c == 'b' && this.options.randomness == 1) {
190 pieces['b'] = pieces['w'];
191 flags += flags;
192 break;
193 }
194
195 let positions = ArrayFun.range(8);
196
197 // Get random squares for bishops
198 let randIndex = 2 * Random.randInt(4);
199 const bishop1Pos = positions[randIndex];
200 // The second bishop must be on a square of different color
201 let randIndex_tmp = 2 * Random.randInt(4) + 1;
202 const bishop2Pos = positions[randIndex_tmp];
203 // Remove chosen squares
204 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
205 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
206
207 // Get random squares for knights
208 randIndex = Random.randInt(6);
209 const knight1Pos = positions[randIndex];
210 positions.splice(randIndex, 1);
211 randIndex = Random.randInt(5);
212 const knight2Pos = positions[randIndex];
213 positions.splice(randIndex, 1);
214
215 // Get random square for queen
216 randIndex = Random.randInt(4);
217 const queenPos = positions[randIndex];
218 positions.splice(randIndex, 1);
219
220 // Rooks and king positions are now fixed,
221 // because of the ordering rook-king-rook
222 const rook1Pos = positions[0];
223 const kingPos = positions[1];
224 const rook2Pos = positions[2];
225
226 // Finally put the shuffled pieces in the board array
227 pieces[c][rook1Pos] = "r";
228 pieces[c][knight1Pos] = "n";
229 pieces[c][bishop1Pos] = "b";
230 pieces[c][queenPos] = "q";
231 pieces[c][kingPos] = "k";
232 pieces[c][bishop2Pos] = "b";
233 pieces[c][knight2Pos] = "n";
234 pieces[c][rook2Pos] = "r";
235 flags += rook1Pos.toString() + rook2Pos.toString();
236 }
237 fen = (
238 pieces["b"].join("") +
239 "/pppppppp/8/8/8/8/PPPPPPPP/" +
240 pieces["w"].join("").toUpperCase() +
241 " w 0"
242 );
243 }
244 // Add turn + flags + enpassant (+ reserve)
245 let parts = [];
b4ae3ff6
BA
246 if (this.hasFlags)
247 parts.push(`"flags":"${flags}"`);
248 if (this.hasEnpassant)
249 parts.push('"enpassant":"-"');
250 if (this.hasReserve)
251 parts.push('"reserve":"000000000000"');
252 if (this.options["crazyhouse"])
253 parts.push('"ispawn":"-"');
254 if (parts.length >= 1)
255 fen += " {" + parts.join(",") + "}";
41534b92
BA
256 return fen;
257 }
258
259 // "Parse" FEN: just return untransformed string data
260 parseFen(fen) {
261 const fenParts = fen.split(" ");
262 let res = {
263 position: fenParts[0],
264 turn: fenParts[1],
265 movesCount: fenParts[2]
266 };
b4ae3ff6
BA
267 if (fenParts.length > 3)
268 res = Object.assign(res, JSON.parse(fenParts[3]));
41534b92
BA
269 return res;
270 }
271
272 // Return current fen (game state)
273 getFen() {
274 let fen = (
15106e82 275 this.getPosition() + " " +
41534b92
BA
276 this.getTurnFen() + " " +
277 this.movesCount
278 );
279 let parts = [];
b4ae3ff6
BA
280 if (this.hasFlags)
281 parts.push(`"flags":"${this.getFlagsFen()}"`);
41534b92
BA
282 if (this.hasEnpassant)
283 parts.push(`"enpassant":"${this.getEnpassantFen()}"`);
b4ae3ff6
BA
284 if (this.hasReserve)
285 parts.push(`"reserve":"${this.getReserveFen()}"`);
41534b92
BA
286 if (this.options["crazyhouse"])
287 parts.push(`"ispawn":"${this.getIspawnFen()}"`);
b4ae3ff6
BA
288 if (parts.length >= 1)
289 fen += " {" + parts.join(",") + "}";
41534b92
BA
290 return fen;
291 }
292
d621e620
BA
293 static FenEmptySquares(count) {
294 // if more than 9 consecutive free spaces, break the integer,
295 // otherwise FEN parsing will fail.
296 if (count <= 9)
297 return count;
298 // Most boards of size < 18:
299 if (count <= 18)
300 return "9" + (count - 9);
301 // Except Gomoku:
302 return "99" + (count - 18);
303 }
304
41534b92 305 // Position part of the FEN string
15106e82 306 getPosition() {
41534b92
BA
307 let position = "";
308 for (let i = 0; i < this.size.y; i++) {
309 let emptyCount = 0;
310 for (let j = 0; j < this.size.x; j++) {
b4ae3ff6
BA
311 if (this.board[i][j] == "")
312 emptyCount++;
41534b92
BA
313 else {
314 if (emptyCount > 0) {
315 // Add empty squares in-between
d621e620 316 position += C.FenEmptySquares(emptyCount);
41534b92
BA
317 emptyCount = 0;
318 }
319 position += this.board2fen(this.board[i][j]);
320 }
321 }
322 if (emptyCount > 0)
323 // "Flush remainder"
d621e620 324 position += C.FenEmptySquares(emptyCount);
b4ae3ff6
BA
325 if (i < this.size.y - 1)
326 position += "/"; //separate rows
41534b92
BA
327 }
328 return position;
329 }
330
331 getTurnFen() {
332 return this.turn;
333 }
334
335 // Flags part of the FEN string
336 getFlagsFen() {
337 return ["w", "b"].map(c => {
15106e82 338 return this.castleFlags[c].map(x => x.toString(36)).join("");
41534b92
BA
339 }).join("");
340 }
341
342 // Enpassant part of the FEN string
343 getEnpassantFen() {
b4ae3ff6
BA
344 if (!this.epSquare)
345 return "-"; //no en-passant
cc2c7183 346 return C.CoordsToSquare(this.epSquare);
41534b92
BA
347 }
348
349 getReserveFen() {
350 return (
351 ["w","b"].map(c => Object.values(this.reserve[c]).join("")).join("")
352 );
353 }
354
355 getIspawnFen() {
15106e82
BA
356 const squares = Object.keys(this.ispawn);
357 if (squares.length == 0)
b4ae3ff6 358 return "-";
15106e82 359 return squares.join(",");
41534b92
BA
360 }
361
362 // Set flags from fen (castle: white a,h then black a,h)
363 setFlags(fenflags) {
364 this.castleFlags = {
15106e82
BA
365 w: [0, 1].map(i => parseInt(fenflags.charAt(i), 36)),
366 b: [2, 3].map(i => parseInt(fenflags.charAt(i), 36))
41534b92
BA
367 };
368 }
369
370 //////////////////
371 // INITIALIZATION
372
41534b92
BA
373 constructor(o) {
374 this.options = o.options;
375 this.playerColor = o.color;
15106e82 376 this.afterPlay = o.afterPlay; //trigger some actions after playing a move
41534b92 377
c9ab0340 378 // Fen string fully describes the game state
b4ae3ff6
BA
379 if (!o.fen)
380 o.fen = this.genRandInitFen(o.seed);
41534b92
BA
381 const fenParsed = this.parseFen(o.fen);
382 this.board = this.getBoard(fenParsed.position);
383 this.turn = fenParsed.turn;
384 this.movesCount = parseInt(fenParsed.movesCount, 10);
385 this.setOtherVariables(fenParsed);
386
387 // Graphical (can use variables defined above)
388 this.containerId = o.element;
389 this.graphicalInit();
390 }
391
392 // Turn position fen into double array ["wb","wp","bk",...]
393 getBoard(position) {
394 const rows = position.split("/");
395 let board = ArrayFun.init(this.size.x, this.size.y, "");
396 for (let i = 0; i < rows.length; i++) {
397 let j = 0;
398 for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
399 const character = rows[i][indexInRow];
400 const num = parseInt(character, 10);
401 // If num is a number, just shift j:
b4ae3ff6
BA
402 if (!isNaN(num))
403 j += num;
41534b92 404 // Else: something at position i,j
b4ae3ff6
BA
405 else
406 board[i][j++] = this.fen2board(character);
41534b92
BA
407 }
408 }
409 return board;
410 }
411
412 // Some additional variables from FEN (variant dependant)
413 setOtherVariables(fenParsed) {
414 // Set flags and enpassant:
b4ae3ff6
BA
415 if (this.hasFlags)
416 this.setFlags(fenParsed.flags);
41534b92
BA
417 if (this.hasEnpassant)
418 this.epSquare = this.getEpSquare(fenParsed.enpassant);
b4ae3ff6
BA
419 if (this.hasReserve)
420 this.initReserves(fenParsed.reserve);
421 if (this.options["crazyhouse"])
422 this.initIspawn(fenParsed.ispawn);
41534b92 423 this.subTurn = 1; //may be unused
cc2c7183
BA
424 if (this.options["teleport"]) {
425 this.subTurnTeleport = 1;
426 this.captured = null;
427 }
41534b92 428 if (this.options["dark"]) {
41534b92 429 // Setup enlightened: squares reachable by player side
c9ab0340
BA
430 this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
431 this.updateEnlightened();
41534b92
BA
432 }
433 }
434
c9ab0340
BA
435 updateEnlightened() {
436 this.oldEnlightened = this.enlightened;
437 this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
41534b92 438 // Add pieces positions + all squares reachable by moves (includes Zen):
41534b92
BA
439 for (let x=0; x<this.size.x; x++) {
440 for (let y=0; y<this.size.y; y++) {
441 if (this.board[x][y] != "" && this.getColor(x, y) == this.playerColor)
442 {
c9ab0340 443 this.enlightened[x][y] = true;
41534b92 444 this.getPotentialMovesFrom([x, y]).forEach(m => {
c9ab0340 445 this.enlightened[m.end.x][m.end.y] = true;
41534b92
BA
446 });
447 }
448 }
449 }
b4ae3ff6 450 if (this.epSquare)
c9ab0340 451 this.enlightEnpassant();
41534b92
BA
452 }
453
c9ab0340
BA
454 // Include square of the en-passant capturing square:
455 enlightEnpassant() {
456 // NOTE: shortcut, pawn has only one attack type, doesn't depend on square
457 const steps = this.pieces(this.playerColor)["p"].attack[0].steps;
41534b92
BA
458 for (let step of steps) {
459 const x = this.epSquare.x - step[0],
d262cff4 460 y = this.getY(this.epSquare.y - step[1]);
41534b92
BA
461 if (
462 this.onBoard(x, y) &&
463 this.getColor(x, y) == this.playerColor &&
cc2c7183 464 this.getPieceType(x, y) == "p"
41534b92 465 ) {
c9ab0340 466 this.enlightened[x][this.epSquare.y] = true;
41534b92
BA
467 break;
468 }
469 }
470 }
471
c9ab0340 472 // ordering as in pieces() p,r,n,b,q,k (+ count in base 30 if needed)
41534b92
BA
473 initReserves(reserveStr) {
474 const counts = reserveStr.split("").map(c => parseInt(c, 30));
475 this.reserve = { w: {}, b: {} };
c9ab0340
BA
476 const pieceName = ['p', 'r', 'n', 'b', 'q', 'k'];
477 const L = pieceName.length;
478 for (let i of ArrayFun.range(2 * L)) {
479 if (i < L)
b4ae3ff6
BA
480 this.reserve['w'][pieceName[i]] = counts[i];
481 else
c9ab0340 482 this.reserve['b'][pieceName[i-L]] = counts[i];
41534b92
BA
483 }
484 }
485
486 initIspawn(ispawnStr) {
15106e82
BA
487 if (ispawnStr != "-")
488 this.ispawn = ArrayFun.toObject(ispawnStr.split(","), true);
b4ae3ff6
BA
489 else
490 this.ispawn = {};
41534b92
BA
491 }
492
493 getNbReservePieces(color) {
494 return (
495 Object.values(this.reserve[color]).reduce(
496 (oldV,newV) => oldV + (newV > 0 ? 1 : 0), 0)
497 );
498 }
499
15106e82
BA
500 getRankInReserve(c, p) {
501 const pieces = Object.keys(this.pieces());
502 const lastIndex = pieces.findIndex(pp => pp == p)
503 let toTest = pieces.slice(0, lastIndex);
504 return toTest.reduce(
505 (oldV,newV) => oldV + (this.reserve[c][newV] > 0 ? 1 : 0), 0);
506 }
507
41534b92
BA
508 //////////////
509 // VISUAL PART
510
511 getPieceWidth(rwidth) {
512 return (rwidth / this.size.y);
513 }
514
41534b92 515 getReserveSquareSize(rwidth, nbR) {
15106e82 516 const sqSize = this.getPieceWidth(rwidth);
41534b92
BA
517 return Math.min(sqSize, rwidth / nbR);
518 }
519
520 getReserveNumId(color, piece) {
521 return `${this.containerId}|rnum-${color}${piece}`;
522 }
523
524 graphicalInit() {
525 // NOTE: not window.onresize = this.re_drawBoardElts because scope (this)
526 window.onresize = () => this.re_drawBoardElements();
527 this.re_drawBoardElements();
528 this.initMouseEvents();
3c61449b
BA
529 const chessboard =
530 document.getElementById(this.containerId).querySelector(".chessboard");
531 new ResizeObserver(this.rescale).observe(chessboard);
41534b92
BA
532 }
533
534 re_drawBoardElements() {
535 const board = this.getSvgChessboard();
cc2c7183 536 const oppCol = C.GetOppCol(this.playerColor);
3c61449b
BA
537 let chessboard =
538 document.getElementById(this.containerId).querySelector(".chessboard");
539 chessboard.innerHTML = "";
540 chessboard.insertAdjacentHTML('beforeend', board);
41534b92
BA
541 // Compare window ratio width / height to aspectRatio:
542 const windowRatio = window.innerWidth / window.innerHeight;
543 let cbWidth, cbHeight;
15106e82 544 if (windowRatio <= this.size.ratio) {
41534b92
BA
545 // Limiting dimension is width:
546 cbWidth = Math.min(window.innerWidth, 767);
15106e82 547 cbHeight = cbWidth / this.size.ratio;
41534b92
BA
548 }
549 else {
550 // Limiting dimension is height:
551 cbHeight = Math.min(window.innerHeight, 767);
15106e82 552 cbWidth = cbHeight * this.size.ratio;
41534b92 553 }
1a7c0492 554 if (this.hasReserve) {
41534b92
BA
555 const sqSize = cbWidth / this.size.y;
556 // NOTE: allocate space for reserves (up/down) even if they are empty
15106e82 557 // Cannot use getReserveSquareSize() here, but sqSize is an upper bound.
41534b92
BA
558 if ((window.innerHeight - cbHeight) / 2 < sqSize + 5) {
559 cbHeight = window.innerHeight - 2 * (sqSize + 5);
15106e82 560 cbWidth = cbHeight * this.size.ratio;
41534b92
BA
561 }
562 }
3c61449b
BA
563 chessboard.style.width = cbWidth + "px";
564 chessboard.style.height = cbHeight + "px";
41534b92
BA
565 // Center chessboard:
566 const spaceLeft = (window.innerWidth - cbWidth) / 2,
567 spaceTop = (window.innerHeight - cbHeight) / 2;
3c61449b
BA
568 chessboard.style.left = spaceLeft + "px";
569 chessboard.style.top = spaceTop + "px";
41534b92
BA
570 // Give sizes instead of recomputing them,
571 // because chessboard might not be drawn yet.
572 this.setupPieces({
573 width: cbWidth,
574 height: cbHeight,
575 x: spaceLeft,
576 y: spaceTop
577 });
578 }
579
580 // Get SVG board (background, no pieces)
581 getSvgChessboard() {
41534b92
BA
582 const flipped = (this.playerColor == 'b');
583 let board = `
584 <svg
585 viewBox="0 0 80 80"
3c61449b 586 class="chessboard_SVG">
41534b92 587 <g>`;
728cb1e3
BA
588 for (let i=0; i < this.size.x; i++) {
589 for (let j=0; j < this.size.y; j++) {
41534b92
BA
590 const ii = (flipped ? this.size.x - 1 - i : i);
591 const jj = (flipped ? this.size.y - 1 - j : j);
c7bf7b1b
BA
592 let classes = this.getSquareColorClass(ii, jj);
593 if (this.enlightened && !this.enlightened[ii][jj])
594 classes += " in-shadow";
41534b92 595 // NOTE: x / y reversed because coordinates system is reversed.
c7bf7b1b
BA
596 board += `<rect
597 class="${classes}"
15106e82 598 id="${this.coordsToId({x: ii, y: jj})}"
41534b92
BA
599 width="10"
600 height="10"
601 x="${10*j}"
602 y="${10*i}" />`;
603 }
604 }
605 board += "</g></svg>";
606 return board;
607 }
608
cc2c7183 609 // Generally light square bottom-right
15106e82
BA
610 getSquareColorClass(x, y) {
611 return ((x+y) % 2 == 0 ? "light-square": "dark-square");
41534b92
BA
612 }
613
614 setupPieces(r) {
615 if (this.g_pieces) {
616 // Refreshing: delete old pieces first
617 for (let i=0; i<this.size.x; i++) {
618 for (let j=0; j<this.size.y; j++) {
619 if (this.g_pieces[i][j]) {
620 this.g_pieces[i][j].remove();
621 this.g_pieces[i][j] = null;
622 }
623 }
624 }
625 }
b4ae3ff6
BA
626 else
627 this.g_pieces = ArrayFun.init(this.size.x, this.size.y, null);
3c61449b
BA
628 let chessboard =
629 document.getElementById(this.containerId).querySelector(".chessboard");
b4ae3ff6
BA
630 if (!r)
631 r = chessboard.getBoundingClientRect();
41534b92
BA
632 const pieceWidth = this.getPieceWidth(r.width);
633 for (let i=0; i < this.size.x; i++) {
634 for (let j=0; j < this.size.y; j++) {
c9ab0340 635 if (this.board[i][j] != "") {
41534b92 636 const color = this.getColor(i, j);
cc2c7183 637 const piece = this.getPiece(i, j);
41534b92
BA
638 this.g_pieces[i][j] = document.createElement("piece");
639 this.g_pieces[i][j].classList.add(this.pieces()[piece]["class"]);
15106e82 640 this.g_pieces[i][j].classList.add(C.GetColorClass(color));
41534b92
BA
641 this.g_pieces[i][j].style.width = pieceWidth + "px";
642 this.g_pieces[i][j].style.height = pieceWidth + "px";
9db5050a
BA
643 let [ip, jp] = this.getPixelPosition(i, j, r);
644 // Translate coordinates to use chessboard as reference:
645 this.g_pieces[i][j].style.transform =
646 `translate(${ip - r.x}px,${jp - r.y}px)`;
c9ab0340
BA
647 if (this.enlightened && !this.enlightened[i][j])
648 this.g_pieces[i][j].classList.add("hidden");
3c61449b 649 chessboard.appendChild(this.g_pieces[i][j]);
41534b92
BA
650 }
651 }
652 }
1a7c0492 653 if (this.hasReserve)
b4ae3ff6 654 this.re_drawReserve(['w', 'b'], r);
41534b92
BA
655 }
656
657 // NOTE: assume !!this.reserve
658 re_drawReserve(colors, r) {
659 if (this.r_pieces) {
660 // Remove (old) reserve pieces
661 for (let c of colors) {
b4ae3ff6
BA
662 if (!this.reserve[c])
663 continue;
41534b92
BA
664 Object.keys(this.reserve[c]).forEach(p => {
665 if (this.r_pieces[c][p]) {
666 this.r_pieces[c][p].remove();
667 delete this.r_pieces[c][p];
668 const numId = this.getReserveNumId(c, p);
669 document.getElementById(numId).remove();
670 }
671 });
672 let reservesDiv = document.getElementById("reserves_" + c);
b4ae3ff6
BA
673 if (reservesDiv)
674 reservesDiv.remove();
41534b92
BA
675 }
676 }
b4ae3ff6 677 else
9db5050a
BA
678 this.r_pieces = { w: {}, b: {} };
679 let container = document.getElementById(this.containerId);
b4ae3ff6 680 if (!r)
9db5050a 681 r = container.querySelector(".chessboard").getBoundingClientRect();
41534b92 682 for (let c of colors) {
b4ae3ff6
BA
683 if (!this.reserve[c])
684 continue;
41534b92 685 const nbR = this.getNbReservePieces(c);
b4ae3ff6
BA
686 if (nbR == 0)
687 continue;
41534b92
BA
688 const sqResSize = this.getReserveSquareSize(r.width, nbR);
689 let ridx = 0;
690 const vShift = (c == this.playerColor ? r.height + 5 : -sqResSize - 5);
691 const [i0, j0] = [r.x, r.y + vShift];
692 let rcontainer = document.createElement("div");
693 rcontainer.id = "reserves_" + c;
694 rcontainer.classList.add("reserves");
695 rcontainer.style.left = i0 + "px";
696 rcontainer.style.top = j0 + "px";
1aa9054d
BA
697 // NOTE: +1 fix display bug on Firefox at least
698 rcontainer.style.width = (nbR * sqResSize + 1) + "px";
41534b92 699 rcontainer.style.height = sqResSize + "px";
9db5050a 700 container.appendChild(rcontainer);
41534b92 701 for (let p of Object.keys(this.reserve[c])) {
b4ae3ff6
BA
702 if (this.reserve[c][p] == 0)
703 continue;
41534b92 704 let r_cell = document.createElement("div");
15106e82 705 r_cell.id = this.coordsToId({x: c, y: p});
41534b92 706 r_cell.classList.add("reserve-cell");
1aa9054d
BA
707 r_cell.style.width = sqResSize + "px";
708 r_cell.style.height = sqResSize + "px";
41534b92
BA
709 rcontainer.appendChild(r_cell);
710 let piece = document.createElement("piece");
c9ab0340 711 const pieceSpec = this.pieces()[p];
41534b92 712 piece.classList.add(pieceSpec["class"]);
15106e82 713 piece.classList.add(C.GetColorClass(c));
41534b92
BA
714 piece.style.width = "100%";
715 piece.style.height = "100%";
716 this.r_pieces[c][p] = piece;
717 r_cell.appendChild(piece);
718 let number = document.createElement("div");
719 number.textContent = this.reserve[c][p];
720 number.classList.add("reserve-num");
721 number.id = this.getReserveNumId(c, p);
722 const fontSize = "1.3em";
723 number.style.fontSize = fontSize;
724 number.style.fontSize = fontSize;
725 r_cell.appendChild(number);
726 ridx++;
727 }
728 }
729 }
730
731 updateReserve(color, piece, count) {
55a15dcb 732 if (this.options["cannibal"] && C.CannibalKings[piece])
cc2c7183 733 piece = "k"; //capturing cannibal king: back to king form
41534b92
BA
734 const oldCount = this.reserve[color][piece];
735 this.reserve[color][piece] = count;
736 // Redrawing is much easier if count==0
b4ae3ff6
BA
737 if ([oldCount, count].includes(0))
738 this.re_drawReserve([color]);
41534b92
BA
739 else {
740 const numId = this.getReserveNumId(color, piece);
741 document.getElementById(numId).textContent = count;
742 }
743 }
744
15106e82
BA
745 // Apply diff this.enlightened --> oldEnlightened on board
746 graphUpdateEnlightened() {
747 let chessboard =
748 document.getElementById(this.containerId).querySelector(".chessboard");
749 const r = chessboard.getBoundingClientRect();
750 const pieceWidth = this.getPieceWidth(r.width);
751 for (let x=0; x<this.size.x; x++) {
752 for (let y=0; y<this.size.y; y++) {
753 if (!this.enlightened[x][y] && this.oldEnlightened[x][y]) {
6997e386 754 let elt = document.getElementById(this.coordsToId({x: x, y: y}));
15106e82
BA
755 elt.classList.add("in-shadow");
756 if (this.g_pieces[x][y])
757 this.g_pieces[x][y].classList.add("hidden");
758 }
759 else if (this.enlightened[x][y] && !this.oldEnlightened[x][y]) {
6997e386 760 let elt = document.getElementById(this.coordsToId({x: x, y: y}));
15106e82
BA
761 elt.classList.remove("in-shadow");
762 if (this.g_pieces[x][y])
763 this.g_pieces[x][y].classList.remove("hidden");
764 }
765 }
766 }
767 }
768
41534b92
BA
769 // After resize event: no need to destroy/recreate pieces
770 rescale() {
3c61449b 771 const container = document.getElementById(this.containerId);
b4ae3ff6
BA
772 if (!container)
773 return; //useful at initial loading
3c61449b
BA
774 let chessboard = container.querySelector(".chessboard");
775 const r = chessboard.getBoundingClientRect();
41534b92 776 const newRatio = r.width / r.height;
41534b92
BA
777 let newWidth = r.width,
778 newHeight = r.height;
15106e82
BA
779 if (newRatio > this.size.ratio) {
780 newWidth = r.height * this.size.ratio;
3c61449b 781 chessboard.style.width = newWidth + "px";
41534b92 782 }
15106e82
BA
783 else if (newRatio < this.size.ratio) {
784 newHeight = r.width / this.size.ratio;
3c61449b 785 chessboard.style.height = newHeight + "px";
41534b92
BA
786 }
787 const newX = (window.innerWidth - newWidth) / 2;
3c61449b 788 chessboard.style.left = newX + "px";
41534b92 789 const newY = (window.innerHeight - newHeight) / 2;
3c61449b 790 chessboard.style.top = newY + "px";
9db5050a 791 const newR = {x: newX, y: newY, width: newWidth, height: newHeight};
41534b92 792 const pieceWidth = this.getPieceWidth(newWidth);
d621e620
BA
793 // NOTE: next "if" for variants which use squares filling
794 // instead of "physical", moving pieces
795 if (this.g_pieces) {
796 for (let i=0; i < this.size.x; i++) {
797 for (let j=0; j < this.size.y; j++) {
798 if (this.g_pieces[i][j]) {
799 // NOTE: could also use CSS transform "scale"
800 this.g_pieces[i][j].style.width = pieceWidth + "px";
801 this.g_pieces[i][j].style.height = pieceWidth + "px";
802 const [ip, jp] = this.getPixelPosition(i, j, newR);
803 // Translate coordinates to use chessboard as reference:
804 this.g_pieces[i][j].style.transform =
805 `translate(${ip - newX}px,${jp - newY}px)`;
806 }
41534b92
BA
807 }
808 }
809 }
1a7c0492 810 if (this.hasReserve)
b4ae3ff6 811 this.rescaleReserve(newR);
41534b92
BA
812 }
813
814 rescaleReserve(r) {
41534b92 815 for (let c of ['w','b']) {
b4ae3ff6
BA
816 if (!this.reserve[c])
817 continue;
41534b92 818 const nbR = this.getNbReservePieces(c);
b4ae3ff6
BA
819 if (nbR == 0)
820 continue;
41534b92
BA
821 // Resize container first
822 const sqResSize = this.getReserveSquareSize(r.width, nbR);
823 const vShift = (c == this.playerColor ? r.height + 5 : -sqResSize - 5);
824 const [i0, j0] = [r.x, r.y + vShift];
825 let rcontainer = document.getElementById("reserves_" + c);
826 rcontainer.style.left = i0 + "px";
827 rcontainer.style.top = j0 + "px";
1aa9054d 828 rcontainer.style.width = (nbR * sqResSize + 1) + "px";
41534b92
BA
829 rcontainer.style.height = sqResSize + "px";
830 // And then reserve cells:
831 const rpieceWidth = this.getReserveSquareSize(r.width, nbR);
832 Object.keys(this.reserve[c]).forEach(p => {
b4ae3ff6
BA
833 if (this.reserve[c][p] == 0)
834 return;
15106e82 835 let r_cell = document.getElementById(this.coordsToId({x: c, y: p}));
1aa9054d
BA
836 r_cell.style.width = sqResSize + "px";
837 r_cell.style.height = sqResSize + "px";
41534b92
BA
838 });
839 }
840 }
841
9db5050a 842 // Return the absolute pixel coordinates given current position.
41534b92
BA
843 // Our coordinate system differs from CSS one (x <--> y).
844 // We return here the CSS coordinates (more useful).
845 getPixelPosition(i, j, r) {
b4ae3ff6
BA
846 if (i < 0 || j < 0)
847 return [0, 0]; //piece vanishes
15106e82
BA
848 let x, y;
849 if (typeof i == "string") {
850 // Reserves: need to know the rank of piece
851 const nbR = this.getNbReservePieces(i);
852 const rsqSize = this.getReserveSquareSize(r.width, nbR);
853 x = this.getRankInReserve(i, j) * rsqSize;
854 y = (this.playerColor == i ? y = r.height + 5 : - 5 - rsqSize);
855 }
856 else {
857 const sqSize = r.width / this.size.y;
858 const flipped = (this.playerColor == 'b');
859 x = (flipped ? this.size.y - 1 - j : j) * sqSize;
860 y = (flipped ? this.size.x - 1 - i : i) * sqSize;
861 }
9db5050a 862 return [r.x + x, r.y + y];
41534b92
BA
863 }
864
865 initMouseEvents() {
9db5050a
BA
866 let container = document.getElementById(this.containerId);
867 let chessboard = container.querySelector(".chessboard");
41534b92
BA
868
869 const getOffset = e => {
3c61449b
BA
870 if (e.clientX)
871 // Mouse
872 return {x: e.clientX, y: e.clientY};
41534b92
BA
873 let touchLocation = null;
874 if (e.targetTouches && e.targetTouches.length >= 1)
875 // Touch screen, dragstart
876 touchLocation = e.targetTouches[0];
877 else if (e.changedTouches && e.changedTouches.length >= 1)
878 // Touch screen, dragend
879 touchLocation = e.changedTouches[0];
880 if (touchLocation)
11625344 881 return {x: touchLocation.clientX, y: touchLocation.clientY};
57b8015b 882 return {x: 0, y: 0}; //shouldn't reach here =)
41534b92
BA
883 }
884
885 const centerOnCursor = (piece, e) => {
15106e82 886 const centerShift = this.getPieceWidth(r.width) / 2;
41534b92 887 const offset = getOffset(e);
9db5050a
BA
888 piece.style.left = (offset.x - centerShift) + "px";
889 piece.style.top = (offset.y - centerShift) + "px";
41534b92
BA
890 }
891
892 let start = null,
893 r = null,
894 startPiece, curPiece = null,
15106e82 895 pieceWidth;
41534b92 896 const mousedown = (e) => {
cb17fed8 897 // Disable zoom on smartphones:
b4ae3ff6
BA
898 if (e.touches && e.touches.length > 1)
899 e.preventDefault();
3c61449b 900 r = chessboard.getBoundingClientRect();
15106e82
BA
901 pieceWidth = this.getPieceWidth(r.width);
902 const cd = this.idToCoords(e.target.id);
903 if (cd) {
904 const move = this.doClick(cd);
b4ae3ff6
BA
905 if (move)
906 this.playPlusVisual(move);
41534b92 907 else {
15106e82
BA
908 const [x, y] = Object.values(cd);
909 if (typeof x != "number")
910 startPiece = this.r_pieces[x][y];
911 else
912 startPiece = this.g_pieces[x][y];
913 if (startPiece && this.canIplay(x, y)) {
41534b92 914 e.preventDefault();
15106e82 915 start = cd;
41534b92
BA
916 curPiece = startPiece.cloneNode();
917 curPiece.style.transform = "none";
918 curPiece.style.zIndex = 5;
15106e82
BA
919 curPiece.style.width = pieceWidth + "px";
920 curPiece.style.height = pieceWidth + "px";
41534b92 921 centerOnCursor(curPiece, e);
9db5050a 922 container.appendChild(curPiece);
41534b92 923 startPiece.style.opacity = "0.4";
3c61449b 924 chessboard.style.cursor = "none";
41534b92
BA
925 }
926 }
927 }
928 };
929
930 const mousemove = (e) => {
931 if (start) {
932 e.preventDefault();
933 centerOnCursor(curPiece, e);
934 }
11625344
BA
935 else if (e.changedTouches && e.changedTouches.length >= 1)
936 // Attempt to prevent horizontal swipe...
937 e.preventDefault();
41534b92
BA
938 };
939
940 const mouseup = (e) => {
3c61449b 941 const newR = chessboard.getBoundingClientRect();
41534b92
BA
942 if (newR.width != r.width || newR.height != r.height) {
943 this.rescale();
944 return;
945 }
b4ae3ff6
BA
946 if (!start)
947 return;
41534b92
BA
948 const [x, y] = [start.x, start.y];
949 start = null;
950 e.preventDefault();
3c61449b 951 chessboard.style.cursor = "pointer";
41534b92
BA
952 startPiece.style.opacity = "1";
953 const offset = getOffset(e);
954 const landingElt = document.elementFromPoint(offset.x, offset.y);
15106e82
BA
955 const cd =
956 (landingElt ? this.idToCoords(landingElt.id) : undefined);
957 if (cd) {
41534b92
BA
958 // NOTE: clearly suboptimal, but much easier, and not a big deal.
959 const potentialMoves = this.getPotentialMovesFrom([x, y])
15106e82 960 .filter(m => m.end.x == cd.x && m.end.y == cd.y);
41534b92 961 const moves = this.filterValid(potentialMoves);
b4ae3ff6
BA
962 if (moves.length >= 2)
963 this.showChoices(moves, r);
964 else if (moves.length == 1)
965 this.playPlusVisual(moves[0], r);
41534b92
BA
966 }
967 curPiece.remove();
968 };
969
970 if ('onmousedown' in window) {
971 document.addEventListener("mousedown", mousedown);
972 document.addEventListener("mousemove", mousemove);
973 document.addEventListener("mouseup", mouseup);
974 }
975 if ('ontouchstart' in window) {
cb17fed8
BA
976 // https://stackoverflow.com/a/42509310/12660887
977 document.addEventListener("touchstart", mousedown, {passive: false});
978 document.addEventListener("touchmove", mousemove, {passive: false});
979 document.addEventListener("touchend", mouseup, {passive: false});
41534b92 980 }
11625344 981 // TODO: onpointerdown/move/up ? See reveal.js /controllers/touch.js
41534b92
BA
982 }
983
984 showChoices(moves, r) {
985 let container = document.getElementById(this.containerId);
3c61449b 986 let chessboard = container.querySelector(".chessboard");
41534b92
BA
987 let choices = document.createElement("div");
988 choices.id = "choices";
989 choices.style.width = r.width + "px";
990 choices.style.height = r.height + "px";
991 choices.style.left = r.x + "px";
992 choices.style.top = r.y + "px";
3c61449b
BA
993 chessboard.style.opacity = "0.5";
994 container.appendChild(choices);
15106e82 995 const squareWidth = r.width / this.size.y;
41534b92
BA
996 const firstUpLeft = (r.width - (moves.length * squareWidth)) / 2;
997 const firstUpTop = (r.height - squareWidth) / 2;
998 const color = moves[0].appear[0].c;
999 const callback = (m) => {
3c61449b
BA
1000 chessboard.style.opacity = "1";
1001 container.removeChild(choices);
41534b92
BA
1002 this.playPlusVisual(m, r);
1003 }
1004 for (let i=0; i < moves.length; i++) {
1005 let choice = document.createElement("div");
1006 choice.classList.add("choice");
1007 choice.style.width = squareWidth + "px";
1008 choice.style.height = squareWidth + "px";
1009 choice.style.left = (firstUpLeft + i * squareWidth) + "px";
1010 choice.style.top = firstUpTop + "px";
1011 choice.style.backgroundColor = "lightyellow";
1012 choice.onclick = () => callback(moves[i]);
1013 const piece = document.createElement("piece");
c9ab0340 1014 const pieceSpec = this.pieces()[moves[i].appear[0].p];
41534b92 1015 piece.classList.add(pieceSpec["class"]);
15106e82 1016 piece.classList.add(C.GetColorClass(color));
41534b92
BA
1017 piece.style.width = "100%";
1018 piece.style.height = "100%";
1019 choice.appendChild(piece);
1020 choices.appendChild(choice);
1021 }
1022 }
1023
1024 //////////////
1025 // BASIC UTILS
1026
1027 get size() {
15106e82
BA
1028 return {
1029 x: 8,
1030 y: 8,
1031 ratio: 1 //for rectangular board = y / x
1032 };
41534b92
BA
1033 }
1034
1035 // Color of thing on square (i,j). 'undefined' if square is empty
1036 getColor(i, j) {
15106e82
BA
1037 if (typeof i == "string")
1038 return i; //reserves
41534b92
BA
1039 return this.board[i][j].charAt(0);
1040 }
1041
15106e82
BA
1042 static GetColorClass(c) {
1043 return (c == 'w' ? "white" : "black");
1044 }
1045
cc2c7183 1046 // Assume square i,j isn't empty
41534b92 1047 getPiece(i, j) {
15106e82
BA
1048 if (typeof j == "string")
1049 return j; //reserves
41534b92
BA
1050 return this.board[i][j].charAt(1);
1051 }
1052
cc2c7183
BA
1053 // Piece type on square (i,j)
1054 getPieceType(i, j) {
6997e386 1055 const p = this.getPiece(i, j);
cc2c7183
BA
1056 return C.CannibalKings[p] || p; //a cannibal king move as...
1057 }
1058
41534b92
BA
1059 // Get opponent color
1060 static GetOppCol(color) {
1061 return (color == "w" ? "b" : "w");
1062 }
1063
c9ab0340 1064 // Can thing on square1 capture (no return) thing on square2?
41534b92 1065 canTake([x1, y1], [x2, y2]) {
c9ab0340 1066 return (this.getColor(x1, y1) !== this.getColor(x2, y2));
41534b92
BA
1067 }
1068
1069 // Is (x,y) on the chessboard?
1070 onBoard(x, y) {
b99ce1fb
BA
1071 return (x >= 0 && x < this.size.x &&
1072 y >= 0 && y < this.size.y);
41534b92
BA
1073 }
1074
15106e82 1075 // Am I allowed to move thing at square x,y ?
41534b92 1076 canIplay(x, y) {
0c44c676 1077 return (this.playerColor == this.turn && this.getColor(x, y) == this.turn);
41534b92
BA
1078 }
1079
1080 ////////////////////////
1081 // PIECES SPECIFICATIONS
1082
c9ab0340 1083 pieces(color, x, y) {
41534b92 1084 const pawnShift = (color == "w" ? -1 : 1);
9db5050a
BA
1085 // NOTE: jump 2 squares from first rank (pawns can be here sometimes)
1086 const initRank = ((color == 'w' && x >= 6) || (color == 'b' && x <= 1));
41534b92
BA
1087 return {
1088 'p': {
1089 "class": "pawn",
c9ab0340
BA
1090 moves: [
1091 {
1092 steps: [[pawnShift, 0]],
1093 range: (initRank ? 2 : 1)
1094 }
1095 ],
1096 attack: [
1097 {
1098 steps: [[pawnShift, 1], [pawnShift, -1]],
1099 range: 1
1100 }
1101 ]
41534b92
BA
1102 },
1103 // rook
1104 'r': {
1105 "class": "rook",
c9ab0340
BA
1106 moves: [
1107 {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
1108 ]
41534b92
BA
1109 },
1110 // knight
1111 'n': {
1112 "class": "knight",
c9ab0340
BA
1113 moves: [
1114 {
1115 steps: [
1116 [1, 2], [1, -2], [-1, 2], [-1, -2],
1117 [2, 1], [-2, 1], [2, -1], [-2, -1]
1118 ],
1119 range: 1
1120 }
1121 ]
41534b92
BA
1122 },
1123 // bishop
1124 'b': {
1125 "class": "bishop",
c9ab0340
BA
1126 moves: [
1127 {steps: [[1, 1], [1, -1], [-1, 1], [-1, -1]]}
1128 ]
41534b92
BA
1129 },
1130 // queen
1131 'q': {
1132 "class": "queen",
c9ab0340
BA
1133 moves: [
1134 {
1135 steps: [
1136 [0, 1], [0, -1], [1, 0], [-1, 0],
1137 [1, 1], [1, -1], [-1, 1], [-1, -1]
1138 ]
1139 }
41534b92
BA
1140 ]
1141 },
1142 // king
1143 'k': {
1144 "class": "king",
c9ab0340
BA
1145 moves: [
1146 {
1147 steps: [
1148 [0, 1], [0, -1], [1, 0], [-1, 0],
1149 [1, 1], [1, -1], [-1, 1], [-1, -1]
1150 ],
1151 range: 1
1152 }
1153 ]
cc2c7183
BA
1154 },
1155 // Cannibal kings:
c9ab0340
BA
1156 '!': {"class": "king-pawn", moveas: "p"},
1157 '#': {"class": "king-rook", moveas: "r"},
1158 '$': {"class": "king-knight", moveas: "n"},
1159 '%': {"class": "king-bishop", moveas: "b"},
1160 '*': {"class": "king-queen", moveas: "q"}
41534b92
BA
1161 };
1162 }
1163
41534b92
BA
1164 ////////////////////
1165 // MOVES GENERATION
1166
adf7c659
BA
1167 // For Cylinder: get Y coordinate
1168 getY(y) {
b4ae3ff6
BA
1169 if (!this.options["cylinder"])
1170 return y;
41534b92 1171 let res = y % this.size.y;
b4ae3ff6 1172 if (res < 0)
adf7c659 1173 res += this.size.y;
41534b92
BA
1174 return res;
1175 }
1176
1177 // Stop at the first capture found
1178 atLeastOneCapture(color) {
1179 color = color || this.turn;
cc2c7183 1180 const oppCol = C.GetOppCol(color);
41534b92
BA
1181 for (let i = 0; i < this.size.x; i++) {
1182 for (let j = 0; j < this.size.y; j++) {
1183 if (this.board[i][j] != "" && this.getColor(i, j) == color) {
c9ab0340
BA
1184 const allSpecs = this.pieces(color, i, j)
1185 let specs = allSpecs[this.getPieceType(i, j)];
1186 const attacks = specs.attack || specs.moves;
1187 for (let a of attacks) {
1188 outerLoop: for (let step of a.steps) {
d262cff4 1189 let [ii, jj] = [i + step[0], this.getY(j + step[1])];
c9ab0340
BA
1190 let stepCounter = 1;
1191 while (this.onBoard(ii, jj) && this.board[ii][jj] == "") {
1192 if (a.range <= stepCounter++)
1193 continue outerLoop;
1194 ii += step[0];
d262cff4 1195 jj = this.getY(jj + step[1]);
c9ab0340
BA
1196 }
1197 if (
1198 this.onBoard(ii, jj) &&
1199 this.getColor(ii, jj) == oppCol &&
1200 this.filterValid(
1201 [this.getBasicMove([i, j], [ii, jj])]
1202 ).length >= 1
1203 ) {
1204 return true;
1205 }
41534b92
BA
1206 }
1207 }
1208 }
1209 }
1210 }
1211 return false;
1212 }
1213
1214 getDropMovesFrom([c, p]) {
1215 // NOTE: by design, this.reserve[c][p] >= 1 on user click
1a7c0492 1216 // (but not necessarily otherwise: atLeastOneMove() etc)
b4ae3ff6
BA
1217 if (this.reserve[c][p] == 0)
1218 return [];
41534b92
BA
1219 let moves = [];
1220 for (let i=0; i<this.size.x; i++) {
1221 for (let j=0; j<this.size.y; j++) {
41534b92
BA
1222 if (
1223 this.board[i][j] == "" &&
c9ab0340 1224 (!this.enlightened || this.enlightened[i][j]) &&
41534b92 1225 (
cc2c7183 1226 p != "p" ||
41534b92
BA
1227 (c == 'w' && i < this.size.x - 1) ||
1228 (c == 'b' && i > 0)
1229 )
1230 ) {
1231 moves.push(
1232 new Move({
1233 start: {x: c, y: p},
1234 end: {x: i, y: j},
1235 appear: [new PiPo({x: i, y: j, c: c, p: p})],
1236 vanish: []
1237 })
1238 );
1239 }
1240 }
1241 }
1242 return moves;
1243 }
1244
1245 // All possible moves from selected square
c7bf7b1b 1246 getPotentialMovesFrom(sq, color) {
8b301184
BA
1247 if (this.subTurnTeleport == 2)
1248 return [];
b4ae3ff6
BA
1249 if (typeof sq[0] == "string")
1250 return this.getDropMovesFrom(sq);
57b8015b 1251 if (this.isImmobilized(sq))
b4ae3ff6 1252 return [];
cc2c7183 1253 const piece = this.getPieceType(sq[0], sq[1]);
c9ab0340
BA
1254 let moves = this.getPotentialMovesOf(piece, sq);
1255 if (
1256 piece == "p" &&
1257 this.hasEnpassant &&
1258 this.epSquare
1259 ) {
1260 Array.prototype.push.apply(moves, this.getEnpassantCaptures(sq));
1261 }
41534b92 1262 if (
cc2c7183 1263 piece == "k" &&
41534b92
BA
1264 this.hasCastle &&
1265 this.castleFlags[color || this.turn].some(v => v < this.size.y)
1266 ) {
1267 Array.prototype.push.apply(moves, this.getCastleMoves(sq));
1268 }
1269 return this.postProcessPotentialMoves(moves);
1270 }
1271
1272 postProcessPotentialMoves(moves) {
b4ae3ff6
BA
1273 if (moves.length == 0)
1274 return [];
41534b92 1275 const color = this.getColor(moves[0].start.x, moves[0].start.y);
cc2c7183 1276 const oppCol = C.GetOppCol(color);
41534b92 1277
57b8015b
BA
1278 if (this.options["capture"] && this.atLeastOneCapture())
1279 moves = this.capturePostProcess(moves, oppCol);
41534b92 1280
57b8015b
BA
1281 if (this.options["atomic"])
1282 this.atomicPostProcess(moves, oppCol);
cc2c7183 1283
c9ab0340
BA
1284 if (
1285 moves.length > 0 &&
1286 this.getPieceType(moves[0].start.x, moves[0].start.y) == "p"
1287 ) {
57b8015b 1288 this.pawnPostProcess(moves, color, oppCol);
c9ab0340
BA
1289 }
1290
cc2c7183
BA
1291 if (
1292 this.options["cannibal"] &&
57b8015b 1293 this.options["rifle"]
cc2c7183
BA
1294 ) {
1295 // In this case a rifle-capture from last rank may promote a pawn
9db5050a 1296 this.riflePromotePostProcess(moves, color);
57b8015b
BA
1297 }
1298
1299 return moves;
1300 }
1301
1302 capturePostProcess(moves, oppCol) {
1303 // Filter out non-capturing moves (not using m.vanish because of
1304 // self captures of Recycle and Teleport).
1305 return moves.filter(m => {
1306 return (
1307 this.board[m.end.x][m.end.y] != "" &&
1308 this.getColor(m.end.x, m.end.y) == oppCol
1309 );
1310 });
1311 }
1312
1313 atomicPostProcess(moves, oppCol) {
1314 moves.forEach(m => {
1315 if (
1316 this.board[m.end.x][m.end.y] != "" &&
1317 this.getColor(m.end.x, m.end.y) == oppCol
1318 ) {
1319 // Explosion!
1320 let steps = [
1321 [-1, -1],
1322 [-1, 0],
1323 [-1, 1],
1324 [0, -1],
1325 [0, 1],
1326 [1, -1],
1327 [1, 0],
1328 [1, 1]
1329 ];
1330 for (let step of steps) {
1331 let x = m.end.x + step[0];
d262cff4 1332 let y = this.getY(m.end.y + step[1]);
57b8015b
BA
1333 if (
1334 this.onBoard(x, y) &&
1335 this.board[x][y] != "" &&
1336 this.getPieceType(x, y) != "p"
1337 ) {
1338 m.vanish.push(
1339 new PiPo({
1340 p: this.getPiece(x, y),
1341 c: this.getColor(x, y),
1342 x: x,
1343 y: y
1344 })
1345 );
1346 }
1347 }
1348 if (!this.options["rifle"])
0c44c676 1349 m.appear.pop(); //nothing appears
57b8015b
BA
1350 }
1351 });
1352 }
1353
1354 pawnPostProcess(moves, color, oppCol) {
1355 let moreMoves = [];
1356 const lastRank = (color == "w" ? 0 : this.size.x - 1);
1357 const initPiece = this.getPiece(moves[0].start.x, moves[0].start.y);
1358 moves.forEach(m => {
57b8015b
BA
1359 const [x1, y1] = [m.start.x, m.start.y];
1360 const [x2, y2] = [m.end.x, m.end.y];
1361 const promotionOk = (
1362 x2 == lastRank &&
1363 (!this.options["rifle"] || this.board[x2][y2] == "")
1364 );
1365 if (!promotionOk)
1366 return; //nothing to do
8cc2f6d0
BA
1367 if (this.options["pawnfall"]) {
1368 m.appear.shift();
8cc2f6d0
BA
1369 return;
1370 }
99ea2453
BA
1371 let finalPieces = ["p"];
1372 if (
1373 this.options["cannibal"] &&
1374 this.board[x2][y2] != "" &&
1375 this.getColor(x2, y2) == oppCol
1376 ) {
1377 finalPieces = [this.getPieceType(x2, y2)];
1378 }
1379 else
1380 finalPieces = this.pawnPromotions;
57b8015b
BA
1381 m.appear[0].p = finalPieces[0];
1382 if (initPiece == "!") //cannibal king-pawn
1383 m.appear[0].p = C.CannibalKingCode[finalPieces[0]];
1384 for (let i=1; i<finalPieces.length; i++) {
1385 const piece = finalPieces[i];
99ea2453
BA
1386 const tr = {
1387 c: color,
1388 p: (initPiece != "!" ? piece : C.CannibalKingCode[piece])
1389 };
57b8015b 1390 let newMove = this.getBasicMove([x1, y1], [x2, y2], tr);
57b8015b
BA
1391 moreMoves.push(newMove);
1392 }
1393 });
1394 Array.prototype.push.apply(moves, moreMoves);
1395 }
cc2c7183 1396
9db5050a 1397 riflePromotePostProcess(moves, color) {
57b8015b
BA
1398 const lastRank = (color == "w" ? 0 : this.size.x - 1);
1399 let newMoves = [];
1400 moves.forEach(m => {
1401 if (
1402 m.start.x == lastRank &&
1403 m.appear.length >= 1 &&
1404 m.appear[0].p == "p" &&
1405 m.appear[0].x == m.start.x &&
1406 m.appear[0].y == m.start.y
1407 ) {
57b8015b
BA
1408 m.appear[0].p = this.pawnPromotions[0];
1409 for (let i=1; i<this.pawnPromotions.length; i++) {
1410 let newMv = JSON.parse(JSON.stringify(m));
1411 newMv.appear[0].p = this.pawnSpecs.promotions[i];
1412 newMoves.push(newMv);
1413 }
1414 }
1415 });
1416 Array.prototype.push.apply(moves, newMoves);
41534b92
BA
1417 }
1418
b99ce1fb 1419 // NOTE: using special symbols to not interfere with variants' pieces codes
cc2c7183
BA
1420 static get CannibalKings() {
1421 return {
b99ce1fb
BA
1422 "!": "p",
1423 "#": "r",
1424 "$": "n",
1425 "%": "b",
6997e386
BA
1426 "*": "q",
1427 "k": "k"
cc2c7183
BA
1428 };
1429 }
1430
1431 static get CannibalKingCode() {
1432 return {
b99ce1fb
BA
1433 "p": "!",
1434 "r": "#",
1435 "n": "$",
1436 "b": "%",
1437 "q": "*",
cc2c7183
BA
1438 "k": "k"
1439 };
1440 }
1441
1442 isKing(symbol) {
6997e386 1443 return !!C.CannibalKings[symbol];
cc2c7183
BA
1444 }
1445
41534b92
BA
1446 // For Madrasi:
1447 // (redefined in Baroque etc, where Madrasi condition doesn't make sense)
1448 isImmobilized([x, y]) {
57b8015b
BA
1449 if (!this.options["madrasi"])
1450 return false;
41534b92 1451 const color = this.getColor(x, y);
cc2c7183 1452 const oppCol = C.GetOppCol(color);
c9ab0340 1453 const piece = this.getPieceType(x, y); //ok not cannibal king
57b8015b 1454 const stepSpec = this.pieces(color, x, y)[piece];
c9ab0340
BA
1455 const attacks = stepSpec.attack || stepSpec.moves;
1456 for (let a of attacks) {
1457 outerLoop: for (let step of a.steps) {
1458 let [i, j] = [x + step[0], y + step[1]];
1459 let stepCounter = 1;
1460 while (this.onBoard(i, j) && this.board[i][j] == "") {
1461 if (a.range <= stepCounter++)
1462 continue outerLoop;
1463 i += step[0];
d262cff4 1464 j = this.getY(j + step[1]);
c9ab0340
BA
1465 }
1466 if (
1467 this.onBoard(i, j) &&
1468 this.getColor(i, j) == oppCol &&
1469 this.getPieceType(i, j) == piece
1470 ) {
1471 return true;
1472 }
41534b92
BA
1473 }
1474 }
1475 return false;
1476 }
1477
1478 // Generic method to find possible moves of "sliding or jumping" pieces
1479 getPotentialMovesOf(piece, [x, y]) {
1480 const color = this.getColor(x, y);
c9ab0340 1481 const stepSpec = this.pieces(color, x, y)[piece];
41534b92 1482 let moves = [];
adf7c659
BA
1483 // Next 3 for Cylinder mode:
1484 let explored = {};
1485 let segments = [];
1486 let segStart = [];
1487
1488 const addMove = (start, end) => {
1489 let newMove = this.getBasicMove(start, end);
1490 if (segments.length > 0) {
1491 newMove.segments = JSON.parse(JSON.stringify(segments));
1492 newMove.segments.push([[segStart[0], segStart[1]], [end[0], end[1]]]);
1493 }
1494 moves.push(newMove);
1495 };
c9ab0340
BA
1496
1497 const findAddMoves = (type, stepArray) => {
1498 for (let s of stepArray) {
1499 outerLoop: for (let step of s.steps) {
adf7c659
BA
1500 segments = [];
1501 segStart = [x, y];
d262cff4
BA
1502 let [i, j] = [x, y];
1503 let stepCounter = 0;
1504 while (
1505 this.onBoard(i, j) &&
1506 (this.board[i][j] == "" || (i == x && j == y))
1507 ) {
1508 if (
1509 type != "attack" &&
1510 !explored[i + "." + j] &&
1511 (i != x || j != y)
1512 ) {
c9ab0340 1513 explored[i + "." + j] = true;
adf7c659 1514 addMove([x, y], [i, j]);
c9ab0340
BA
1515 }
1516 if (s.range <= stepCounter++)
1517 continue outerLoop;
d262cff4 1518 const oldIJ = [i, j];
c9ab0340 1519 i += step[0];
adf7c659
BA
1520 j = this.getY(j + step[1]);
1521 if (Math.abs(j - oldIJ[1]) > 1) {
d262cff4 1522 // Boundary between segments (cylinder mode)
adf7c659
BA
1523 segments.push([[segStart[0], segStart[1]], oldIJ]);
1524 segStart = [i, j];
d262cff4 1525 }
c9ab0340
BA
1526 }
1527 if (!this.onBoard(i, j))
1528 continue;
1529 const pieceIJ = this.getPieceType(i, j);
1530 if (
1531 type != "moveonly" &&
1532 !explored[i + "." + j] &&
1533 (
1534 !this.options["zen"] ||
1535 pieceIJ == "k"
1536 ) &&
1537 (
1538 this.canTake([x, y], [i, j]) ||
1539 (
1540 (this.options["recycle"] || this.options["teleport"]) &&
1541 pieceIJ != "k"
1542 )
1543 )
1544 ) {
1545 explored[i + "." + j] = true;
adf7c659 1546 addMove([x, y], [i, j]);
c9ab0340
BA
1547 }
1548 }
41534b92 1549 }
c9ab0340
BA
1550 };
1551
1552 const specialAttack = !!stepSpec.attack;
1553 if (specialAttack)
1554 findAddMoves("attack", stepSpec.attack);
1555 findAddMoves(specialAttack ? "moveonly" : "all", stepSpec.moves);
082e639a
BA
1556 if (this.options["zen"]) {
1557 Array.prototype.push.apply(moves,
1558 this.findCapturesOn([x, y], {zen: true}));
1559 }
41534b92
BA
1560 return moves;
1561 }
1562
082e639a
BA
1563 // Search for enemy (or not) pieces attacking [x, y]
1564 findCapturesOn([x, y], args) {
41534b92 1565 let moves = [];
082e639a
BA
1566 if (!args.oppCol)
1567 args.oppCol = C.GetOppCol(this.getColor(x, y) || this.turn);
c9ab0340
BA
1568 for (let i=0; i<this.size.x; i++) {
1569 for (let j=0; j<this.size.y; j++) {
57b8015b
BA
1570 if (
1571 this.board[i][j] != "" &&
082e639a 1572 this.getColor(i, j) == args.oppCol &&
57b8015b
BA
1573 !this.isImmobilized([i, j])
1574 ) {
082e639a 1575 if (args.zen && this.isKing(this.getPiece(i, j)))
c9ab0340 1576 continue; //king not captured in this way
082e639a
BA
1577 const stepSpec =
1578 this.pieces(args.oppCol, i, j)[this.getPieceType(i, j)];
c9ab0340
BA
1579 const attacks = stepSpec.attack || stepSpec.moves;
1580 for (let a of attacks) {
1581 for (let s of a.steps) {
1582 // Quick check: if step isn't compatible, don't even try
57b8015b 1583 if (!C.CompatibleStep([i, j], [x, y], s, a.range))
c9ab0340
BA
1584 continue;
1585 // Finally verify that nothing stand in-between
d262cff4 1586 let [ii, jj] = [i + s[0], this.getY(j + s[1])];
c9ab0340 1587 let stepCounter = 1;
082e639a
BA
1588 while (
1589 this.onBoard(ii, jj) &&
1590 this.board[ii][jj] == "" &&
1591 (ii != x || jj != y) //condition to attack empty squares too
1592 ) {
c9ab0340 1593 ii += s[0];
d262cff4 1594 jj = this.getY(jj + s[1]);
c9ab0340
BA
1595 }
1596 if (ii == x && jj == y) {
082e639a
BA
1597 if (args.zen)
1598 // Reverse capture:
1599 moves.push(this.getBasicMove([x, y], [i, j]));
1600 else
1601 moves.push(this.getBasicMove([i, j], [x, y]));
1602 if (args.one)
c9ab0340
BA
1603 return moves; //test for underCheck
1604 }
1605 }
1606 }
41534b92 1607 }
c9ab0340
BA
1608 }
1609 }
41534b92
BA
1610 return moves;
1611 }
1612
57b8015b
BA
1613 static CompatibleStep([x1, y1], [x2, y2], step, range) {
1614 const rx = (x2 - x1) / step[0],
1615 ry = (y2 - y1) / step[1];
1616 if (
1617 (!Number.isFinite(rx) && !Number.isNaN(rx)) ||
1618 (!Number.isFinite(ry) && !Number.isNaN(ry))
1619 ) {
1620 return false;
1621 }
1622 let distance = (Number.isNaN(rx) ? ry : rx);
1623 // TODO: 1e-7 here is totally arbitrary
1624 if (Math.abs(distance - Math.round(distance)) > 1e-7)
1625 return false;
1626 distance = Math.round(distance); //in case of (numerical...)
1627 if (range < distance)
1628 return false;
1629 return true;
1630 }
1631
41534b92
BA
1632 // Build a regular move from its initial and destination squares.
1633 // tr: transformation
1634 getBasicMove([sx, sy], [ex, ey], tr) {
1635 const initColor = this.getColor(sx, sy);
cc2c7183 1636 const initPiece = this.getPiece(sx, sy);
41534b92
BA
1637 const destColor = (this.board[ex][ey] != "" ? this.getColor(ex, ey) : "");
1638 let mv = new Move({
1639 appear: [],
1640 vanish: [],
15106e82
BA
1641 start: {x: sx, y: sy},
1642 end: {x: ex, y: ey}
41534b92
BA
1643 });
1644 if (
1645 !this.options["rifle"] ||
1646 this.board[ex][ey] == "" ||
1647 destColor == initColor //Recycle, Teleport
1648 ) {
1649 mv.appear = [
1650 new PiPo({
1651 x: ex,
1652 y: ey,
1653 c: !!tr ? tr.c : initColor,
1654 p: !!tr ? tr.p : initPiece
1655 })
1656 ];
1657 mv.vanish = [
1658 new PiPo({
1659 x: sx,
1660 y: sy,
1661 c: initColor,
1662 p: initPiece
1663 })
1664 ];
1665 }
1666 if (this.board[ex][ey] != "") {
1667 mv.vanish.push(
1668 new PiPo({
1669 x: ex,
1670 y: ey,
1671 c: this.getColor(ex, ey),
cc2c7183 1672 p: this.getPiece(ex, ey)
41534b92
BA
1673 })
1674 );
41534b92
BA
1675 if (this.options["cannibal"] && destColor != initColor) {
1676 const lastIdx = mv.vanish.length - 1;
cc2c7183
BA
1677 let trPiece = mv.vanish[lastIdx].p;
1678 if (this.isKing(this.getPiece(sx, sy)))
1679 trPiece = C.CannibalKingCode[trPiece];
b4ae3ff6
BA
1680 if (mv.appear.length >= 1)
1681 mv.appear[0].p = trPiece;
41534b92
BA
1682 else if (this.options["rifle"]) {
1683 mv.appear.unshift(
1684 new PiPo({
1685 x: sx,
1686 y: sy,
1687 c: initColor,
cc2c7183 1688 p: trPiece
41534b92
BA
1689 })
1690 );
1691 mv.vanish.unshift(
1692 new PiPo({
1693 x: sx,
1694 y: sy,
1695 c: initColor,
1696 p: initPiece
1697 })
1698 );
1699 }
1700 }
1701 }
1702 return mv;
1703 }
1704
1705 // En-passant square, if any
1706 getEpSquare(moveOrSquare) {
1707 if (typeof moveOrSquare === "string") {
1708 const square = moveOrSquare;
b4ae3ff6
BA
1709 if (square == "-")
1710 return undefined;
cc2c7183 1711 return C.SquareToCoords(square);
41534b92
BA
1712 }
1713 // Argument is a move:
1714 const move = moveOrSquare;
1715 const s = move.start,
1716 e = move.end;
1717 if (
1718 s.y == e.y &&
1719 Math.abs(s.x - e.x) == 2 &&
1720 // Next conditions for variants like Atomic or Rifle, Recycle...
cc2c7183
BA
1721 (move.appear.length > 0 && move.appear[0].p == "p") &&
1722 (move.vanish.length > 0 && move.vanish[0].p == "p")
41534b92
BA
1723 ) {
1724 return {
1725 x: (s.x + e.x) / 2,
1726 y: s.y
1727 };
1728 }
1729 return undefined; //default
1730 }
1731
1732 // Special case of en-passant captures: treated separately
c9ab0340 1733 getEnpassantCaptures([x, y]) {
41534b92 1734 const color = this.getColor(x, y);
c9ab0340 1735 const shiftX = (color == 'w' ? -1 : 1);
cc2c7183 1736 const oppCol = C.GetOppCol(color);
41534b92
BA
1737 let enpassantMove = null;
1738 if (
1739 !!this.epSquare &&
1740 this.epSquare.x == x + shiftX &&
d262cff4 1741 Math.abs(this.getY(this.epSquare.y - y)) == 1 &&
41534b92
BA
1742 this.getColor(x, this.epSquare.y) == oppCol //Doublemove guard...
1743 ) {
1744 const [epx, epy] = [this.epSquare.x, this.epSquare.y];
1745 this.board[epx][epy] = oppCol + "p";
1746 enpassantMove = this.getBasicMove([x, y], [epx, epy]);
1747 this.board[epx][epy] = "";
1748 const lastIdx = enpassantMove.vanish.length - 1; //think Rifle
1749 enpassantMove.vanish[lastIdx].x = x;
1750 }
1751 return !!enpassantMove ? [enpassantMove] : [];
1752 }
1753
41534b92
BA
1754 // "castleInCheck" arg to let some variants castle under check
1755 getCastleMoves([x, y], finalSquares, castleInCheck, castleWith) {
1756 const c = this.getColor(x, y);
1757
1758 // Castling ?
cc2c7183 1759 const oppCol = C.GetOppCol(c);
41534b92
BA
1760 let moves = [];
1761 // King, then rook:
1762 finalSquares =
1763 finalSquares || [ [2, 3], [this.size.y - 2, this.size.y - 3] ];
cc2c7183 1764 const castlingKing = this.getPiece(x, y);
41534b92
BA
1765 castlingCheck: for (
1766 let castleSide = 0;
1767 castleSide < 2;
1768 castleSide++ //large, then small
1769 ) {
b4ae3ff6
BA
1770 if (this.castleFlags[c][castleSide] >= this.size.y)
1771 continue;
41534b92
BA
1772 // If this code is reached, rook and king are on initial position
1773
1774 // NOTE: in some variants this is not a rook
1775 const rookPos = this.castleFlags[c][castleSide];
cc2c7183 1776 const castlingPiece = this.getPiece(x, rookPos);
41534b92
BA
1777 if (
1778 this.board[x][rookPos] == "" ||
1779 this.getColor(x, rookPos) != c ||
1780 (!!castleWith && !castleWith.includes(castlingPiece))
1781 ) {
1782 // Rook is not here, or changed color (see Benedict)
1783 continue;
1784 }
1785 // Nothing on the path of the king ? (and no checks)
1786 const finDist = finalSquares[castleSide][0] - y;
1787 let step = finDist / Math.max(1, Math.abs(finDist));
1788 let i = y;
1789 do {
1790 if (
1791 (!castleInCheck && this.underCheck([x, i], oppCol)) ||
1792 (
1793 this.board[x][i] != "" &&
1794 // NOTE: next check is enough, because of chessboard constraints
1795 (this.getColor(x, i) != c || ![rookPos, y].includes(i))
1796 )
1797 ) {
1798 continue castlingCheck;
1799 }
1800 i += step;
1801 } while (i != finalSquares[castleSide][0]);
1802 // Nothing on the path to the rook?
1803 step = (castleSide == 0 ? -1 : 1);
1804 for (i = y + step; i != rookPos; i += step) {
b4ae3ff6
BA
1805 if (this.board[x][i] != "")
1806 continue castlingCheck;
41534b92
BA
1807 }
1808
1809 // Nothing on final squares, except maybe king and castling rook?
1810 for (i = 0; i < 2; i++) {
1811 if (
1812 finalSquares[castleSide][i] != rookPos &&
1813 this.board[x][finalSquares[castleSide][i]] != "" &&
1814 (
1815 finalSquares[castleSide][i] != y ||
1816 this.getColor(x, finalSquares[castleSide][i]) != c
1817 )
1818 ) {
1819 continue castlingCheck;
1820 }
1821 }
1822
1823 // If this code is reached, castle is valid
1824 moves.push(
1825 new Move({
1826 appear: [
1827 new PiPo({
1828 x: x,
1829 y: finalSquares[castleSide][0],
1830 p: castlingKing,
1831 c: c
1832 }),
1833 new PiPo({
1834 x: x,
1835 y: finalSquares[castleSide][1],
1836 p: castlingPiece,
1837 c: c
1838 })
1839 ],
1840 vanish: [
1841 // King might be initially disguised (Titan...)
1842 new PiPo({ x: x, y: y, p: castlingKing, c: c }),
1843 new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
1844 ],
1845 end:
1846 Math.abs(y - rookPos) <= 2
c9ab0340
BA
1847 ? {x: x, y: rookPos}
1848 : {x: x, y: y + 2 * (castleSide == 0 ? -1 : 1)}
41534b92
BA
1849 })
1850 );
1851 }
1852
1853 return moves;
1854 }
1855
1856 ////////////////////
1857 // MOVES VALIDATION
1858
082e639a
BA
1859 // Is (king at) given position under check by "oppCol" ?
1860 underCheck([x, y], oppCol) {
b4ae3ff6
BA
1861 if (this.options["taking"] || this.options["dark"])
1862 return false;
082e639a
BA
1863 return (
1864 this.findCapturesOn([x, y], {oppCol: oppCol, one: true}).length >= 1
1865 );
41534b92
BA
1866 }
1867
1868 // Stop at first king found (TODO: multi-kings)
1869 searchKingPos(color) {
1870 for (let i=0; i < this.size.x; i++) {
1871 for (let j=0; j < this.size.y; j++) {
cc2c7183
BA
1872 if (this.getColor(i, j) == color && this.isKing(this.getPiece(i, j)))
1873 return [i, j];
41534b92
BA
1874 }
1875 }
1876 return [-1, -1]; //king not found
1877 }
1878
1879 filterValid(moves) {
b4ae3ff6
BA
1880 if (moves.length == 0)
1881 return [];
41534b92 1882 const color = this.turn;
cc2c7183 1883 const oppCol = C.GetOppCol(color);
41534b92
BA
1884 if (this.options["balance"] && [1, 3].includes(this.movesCount)) {
1885 // Forbid moves either giving check or exploding opponent's king:
1886 const oppKingPos = this.searchKingPos(oppCol);
1887 moves = moves.filter(m => {
1888 if (
cc2c7183
BA
1889 m.vanish.some(v => v.c == oppCol && v.p == "k") &&
1890 m.appear.every(a => a.c != oppCol || a.p != "k")
41534b92
BA
1891 )
1892 return false;
1893 this.playOnBoard(m);
1894 const res = !this.underCheck(oppKingPos, color);
1895 this.undoOnBoard(m);
1896 return res;
1897 });
1898 }
b4ae3ff6
BA
1899 if (this.options["taking"] || this.options["dark"])
1900 return moves;
41534b92
BA
1901 const kingPos = this.searchKingPos(color);
1902 let filtered = {}; //avoid re-checking similar moves (promotions...)
1903 return moves.filter(m => {
1904 const key = m.start.x + m.start.y + '.' + m.end.x + m.end.y;
1905 if (!filtered[key]) {
1906 this.playOnBoard(m);
1907 let square = kingPos,
1908 res = true; //a priori valid
cc2c7183 1909 if (m.vanish.some(v => {
6997e386 1910 return C.CannibalKings[v.p] && v.c == color;
cc2c7183 1911 })) {
41534b92
BA
1912 // Search king in appear array:
1913 const newKingIdx =
cc2c7183 1914 m.appear.findIndex(a => {
6997e386 1915 return C.CannibalKings[a.p] && a.c == color;
cc2c7183 1916 });
41534b92
BA
1917 if (newKingIdx >= 0)
1918 square = [m.appear[newKingIdx].x, m.appear[newKingIdx].y];
b4ae3ff6
BA
1919 else
1920 res = false;
41534b92
BA
1921 }
1922 res &&= !this.underCheck(square, oppCol);
1923 this.undoOnBoard(m);
1924 filtered[key] = res;
1925 return res;
1926 }
1927 return filtered[key];
1928 });
1929 }
1930
1931 /////////////////
1932 // MOVES PLAYING
1933
1934 // Aggregate flags into one object
1935 aggregateFlags() {
1936 return this.castleFlags;
1937 }
1938
1939 // Reverse operation
1940 disaggregateFlags(flags) {
1941 this.castleFlags = flags;
1942 }
1943
1944 // Apply a move on board
1945 playOnBoard(move) {
6997e386
BA
1946 for (let psq of move.vanish)
1947 this.board[psq.x][psq.y] = "";
1948 for (let psq of move.appear)
1949 this.board[psq.x][psq.y] = psq.c + psq.p;
41534b92
BA
1950 }
1951 // Un-apply the played move
1952 undoOnBoard(move) {
6997e386
BA
1953 for (let psq of move.appear)
1954 this.board[psq.x][psq.y] = "";
1955 for (let psq of move.vanish)
1956 this.board[psq.x][psq.y] = psq.c + psq.p;
41534b92
BA
1957 }
1958
1959 updateCastleFlags(move) {
1960 // Update castling flags if start or arrive from/at rook/king locations
1961 move.appear.concat(move.vanish).forEach(psq => {
1962 if (
1963 this.board[psq.x][psq.y] != "" &&
cc2c7183 1964 this.getPieceType(psq.x, psq.y) == "k"
41534b92
BA
1965 ) {
1966 this.castleFlags[psq.c] = [this.size.y, this.size.y];
1967 }
1968 // NOTE: not "else if" because king can capture enemy rook...
cc2c7183 1969 let c = "";
b4ae3ff6
BA
1970 if (psq.x == 0)
1971 c = "b";
1972 else if (psq.x == this.size.x - 1)
1973 c = "w";
cc2c7183 1974 if (c != "") {
41534b92 1975 const fidx = this.castleFlags[c].findIndex(f => f == psq.y);
b4ae3ff6
BA
1976 if (fidx >= 0)
1977 this.castleFlags[c][fidx] = this.size.y;
41534b92
BA
1978 }
1979 });
1980 }
1981
1982 prePlay(move) {
1983 if (
99ea2453
BA
1984 this.hasCastle &&
1985 // If flags already off, no need to re-check:
1986 Object.keys(this.castleFlags).some(c => {
1987 return this.castleFlags[c].some(val => val < this.size.y)})
41534b92 1988 ) {
99ea2453
BA
1989 this.updateCastleFlags(move);
1990 }
1991 if (this.options["crazyhouse"]) {
1992 move.vanish.forEach(v => {
1993 const square = C.CoordsToSquare({x: v.x, y: v.y});
1994 if (this.ispawn[square])
1995 delete this.ispawn[square];
1996 });
1997 if (move.appear.length > 0 && move.vanish.length > 0) {
1998 // Assumption: something is moving
1999 const initSquare = C.CoordsToSquare(move.start);
f429756d 2000 const destSquare = C.CoordsToSquare(move.end);
99ea2453
BA
2001 if (
2002 this.ispawn[initSquare] ||
2003 (move.vanish[0].p == "p" && move.appear[0].p != "p")
41534b92 2004 ) {
f429756d
BA
2005 this.ispawn[destSquare] = true;
2006 }
2007 else if (
2008 this.ispawn[destSquare] &&
2009 this.getColor(move.end.x, move.end.y) != move.vanish[0].c
2010 ) {
2011 move.vanish[1].p = "p";
2012 delete this.ispawn[destSquare];
41534b92
BA
2013 }
2014 }
2015 }
2016 const minSize = Math.min(move.appear.length, move.vanish.length);
0c44c676
BA
2017 if (
2018 this.hasReserve &&
2019 // Warning; atomic pawn removal isn't a capture
2020 (!this.options["atomic"] || !this.rempawn || this.movesCount >= 1)
2021 ) {
41534b92
BA
2022 const color = this.turn;
2023 for (let i=minSize; i<move.appear.length; i++) {
2024 // Something appears = dropped on board (some exceptions, Chakart...)
0c44c676
BA
2025 if (move.appear[i].c == color) {
2026 const piece = move.appear[i].p;
2027 this.updateReserve(color, piece, this.reserve[color][piece] - 1);
2028 }
41534b92
BA
2029 }
2030 for (let i=minSize; i<move.vanish.length; i++) {
2031 // Something vanish: add to reserve except if recycle & opponent
0c44c676
BA
2032 if (
2033 this.options["crazyhouse"] ||
2034 (this.options["recycle"] && move.vanish[i].c == color)
2035 ) {
2036 const piece = move.vanish[i].p;
41534b92 2037 this.updateReserve(color, piece, this.reserve[color][piece] + 1);
0c44c676 2038 }
41534b92
BA
2039 }
2040 }
2041 }
2042
2043 play(move) {
2044 this.prePlay(move);
b4ae3ff6
BA
2045 if (this.hasEnpassant)
2046 this.epSquare = this.getEpSquare(move);
41534b92
BA
2047 this.playOnBoard(move);
2048 this.postPlay(move);
2049 }
2050
2051 postPlay(move) {
2052 const color = this.turn;
cc2c7183 2053 const oppCol = C.GetOppCol(color);
b4ae3ff6 2054 if (this.options["dark"])
c9ab0340 2055 this.updateEnlightened();
41534b92
BA
2056 if (this.options["teleport"]) {
2057 if (
cc2c7183 2058 this.subTurnTeleport == 1 &&
41534b92
BA
2059 move.vanish.length > move.appear.length &&
2060 move.vanish[move.vanish.length - 1].c == color
2061 ) {
2062 const v = move.vanish[move.vanish.length - 1];
2063 this.captured = {x: v.x, y: v.y, c: v.c, p: v.p};
cc2c7183 2064 this.subTurnTeleport = 2;
41534b92
BA
2065 return;
2066 }
cc2c7183 2067 this.subTurnTeleport = 1;
41534b92
BA
2068 this.captured = null;
2069 }
2070 if (this.options["balance"]) {
b4ae3ff6
BA
2071 if (![1, 3].includes(this.movesCount))
2072 this.turn = oppCol;
41534b92
BA
2073 }
2074 else {
2075 if (
2076 (
2077 this.options["doublemove"] &&
2078 this.movesCount >= 1 &&
2079 this.subTurn == 1
2080 ) ||
2081 (this.options["progressive"] && this.subTurn <= this.movesCount)
2082 ) {
2083 const oppKingPos = this.searchKingPos(oppCol);
6f74b81a
BA
2084 if (
2085 oppKingPos[0] >= 0 &&
2086 (
2087 this.options["taking"] ||
2088 !this.underCheck(oppKingPos, color)
2089 )
2090 ) {
41534b92
BA
2091 this.subTurn++;
2092 return;
2093 }
2094 }
2095 this.turn = oppCol;
2096 }
2097 this.movesCount++;
2098 this.subTurn = 1;
2099 }
2100
2101 // "Stop at the first move found"
2102 atLeastOneMove(color) {
2103 color = color || this.turn;
2104 for (let i = 0; i < this.size.x; i++) {
2105 for (let j = 0; j < this.size.y; j++) {
2106 if (this.board[i][j] != "" && this.getColor(i, j) == color) {
cc2c7183
BA
2107 // NOTE: in fact searching for all potential moves from i,j.
2108 // I don't believe this is an issue, for now at least.
41534b92 2109 const moves = this.getPotentialMovesFrom([i, j]);
b4ae3ff6
BA
2110 if (moves.some(m => this.filterValid([m]).length >= 1))
2111 return true;
41534b92
BA
2112 }
2113 }
2114 }
2115 if (this.hasReserve && this.reserve[color]) {
2116 for (let p of Object.keys(this.reserve[color])) {
2117 const moves = this.getDropMovesFrom([color, p]);
b4ae3ff6
BA
2118 if (moves.some(m => this.filterValid([m]).length >= 1))
2119 return true;
41534b92
BA
2120 }
2121 }
2122 return false;
2123 }
2124
2125 // What is the score ? (Interesting if game is over)
2126 getCurrentScore(move) {
2127 const color = this.turn;
cc2c7183 2128 const oppCol = C.GetOppCol(color);
41534b92 2129 const kingPos = [this.searchKingPos(color), this.searchKingPos(oppCol)];
b4ae3ff6
BA
2130 if (kingPos[0][0] < 0 && kingPos[1][0] < 0)
2131 return "1/2";
2132 if (kingPos[0][0] < 0)
2133 return (color == "w" ? "0-1" : "1-0");
2134 if (kingPos[1][0] < 0)
2135 return (color == "w" ? "1-0" : "0-1");
2136 if (this.atLeastOneMove())
2137 return "*";
41534b92 2138 // No valid move: stalemate or checkmate?
c9ab0340 2139 if (!this.underCheck(kingPos[0], color))
b4ae3ff6 2140 return "1/2";
41534b92
BA
2141 // OK, checkmate
2142 return (color == "w" ? "0-1" : "1-0");
2143 }
2144
41534b92
BA
2145 playVisual(move, r) {
2146 move.vanish.forEach(v => {
c9ab0340
BA
2147 // TODO: next "if" shouldn't be required
2148 if (this.g_pieces[v.x][v.y])
2149 this.g_pieces[v.x][v.y].remove();
2150 this.g_pieces[v.x][v.y] = null;
41534b92 2151 });
3c61449b
BA
2152 let chessboard =
2153 document.getElementById(this.containerId).querySelector(".chessboard");
b4ae3ff6
BA
2154 if (!r)
2155 r = chessboard.getBoundingClientRect();
41534b92
BA
2156 const pieceWidth = this.getPieceWidth(r.width);
2157 move.appear.forEach(a => {
41534b92
BA
2158 this.g_pieces[a.x][a.y] = document.createElement("piece");
2159 this.g_pieces[a.x][a.y].classList.add(this.pieces()[a.p]["class"]);
2160 this.g_pieces[a.x][a.y].classList.add(a.c == "w" ? "white" : "black");
2161 this.g_pieces[a.x][a.y].style.width = pieceWidth + "px";
2162 this.g_pieces[a.x][a.y].style.height = pieceWidth + "px";
2163 const [ip, jp] = this.getPixelPosition(a.x, a.y, r);
9db5050a
BA
2164 // Translate coordinates to use chessboard as reference:
2165 this.g_pieces[a.x][a.y].style.transform =
2166 `translate(${ip - r.x}px,${jp - r.y}px)`;
c9ab0340
BA
2167 if (this.enlightened && !this.enlightened[a.x][a.y])
2168 this.g_pieces[a.x][a.y].classList.add("hidden");
3c61449b 2169 chessboard.appendChild(this.g_pieces[a.x][a.y]);
41534b92 2170 });
c9ab0340
BA
2171 if (this.options["dark"])
2172 this.graphUpdateEnlightened();
41534b92
BA
2173 }
2174
2175 playPlusVisual(move, r) {
41534b92 2176 this.play(move);
c9ab0340 2177 this.playVisual(move, r);
41534b92
BA
2178 this.afterPlay(move); //user method
2179 }
2180
15106e82
BA
2181 getMaxDistance(rwidth) {
2182 // Works for all rectangular boards:
2183 return Math.sqrt(rwidth ** 2 + (rwidth / this.size.ratio) ** 2);
2184 }
2185
2186 getDomPiece(x, y) {
2187 return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y];
41534b92
BA
2188 }
2189
2190 animate(move, callback) {
15106e82 2191 if (this.noAnimate || move.noAnimate) {
e8b85c86
BA
2192 callback();
2193 return;
2194 }
9db5050a
BA
2195 let initPiece = this.getDomPiece(move.start.x, move.start.y);
2196 if (!initPiece) { //TODO this shouldn't be required
639afc98
BA
2197 callback();
2198 return;
2199 }
9db5050a
BA
2200 // NOTE: cloning generally not required, but light enough, and simpler
2201 let movingPiece = initPiece.cloneNode();
2202 initPiece.style.opacity = "0";
2203 let container =
2204 document.getElementById(this.containerId)
2205 const r = container.querySelector(".chessboard").getBoundingClientRect();
082e639a
BA
2206 if (typeof move.start.x == "string") {
2207 // Need to bound width/height (was 100% for reserve pieces)
2208 const pieceWidth = this.getPieceWidth(r.width);
2209 movingPiece.style.width = pieceWidth + "px";
2210 movingPiece.style.height = pieceWidth + "px";
2211 }
15106e82 2212 const maxDist = this.getMaxDistance(r.width);
9db5050a 2213 const pieces = this.pieces();
15106e82 2214 if (move.drag) {
15106e82
BA
2215 const startCode = this.getPiece(move.start.x, move.start.y);
2216 movingPiece.classList.remove(pieces[startCode]["class"]);
2217 movingPiece.classList.add(pieces[move.drag.p]["class"]);
2218 const apparentColor = this.getColor(move.start.x, move.start.y);
2219 if (apparentColor != move.drag.c) {
2220 movingPiece.classList.remove(C.GetColorClass(apparentColor));
2221 movingPiece.classList.add(C.GetColorClass(move.drag.c));
41534b92 2222 }
41534b92 2223 }
9db5050a 2224 container.appendChild(movingPiece);
15106e82 2225 const animateSegment = (index, cb) => {
9db5050a 2226 // NOTE: move.drag could be generalized per-segment (usage?)
15106e82
BA
2227 const [i1, j1] = move.segments[index][0];
2228 const [i2, j2] = move.segments[index][1];
2229 const dep = this.getPixelPosition(i1, j1, r);
2230 const arr = this.getPixelPosition(i2, j2, r);
9db5050a
BA
2231 movingPiece.style.transitionDuration = "0s";
2232 movingPiece.style.transform = `translate(${dep[0]}px, ${dep[1]}px)`;
15106e82
BA
2233 const distance =
2234 Math.sqrt((arr[0] - dep[0]) ** 2 + (arr[1] - dep[1]) ** 2);
2235 const duration = 0.2 + (distance / maxDist) * 0.3;
9db5050a
BA
2236 // TODO: unclear why we need this new delay below:
2237 setTimeout(() => {
2238 movingPiece.style.transitionDuration = duration + "s";
adf7c659 2239 // movingPiece is child of container: no need to adjust coordinates
9db5050a
BA
2240 movingPiece.style.transform = `translate(${arr[0]}px, ${arr[1]}px)`;
2241 setTimeout(cb, duration * 1000);
2242 }, 50);
15106e82 2243 };
635418a5
BA
2244 if (!move.segments) {
2245 move.segments = [
2246 [[move.start.x, move.start.y], [move.end.x, move.end.y]]
2247 ];
2248 }
15106e82 2249 let index = 0;
635418a5 2250 const animateSegmentCallback = () => {
15106e82 2251 if (index < move.segments.length)
635418a5 2252 animateSegment(index++, animateSegmentCallback);
15106e82 2253 else {
9db5050a
BA
2254 movingPiece.remove();
2255 initPiece.style.opacity = "1";
41534b92 2256 callback();
15106e82 2257 }
635418a5
BA
2258 };
2259 animateSegmentCallback();
41534b92
BA
2260 }
2261
2262 playReceivedMove(moves, callback) {
21e8e712 2263 const launchAnimation = () => {
3c61449b 2264 const r = container.querySelector(".chessboard").getBoundingClientRect();
21e8e712
BA
2265 const animateRec = i => {
2266 this.animate(moves[i], () => {
21e8e712 2267 this.play(moves[i]);
57b8015b 2268 this.playVisual(moves[i], r);
b4ae3ff6
BA
2269 if (i < moves.length - 1)
2270 setTimeout(() => animateRec(i+1), 300);
2271 else
2272 callback();
21e8e712
BA
2273 });
2274 };
2275 animateRec(0);
2276 };
e081c5eb
BA
2277 // Delay if user wasn't focused:
2278 const checkDisplayThenAnimate = (delay) => {
3c61449b 2279 if (container.style.display == "none") {
21e8e712
BA
2280 alert("New move! Let's go back to game...");
2281 document.getElementById("gameInfos").style.display = "none";
3c61449b 2282 container.style.display = "block";
21e8e712
BA
2283 setTimeout(launchAnimation, 700);
2284 }
b4ae3ff6
BA
2285 else
2286 setTimeout(launchAnimation, delay || 0);
21e8e712 2287 };
3c61449b 2288 let container = document.getElementById(this.containerId);
016306e3
BA
2289 if (document.hidden) {
2290 document.onvisibilitychange = () => {
2291 document.onvisibilitychange = undefined;
e081c5eb 2292 checkDisplayThenAnimate(700);
fd31883b 2293 };
fd31883b 2294 }
b4ae3ff6
BA
2295 else
2296 checkDisplayThenAnimate();
41534b92
BA
2297 }
2298
2299};