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