First draft of Hex game
[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 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 ],
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
64 get pawnPromotions() {
65 return ['q', 'r', 'n', 'b'];
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:
94 doClick(coords) {
95 if (typeof coords.x != "number")
96 return null; //click on reserves
97 if (
98 this.options["teleport"] && this.subTurnTeleport == 2 &&
99 this.board[coords.x][coords.y] == ""
100 ) {
101 let res = new Move({
102 start: {x: this.captured.x, y: this.captured.y},
103 appear: [
104 new PiPo({
105 x: coords.x,
106 y: coords.y,
107 c: this.captured.c, //this.turn,
108 p: this.captured.p
109 })
110 ],
111 vanish: []
112 });
113 res.drag = {c: this.captured.c, p: this.captured.p};
114 return res;
115 }
116 return null;
117 }
118
119 ////////////////////
120 // COORDINATES UTILS
121
122 // 3a --> {x:3, y:10}
123 static SquareToCoords(sq) {
124 return ArrayFun.toObject(["x", "y"],
125 [0, 1].map(i => parseInt(sq[i], 36)));
126 }
127
128 // {x:11, y:12} --> bc
129 static CoordsToSquare(cd) {
130 return Object.values(cd).map(c => c.toString(36)).join("");
131 }
132
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 }
139 // Reserve :
140 return `${this.containerId}|rsq-${cd.x}-${cd.y}`;
141 }
142
143 idToCoords(targetId) {
144 if (!targetId)
145 return null; //outside page, maybe...
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")
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]};
159 }
160
161 /////////////
162 // FEN UTILS
163
164 // Turn "wb" into "B" (for FEN)
165 board2fen(b) {
166 return (b[0] == "w" ? b[1].toUpperCase() : b[1]);
167 }
168
169 // Turn "p" into "bp" (for board)
170 fen2board(f) {
171 return (f.charCodeAt(0) <= 90 ? "w" + f.toLowerCase() : "b" + f);
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";
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 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 = [];
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(",") + "}";
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 };
267 if (fenParts.length > 3)
268 res = Object.assign(res, JSON.parse(fenParts[3]));
269 return res;
270 }
271
272 // Return current fen (game state)
273 getFen() {
274 let fen = (
275 this.getPosition() + " " +
276 this.getTurnFen() + " " +
277 this.movesCount
278 );
279 let parts = [];
280 if (this.hasFlags)
281 parts.push(`"flags":"${this.getFlagsFen()}"`);
282 if (this.hasEnpassant)
283 parts.push(`"enpassant":"${this.getEnpassantFen()}"`);
284 if (this.hasReserve)
285 parts.push(`"reserve":"${this.getReserveFen()}"`);
286 if (this.options["crazyhouse"])
287 parts.push(`"ispawn":"${this.getIspawnFen()}"`);
288 if (parts.length >= 1)
289 fen += " {" + parts.join(",") + "}";
290 return fen;
291 }
292
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
305 // Position part of the FEN string
306 getPosition() {
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++) {
311 if (this.board[i][j] == "")
312 emptyCount++;
313 else {
314 if (emptyCount > 0) {
315 // Add empty squares in-between
316 position += C.FenEmptySquares(emptyCount);
317 emptyCount = 0;
318 }
319 position += this.board2fen(this.board[i][j]);
320 }
321 }
322 if (emptyCount > 0)
323 // "Flush remainder"
324 position += C.FenEmptySquares(emptyCount);
325 if (i < this.size.y - 1)
326 position += "/"; //separate rows
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 => {
338 return this.castleFlags[c].map(x => x.toString(36)).join("");
339 }).join("");
340 }
341
342 // Enpassant part of the FEN string
343 getEnpassantFen() {
344 if (!this.epSquare)
345 return "-"; //no en-passant
346 return C.CoordsToSquare(this.epSquare);
347 }
348
349 getReserveFen() {
350 return (
351 ["w","b"].map(c => Object.values(this.reserve[c]).join("")).join("")
352 );
353 }
354
355 getIspawnFen() {
356 const squares = Object.keys(this.ispawn);
357 if (squares.length == 0)
358 return "-";
359 return squares.join(",");
360 }
361
362 // Set flags from fen (castle: white a,h then black a,h)
363 setFlags(fenflags) {
364 this.castleFlags = {
365 w: [0, 1].map(i => parseInt(fenflags.charAt(i), 36)),
366 b: [2, 3].map(i => parseInt(fenflags.charAt(i), 36))
367 };
368 }
369
370 //////////////////
371 // INITIALIZATION
372
373 constructor(o) {
374 this.options = o.options;
375 this.playerColor = o.color;
376 this.afterPlay = o.afterPlay; //trigger some actions after playing a move
377
378 // Fen string fully describes the game state
379 if (!o.fen)
380 o.fen = this.genRandInitFen(o.seed);
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:
402 if (!isNaN(num))
403 j += num;
404 // Else: something at position i,j
405 else
406 board[i][j++] = this.fen2board(character);
407 }
408 }
409 return board;
410 }
411
412 // Some additional variables from FEN (variant dependant)
413 setOtherVariables(fenParsed) {
414 // Set flags and enpassant:
415 if (this.hasFlags)
416 this.setFlags(fenParsed.flags);
417 if (this.hasEnpassant)
418 this.epSquare = this.getEpSquare(fenParsed.enpassant);
419 if (this.hasReserve)
420 this.initReserves(fenParsed.reserve);
421 if (this.options["crazyhouse"])
422 this.initIspawn(fenParsed.ispawn);
423 this.subTurn = 1; //may be unused
424 if (this.options["teleport"]) {
425 this.subTurnTeleport = 1;
426 this.captured = null;
427 }
428 if (this.options["dark"]) {
429 // Setup enlightened: squares reachable by player side
430 this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
431 this.updateEnlightened();
432 }
433 }
434
435 updateEnlightened() {
436 this.oldEnlightened = this.enlightened;
437 this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
438 // Add pieces positions + all squares reachable by moves (includes Zen):
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 {
443 this.enlightened[x][y] = true;
444 this.getPotentialMovesFrom([x, y]).forEach(m => {
445 this.enlightened[m.end.x][m.end.y] = true;
446 });
447 }
448 }
449 }
450 if (this.epSquare)
451 this.enlightEnpassant();
452 }
453
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;
458 for (let step of steps) {
459 const x = this.epSquare.x - step[0],
460 y = this.getY(this.epSquare.y - step[1]);
461 if (
462 this.onBoard(x, y) &&
463 this.getColor(x, y) == this.playerColor &&
464 this.getPieceType(x, y) == "p"
465 ) {
466 this.enlightened[x][this.epSquare.y] = true;
467 break;
468 }
469 }
470 }
471
472 // ordering as in pieces() p,r,n,b,q,k (+ count in base 30 if needed)
473 initReserves(reserveStr) {
474 const counts = reserveStr.split("").map(c => parseInt(c, 30));
475 this.reserve = { w: {}, b: {} };
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)
480 this.reserve['w'][pieceName[i]] = counts[i];
481 else
482 this.reserve['b'][pieceName[i-L]] = counts[i];
483 }
484 }
485
486 initIspawn(ispawnStr) {
487 if (ispawnStr != "-")
488 this.ispawn = ArrayFun.toObject(ispawnStr.split(","), true);
489 else
490 this.ispawn = {};
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
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
508 //////////////
509 // VISUAL PART
510
511 getPieceWidth(rwidth) {
512 return (rwidth / this.size.y);
513 }
514
515 getReserveSquareSize(rwidth, nbR) {
516 const sqSize = this.getPieceWidth(rwidth);
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();
529 const chessboard =
530 document.getElementById(this.containerId).querySelector(".chessboard");
531 new ResizeObserver(this.rescale).observe(chessboard);
532 }
533
534 re_drawBoardElements() {
535 const board = this.getSvgChessboard();
536 const oppCol = C.GetOppCol(this.playerColor);
537 let chessboard =
538 document.getElementById(this.containerId).querySelector(".chessboard");
539 chessboard.innerHTML = "";
540 chessboard.insertAdjacentHTML('beforeend', board);
541 // Compare window ratio width / height to aspectRatio:
542 const windowRatio = window.innerWidth / window.innerHeight;
543 let cbWidth, cbHeight;
544 if (windowRatio <= this.size.ratio) {
545 // Limiting dimension is width:
546 cbWidth = Math.min(window.innerWidth, 767);
547 cbHeight = cbWidth / this.size.ratio;
548 }
549 else {
550 // Limiting dimension is height:
551 cbHeight = Math.min(window.innerHeight, 767);
552 cbWidth = cbHeight * this.size.ratio;
553 }
554 if (this.hasReserve) {
555 const sqSize = cbWidth / this.size.y;
556 // NOTE: allocate space for reserves (up/down) even if they are empty
557 // Cannot use getReserveSquareSize() here, but sqSize is an upper bound.
558 if ((window.innerHeight - cbHeight) / 2 < sqSize + 5) {
559 cbHeight = window.innerHeight - 2 * (sqSize + 5);
560 cbWidth = cbHeight * this.size.ratio;
561 }
562 }
563 chessboard.style.width = cbWidth + "px";
564 chessboard.style.height = cbHeight + "px";
565 // Center chessboard:
566 const spaceLeft = (window.innerWidth - cbWidth) / 2,
567 spaceTop = (window.innerHeight - cbHeight) / 2;
568 chessboard.style.left = spaceLeft + "px";
569 chessboard.style.top = spaceTop + "px";
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() {
582 const flipped = (this.playerColor == 'b');
583 let board = `
584 <svg
585 viewBox="0 0 80 80"
586 class="chessboard_SVG">
587 <g>`;
588 for (let i=0; i < this.size.x; i++) {
589 for (let j=0; j < this.size.y; j++) {
590 const ii = (flipped ? this.size.x - 1 - i : i);
591 const jj = (flipped ? this.size.y - 1 - j : j);
592 let classes = this.getSquareColorClass(ii, jj);
593 if (this.enlightened && !this.enlightened[ii][jj])
594 classes += " in-shadow";
595 // NOTE: x / y reversed because coordinates system is reversed.
596 board += `<rect
597 class="${classes}"
598 id="${this.coordsToId({x: ii, y: jj})}"
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
609 // Generally light square bottom-right
610 getSquareColorClass(x, y) {
611 return ((x+y) % 2 == 0 ? "light-square": "dark-square");
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 }
626 else
627 this.g_pieces = ArrayFun.init(this.size.x, this.size.y, null);
628 let chessboard =
629 document.getElementById(this.containerId).querySelector(".chessboard");
630 if (!r)
631 r = chessboard.getBoundingClientRect();
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++) {
635 if (this.board[i][j] != "") {
636 const color = this.getColor(i, j);
637 const piece = this.getPiece(i, j);
638 this.g_pieces[i][j] = document.createElement("piece");
639 this.g_pieces[i][j].classList.add(this.pieces()[piece]["class"]);
640 this.g_pieces[i][j].classList.add(C.GetColorClass(color));
641 this.g_pieces[i][j].style.width = pieceWidth + "px";
642 this.g_pieces[i][j].style.height = pieceWidth + "px";
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)`;
647 if (this.enlightened && !this.enlightened[i][j])
648 this.g_pieces[i][j].classList.add("hidden");
649 chessboard.appendChild(this.g_pieces[i][j]);
650 }
651 }
652 }
653 if (this.hasReserve)
654 this.re_drawReserve(['w', 'b'], r);
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) {
662 if (!this.reserve[c])
663 continue;
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);
673 if (reservesDiv)
674 reservesDiv.remove();
675 }
676 }
677 else
678 this.r_pieces = { w: {}, b: {} };
679 let container = document.getElementById(this.containerId);
680 if (!r)
681 r = container.querySelector(".chessboard").getBoundingClientRect();
682 for (let c of colors) {
683 if (!this.reserve[c])
684 continue;
685 const nbR = this.getNbReservePieces(c);
686 if (nbR == 0)
687 continue;
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";
697 // NOTE: +1 fix display bug on Firefox at least
698 rcontainer.style.width = (nbR * sqResSize + 1) + "px";
699 rcontainer.style.height = sqResSize + "px";
700 container.appendChild(rcontainer);
701 for (let p of Object.keys(this.reserve[c])) {
702 if (this.reserve[c][p] == 0)
703 continue;
704 let r_cell = document.createElement("div");
705 r_cell.id = this.coordsToId({x: c, y: p});
706 r_cell.classList.add("reserve-cell");
707 r_cell.style.width = sqResSize + "px";
708 r_cell.style.height = sqResSize + "px";
709 rcontainer.appendChild(r_cell);
710 let piece = document.createElement("piece");
711 const pieceSpec = this.pieces()[p];
712 piece.classList.add(pieceSpec["class"]);
713 piece.classList.add(C.GetColorClass(c));
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) {
732 if (this.options["cannibal"] && C.CannibalKings[piece])
733 piece = "k"; //capturing cannibal king: back to king form
734 const oldCount = this.reserve[color][piece];
735 this.reserve[color][piece] = count;
736 // Redrawing is much easier if count==0
737 if ([oldCount, count].includes(0))
738 this.re_drawReserve([color]);
739 else {
740 const numId = this.getReserveNumId(color, piece);
741 document.getElementById(numId).textContent = count;
742 }
743 }
744
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]) {
754 let elt = document.getElementById(this.coordsToId({x: x, y: y}));
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]) {
760 let elt = document.getElementById(this.coordsToId({x: x, y: y}));
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
769 // After resize event: no need to destroy/recreate pieces
770 rescale() {
771 const container = document.getElementById(this.containerId);
772 if (!container)
773 return; //useful at initial loading
774 let chessboard = container.querySelector(".chessboard");
775 const r = chessboard.getBoundingClientRect();
776 const newRatio = r.width / r.height;
777 let newWidth = r.width,
778 newHeight = r.height;
779 if (newRatio > this.size.ratio) {
780 newWidth = r.height * this.size.ratio;
781 chessboard.style.width = newWidth + "px";
782 }
783 else if (newRatio < this.size.ratio) {
784 newHeight = r.width / this.size.ratio;
785 chessboard.style.height = newHeight + "px";
786 }
787 const newX = (window.innerWidth - newWidth) / 2;
788 chessboard.style.left = newX + "px";
789 const newY = (window.innerHeight - newHeight) / 2;
790 chessboard.style.top = newY + "px";
791 const newR = {x: newX, y: newY, width: newWidth, height: newHeight};
792 const pieceWidth = this.getPieceWidth(newWidth);
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 }
807 }
808 }
809 }
810 if (this.hasReserve)
811 this.rescaleReserve(newR);
812 }
813
814 rescaleReserve(r) {
815 for (let c of ['w','b']) {
816 if (!this.reserve[c])
817 continue;
818 const nbR = this.getNbReservePieces(c);
819 if (nbR == 0)
820 continue;
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";
828 rcontainer.style.width = (nbR * sqResSize + 1) + "px";
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 => {
833 if (this.reserve[c][p] == 0)
834 return;
835 let r_cell = document.getElementById(this.coordsToId({x: c, y: p}));
836 r_cell.style.width = sqResSize + "px";
837 r_cell.style.height = sqResSize + "px";
838 });
839 }
840 }
841
842 // Return the absolute pixel coordinates given current position.
843 // Our coordinate system differs from CSS one (x <--> y).
844 // We return here the CSS coordinates (more useful).
845 getPixelPosition(i, j, r) {
846 if (i < 0 || j < 0)
847 return [0, 0]; //piece vanishes
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 }
862 return [r.x + x, r.y + y];
863 }
864
865 initMouseEvents() {
866 let container = document.getElementById(this.containerId);
867 let chessboard = container.querySelector(".chessboard");
868
869 const getOffset = e => {
870 if (e.clientX)
871 // Mouse
872 return {x: e.clientX, y: e.clientY};
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)
881 return {x: touchLocation.clientX, y: touchLocation.clientY};
882 return {x: 0, y: 0}; //shouldn't reach here =)
883 }
884
885 const centerOnCursor = (piece, e) => {
886 const centerShift = this.getPieceWidth(r.width) / 2;
887 const offset = getOffset(e);
888 piece.style.left = (offset.x - centerShift) + "px";
889 piece.style.top = (offset.y - centerShift) + "px";
890 }
891
892 let start = null,
893 r = null,
894 startPiece, curPiece = null,
895 pieceWidth;
896 const mousedown = (e) => {
897 // Disable zoom on smartphones:
898 if (e.touches && e.touches.length > 1)
899 e.preventDefault();
900 r = chessboard.getBoundingClientRect();
901 pieceWidth = this.getPieceWidth(r.width);
902 const cd = this.idToCoords(e.target.id);
903 if (cd) {
904 const move = this.doClick(cd);
905 if (move)
906 this.playPlusVisual(move);
907 else {
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)) {
914 e.preventDefault();
915 start = cd;
916 curPiece = startPiece.cloneNode();
917 curPiece.style.transform = "none";
918 curPiece.style.zIndex = 5;
919 curPiece.style.width = pieceWidth + "px";
920 curPiece.style.height = pieceWidth + "px";
921 centerOnCursor(curPiece, e);
922 container.appendChild(curPiece);
923 startPiece.style.opacity = "0.4";
924 chessboard.style.cursor = "none";
925 }
926 }
927 }
928 };
929
930 const mousemove = (e) => {
931 if (start) {
932 e.preventDefault();
933 centerOnCursor(curPiece, e);
934 }
935 else if (e.changedTouches && e.changedTouches.length >= 1)
936 // Attempt to prevent horizontal swipe...
937 e.preventDefault();
938 };
939
940 const mouseup = (e) => {
941 const newR = chessboard.getBoundingClientRect();
942 if (newR.width != r.width || newR.height != r.height) {
943 this.rescale();
944 return;
945 }
946 if (!start)
947 return;
948 const [x, y] = [start.x, start.y];
949 start = null;
950 e.preventDefault();
951 chessboard.style.cursor = "pointer";
952 startPiece.style.opacity = "1";
953 const offset = getOffset(e);
954 const landingElt = document.elementFromPoint(offset.x, offset.y);
955 const cd =
956 (landingElt ? this.idToCoords(landingElt.id) : undefined);
957 if (cd) {
958 // NOTE: clearly suboptimal, but much easier, and not a big deal.
959 const potentialMoves = this.getPotentialMovesFrom([x, y])
960 .filter(m => m.end.x == cd.x && m.end.y == cd.y);
961 const moves = this.filterValid(potentialMoves);
962 if (moves.length >= 2)
963 this.showChoices(moves, r);
964 else if (moves.length == 1)
965 this.playPlusVisual(moves[0], r);
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) {
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});
980 }
981 // TODO: onpointerdown/move/up ? See reveal.js /controllers/touch.js
982 }
983
984 showChoices(moves, r) {
985 let container = document.getElementById(this.containerId);
986 let chessboard = container.querySelector(".chessboard");
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";
993 chessboard.style.opacity = "0.5";
994 container.appendChild(choices);
995 const squareWidth = r.width / this.size.y;
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) => {
1000 chessboard.style.opacity = "1";
1001 container.removeChild(choices);
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");
1014 const pieceSpec = this.pieces()[moves[i].appear[0].p];
1015 piece.classList.add(pieceSpec["class"]);
1016 piece.classList.add(C.GetColorClass(color));
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() {
1028 return {
1029 x: 8,
1030 y: 8,
1031 ratio: 1 //for rectangular board = y / x
1032 };
1033 }
1034
1035 // Color of thing on square (i,j). 'undefined' if square is empty
1036 getColor(i, j) {
1037 if (typeof i == "string")
1038 return i; //reserves
1039 return this.board[i][j].charAt(0);
1040 }
1041
1042 static GetColorClass(c) {
1043 return (c == 'w' ? "white" : "black");
1044 }
1045
1046 // Assume square i,j isn't empty
1047 getPiece(i, j) {
1048 if (typeof j == "string")
1049 return j; //reserves
1050 return this.board[i][j].charAt(1);
1051 }
1052
1053 // Piece type on square (i,j)
1054 getPieceType(i, j) {
1055 const p = this.getPiece(i, j);
1056 return C.CannibalKings[p] || p; //a cannibal king move as...
1057 }
1058
1059 // Get opponent color
1060 static GetOppCol(color) {
1061 return (color == "w" ? "b" : "w");
1062 }
1063
1064 // Can thing on square1 capture (no return) thing on square2?
1065 canTake([x1, y1], [x2, y2]) {
1066 return (this.getColor(x1, y1) !== this.getColor(x2, y2));
1067 }
1068
1069 // Is (x,y) on the chessboard?
1070 onBoard(x, y) {
1071 return (x >= 0 && x < this.size.x &&
1072 y >= 0 && y < this.size.y);
1073 }
1074
1075 // Am I allowed to move thing at square x,y ?
1076 canIplay(x, y) {
1077 return (this.playerColor == this.turn && this.getColor(x, y) == this.turn);
1078 }
1079
1080 ////////////////////////
1081 // PIECES SPECIFICATIONS
1082
1083 pieces(color, x, y) {
1084 const pawnShift = (color == "w" ? -1 : 1);
1085 // NOTE: jump 2 squares from first rank (pawns can be here sometimes)
1086 const initRank = ((color == 'w' && x >= 6) || (color == 'b' && x <= 1));
1087 return {
1088 'p': {
1089 "class": "pawn",
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 ]
1102 },
1103 // rook
1104 'r': {
1105 "class": "rook",
1106 moves: [
1107 {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
1108 ]
1109 },
1110 // knight
1111 'n': {
1112 "class": "knight",
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 ]
1122 },
1123 // bishop
1124 'b': {
1125 "class": "bishop",
1126 moves: [
1127 {steps: [[1, 1], [1, -1], [-1, 1], [-1, -1]]}
1128 ]
1129 },
1130 // queen
1131 'q': {
1132 "class": "queen",
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 }
1140 ]
1141 },
1142 // king
1143 'k': {
1144 "class": "king",
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 ]
1154 },
1155 // Cannibal kings:
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"}
1161 };
1162 }
1163
1164 ////////////////////
1165 // MOVES GENERATION
1166
1167 // For Cylinder: get Y coordinate
1168 getY(y) {
1169 if (!this.options["cylinder"])
1170 return y;
1171 let res = y % this.size.y;
1172 if (res < 0)
1173 res += this.size.y;
1174 return res;
1175 }
1176
1177 // Stop at the first capture found
1178 atLeastOneCapture(color) {
1179 color = color || this.turn;
1180 const oppCol = C.GetOppCol(color);
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) {
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) {
1189 let [ii, jj] = [i + step[0], this.getY(j + step[1])];
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];
1195 jj = this.getY(jj + step[1]);
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 }
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
1216 // (but not necessarily otherwise: atLeastOneMove() etc)
1217 if (this.reserve[c][p] == 0)
1218 return [];
1219 let moves = [];
1220 for (let i=0; i<this.size.x; i++) {
1221 for (let j=0; j<this.size.y; j++) {
1222 if (
1223 this.board[i][j] == "" &&
1224 (!this.enlightened || this.enlightened[i][j]) &&
1225 (
1226 p != "p" ||
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
1246 getPotentialMovesFrom(sq, color) {
1247 if (this.subTurnTeleport == 2)
1248 return [];
1249 if (typeof sq[0] == "string")
1250 return this.getDropMovesFrom(sq);
1251 if (this.isImmobilized(sq))
1252 return [];
1253 const piece = this.getPieceType(sq[0], sq[1]);
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 }
1262 if (
1263 piece == "k" &&
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) {
1273 if (moves.length == 0)
1274 return [];
1275 const color = this.getColor(moves[0].start.x, moves[0].start.y);
1276 const oppCol = C.GetOppCol(color);
1277
1278 if (this.options["capture"] && this.atLeastOneCapture())
1279 moves = this.capturePostProcess(moves, oppCol);
1280
1281 if (this.options["atomic"])
1282 this.atomicPostProcess(moves, oppCol);
1283
1284 if (
1285 moves.length > 0 &&
1286 this.getPieceType(moves[0].start.x, moves[0].start.y) == "p"
1287 ) {
1288 this.pawnPostProcess(moves, color, oppCol);
1289 }
1290
1291 if (
1292 this.options["cannibal"] &&
1293 this.options["rifle"]
1294 ) {
1295 // In this case a rifle-capture from last rank may promote a pawn
1296 this.riflePromotePostProcess(moves, color);
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];
1332 let y = this.getY(m.end.y + step[1]);
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"])
1349 m.appear.pop(); //nothing appears
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 => {
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
1367 if (this.options["pawnfall"]) {
1368 m.appear.shift();
1369 return;
1370 }
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;
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];
1386 const tr = {
1387 c: color,
1388 p: (initPiece != "!" ? piece : C.CannibalKingCode[piece])
1389 };
1390 let newMove = this.getBasicMove([x1, y1], [x2, y2], tr);
1391 moreMoves.push(newMove);
1392 }
1393 });
1394 Array.prototype.push.apply(moves, moreMoves);
1395 }
1396
1397 riflePromotePostProcess(moves, color) {
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 ) {
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);
1417 }
1418
1419 // NOTE: using special symbols to not interfere with variants' pieces codes
1420 static get CannibalKings() {
1421 return {
1422 "!": "p",
1423 "#": "r",
1424 "$": "n",
1425 "%": "b",
1426 "*": "q",
1427 "k": "k"
1428 };
1429 }
1430
1431 static get CannibalKingCode() {
1432 return {
1433 "p": "!",
1434 "r": "#",
1435 "n": "$",
1436 "b": "%",
1437 "q": "*",
1438 "k": "k"
1439 };
1440 }
1441
1442 isKing(symbol) {
1443 return !!C.CannibalKings[symbol];
1444 }
1445
1446 // For Madrasi:
1447 // (redefined in Baroque etc, where Madrasi condition doesn't make sense)
1448 isImmobilized([x, y]) {
1449 if (!this.options["madrasi"])
1450 return false;
1451 const color = this.getColor(x, y);
1452 const oppCol = C.GetOppCol(color);
1453 const piece = this.getPieceType(x, y); //ok not cannibal king
1454 const stepSpec = this.pieces(color, x, y)[piece];
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];
1464 j = this.getY(j + step[1]);
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 }
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);
1481 const stepSpec = this.pieces(color, x, y)[piece];
1482 let moves = [];
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 };
1496
1497 const findAddMoves = (type, stepArray) => {
1498 for (let s of stepArray) {
1499 outerLoop: for (let step of s.steps) {
1500 segments = [];
1501 segStart = [x, y];
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 ) {
1513 explored[i + "." + j] = true;
1514 addMove([x, y], [i, j]);
1515 }
1516 if (s.range <= stepCounter++)
1517 continue outerLoop;
1518 const oldIJ = [i, j];
1519 i += step[0];
1520 j = this.getY(j + step[1]);
1521 if (Math.abs(j - oldIJ[1]) > 1) {
1522 // Boundary between segments (cylinder mode)
1523 segments.push([[segStart[0], segStart[1]], oldIJ]);
1524 segStart = [i, j];
1525 }
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;
1546 addMove([x, y], [i, j]);
1547 }
1548 }
1549 }
1550 };
1551
1552 const specialAttack = !!stepSpec.attack;
1553 if (specialAttack)
1554 findAddMoves("attack", stepSpec.attack);
1555 findAddMoves(specialAttack ? "moveonly" : "all", stepSpec.moves);
1556 if (this.options["zen"]) {
1557 Array.prototype.push.apply(moves,
1558 this.findCapturesOn([x, y], {zen: true}));
1559 }
1560 return moves;
1561 }
1562
1563 // Search for enemy (or not) pieces attacking [x, y]
1564 findCapturesOn([x, y], args) {
1565 let moves = [];
1566 if (!args.oppCol)
1567 args.oppCol = C.GetOppCol(this.getColor(x, y) || this.turn);
1568 for (let i=0; i<this.size.x; i++) {
1569 for (let j=0; j<this.size.y; j++) {
1570 if (
1571 this.board[i][j] != "" &&
1572 this.getColor(i, j) == args.oppCol &&
1573 !this.isImmobilized([i, j])
1574 ) {
1575 if (args.zen && this.isKing(this.getPiece(i, j)))
1576 continue; //king not captured in this way
1577 const stepSpec =
1578 this.pieces(args.oppCol, i, j)[this.getPieceType(i, j)];
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
1583 if (!C.CompatibleStep([i, j], [x, y], s, a.range))
1584 continue;
1585 // Finally verify that nothing stand in-between
1586 let [ii, jj] = [i + s[0], this.getY(j + s[1])];
1587 let stepCounter = 1;
1588 while (
1589 this.onBoard(ii, jj) &&
1590 this.board[ii][jj] == "" &&
1591 (ii != x || jj != y) //condition to attack empty squares too
1592 ) {
1593 ii += s[0];
1594 jj = this.getY(jj + s[1]);
1595 }
1596 if (ii == x && jj == y) {
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)
1603 return moves; //test for underCheck
1604 }
1605 }
1606 }
1607 }
1608 }
1609 }
1610 return moves;
1611 }
1612
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
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);
1636 const initPiece = this.getPiece(sx, sy);
1637 const destColor = (this.board[ex][ey] != "" ? this.getColor(ex, ey) : "");
1638 let mv = new Move({
1639 appear: [],
1640 vanish: [],
1641 start: {x: sx, y: sy},
1642 end: {x: ex, y: ey}
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),
1672 p: this.getPiece(ex, ey)
1673 })
1674 );
1675 if (this.options["cannibal"] && destColor != initColor) {
1676 const lastIdx = mv.vanish.length - 1;
1677 let trPiece = mv.vanish[lastIdx].p;
1678 if (this.isKing(this.getPiece(sx, sy)))
1679 trPiece = C.CannibalKingCode[trPiece];
1680 if (mv.appear.length >= 1)
1681 mv.appear[0].p = trPiece;
1682 else if (this.options["rifle"]) {
1683 mv.appear.unshift(
1684 new PiPo({
1685 x: sx,
1686 y: sy,
1687 c: initColor,
1688 p: trPiece
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;
1709 if (square == "-")
1710 return undefined;
1711 return C.SquareToCoords(square);
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...
1721 (move.appear.length > 0 && move.appear[0].p == "p") &&
1722 (move.vanish.length > 0 && move.vanish[0].p == "p")
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
1733 getEnpassantCaptures([x, y]) {
1734 const color = this.getColor(x, y);
1735 const shiftX = (color == 'w' ? -1 : 1);
1736 const oppCol = C.GetOppCol(color);
1737 let enpassantMove = null;
1738 if (
1739 !!this.epSquare &&
1740 this.epSquare.x == x + shiftX &&
1741 Math.abs(this.getY(this.epSquare.y - y)) == 1 &&
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
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 ?
1759 const oppCol = C.GetOppCol(c);
1760 let moves = [];
1761 // King, then rook:
1762 finalSquares =
1763 finalSquares || [ [2, 3], [this.size.y - 2, this.size.y - 3] ];
1764 const castlingKing = this.getPiece(x, y);
1765 castlingCheck: for (
1766 let castleSide = 0;
1767 castleSide < 2;
1768 castleSide++ //large, then small
1769 ) {
1770 if (this.castleFlags[c][castleSide] >= this.size.y)
1771 continue;
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];
1776 const castlingPiece = this.getPiece(x, rookPos);
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) {
1805 if (this.board[x][i] != "")
1806 continue castlingCheck;
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
1847 ? {x: x, y: rookPos}
1848 : {x: x, y: y + 2 * (castleSide == 0 ? -1 : 1)}
1849 })
1850 );
1851 }
1852
1853 return moves;
1854 }
1855
1856 ////////////////////
1857 // MOVES VALIDATION
1858
1859 // Is (king at) given position under check by "oppCol" ?
1860 underCheck([x, y], oppCol) {
1861 if (this.options["taking"] || this.options["dark"])
1862 return false;
1863 return (
1864 this.findCapturesOn([x, y], {oppCol: oppCol, one: true}).length >= 1
1865 );
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++) {
1872 if (this.getColor(i, j) == color && this.isKing(this.getPiece(i, j)))
1873 return [i, j];
1874 }
1875 }
1876 return [-1, -1]; //king not found
1877 }
1878
1879 filterValid(moves) {
1880 if (moves.length == 0)
1881 return [];
1882 const color = this.turn;
1883 const oppCol = C.GetOppCol(color);
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 (
1889 m.vanish.some(v => v.c == oppCol && v.p == "k") &&
1890 m.appear.every(a => a.c != oppCol || a.p != "k")
1891 )
1892 return false;
1893 this.playOnBoard(m);
1894 const res = !this.underCheck(oppKingPos, color);
1895 this.undoOnBoard(m);
1896 return res;
1897 });
1898 }
1899 if (this.options["taking"] || this.options["dark"])
1900 return moves;
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
1909 if (m.vanish.some(v => {
1910 return C.CannibalKings[v.p] && v.c == color;
1911 })) {
1912 // Search king in appear array:
1913 const newKingIdx =
1914 m.appear.findIndex(a => {
1915 return C.CannibalKings[a.p] && a.c == color;
1916 });
1917 if (newKingIdx >= 0)
1918 square = [m.appear[newKingIdx].x, m.appear[newKingIdx].y];
1919 else
1920 res = false;
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) {
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;
1950 }
1951 // Un-apply the played move
1952 undoOnBoard(move) {
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;
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] != "" &&
1964 this.getPieceType(psq.x, psq.y) == "k"
1965 ) {
1966 this.castleFlags[psq.c] = [this.size.y, this.size.y];
1967 }
1968 // NOTE: not "else if" because king can capture enemy rook...
1969 let c = "";
1970 if (psq.x == 0)
1971 c = "b";
1972 else if (psq.x == this.size.x - 1)
1973 c = "w";
1974 if (c != "") {
1975 const fidx = this.castleFlags[c].findIndex(f => f == psq.y);
1976 if (fidx >= 0)
1977 this.castleFlags[c][fidx] = this.size.y;
1978 }
1979 });
1980 }
1981
1982 prePlay(move) {
1983 if (
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)})
1988 ) {
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);
2000 const destSquare = C.CoordsToSquare(move.end);
2001 if (
2002 this.ispawn[initSquare] ||
2003 (move.vanish[0].p == "p" && move.appear[0].p != "p")
2004 ) {
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];
2013 }
2014 }
2015 }
2016 const minSize = Math.min(move.appear.length, move.vanish.length);
2017 if (
2018 this.hasReserve &&
2019 // Warning; atomic pawn removal isn't a capture
2020 (!this.options["atomic"] || !this.rempawn || this.movesCount >= 1)
2021 ) {
2022 const color = this.turn;
2023 for (let i=minSize; i<move.appear.length; i++) {
2024 // Something appears = dropped on board (some exceptions, Chakart...)
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 }
2029 }
2030 for (let i=minSize; i<move.vanish.length; i++) {
2031 // Something vanish: add to reserve except if recycle & opponent
2032 if (
2033 this.options["crazyhouse"] ||
2034 (this.options["recycle"] && move.vanish[i].c == color)
2035 ) {
2036 const piece = move.vanish[i].p;
2037 this.updateReserve(color, piece, this.reserve[color][piece] + 1);
2038 }
2039 }
2040 }
2041 }
2042
2043 play(move) {
2044 this.prePlay(move);
2045 if (this.hasEnpassant)
2046 this.epSquare = this.getEpSquare(move);
2047 this.playOnBoard(move);
2048 this.postPlay(move);
2049 }
2050
2051 postPlay(move) {
2052 const color = this.turn;
2053 const oppCol = C.GetOppCol(color);
2054 if (this.options["dark"])
2055 this.updateEnlightened();
2056 if (this.options["teleport"]) {
2057 if (
2058 this.subTurnTeleport == 1 &&
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};
2064 this.subTurnTeleport = 2;
2065 return;
2066 }
2067 this.subTurnTeleport = 1;
2068 this.captured = null;
2069 }
2070 if (this.options["balance"]) {
2071 if (![1, 3].includes(this.movesCount))
2072 this.turn = oppCol;
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);
2084 if (
2085 oppKingPos[0] >= 0 &&
2086 (
2087 this.options["taking"] ||
2088 !this.underCheck(oppKingPos, color)
2089 )
2090 ) {
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) {
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.
2109 const moves = this.getPotentialMovesFrom([i, j]);
2110 if (moves.some(m => this.filterValid([m]).length >= 1))
2111 return true;
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]);
2118 if (moves.some(m => this.filterValid([m]).length >= 1))
2119 return true;
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;
2128 const oppCol = C.GetOppCol(color);
2129 const kingPos = [this.searchKingPos(color), this.searchKingPos(oppCol)];
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 "*";
2138 // No valid move: stalemate or checkmate?
2139 if (!this.underCheck(kingPos[0], color))
2140 return "1/2";
2141 // OK, checkmate
2142 return (color == "w" ? "0-1" : "1-0");
2143 }
2144
2145 playVisual(move, r) {
2146 move.vanish.forEach(v => {
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;
2151 });
2152 let chessboard =
2153 document.getElementById(this.containerId).querySelector(".chessboard");
2154 if (!r)
2155 r = chessboard.getBoundingClientRect();
2156 const pieceWidth = this.getPieceWidth(r.width);
2157 move.appear.forEach(a => {
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);
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)`;
2167 if (this.enlightened && !this.enlightened[a.x][a.y])
2168 this.g_pieces[a.x][a.y].classList.add("hidden");
2169 chessboard.appendChild(this.g_pieces[a.x][a.y]);
2170 });
2171 if (this.options["dark"])
2172 this.graphUpdateEnlightened();
2173 }
2174
2175 playPlusVisual(move, r) {
2176 this.play(move);
2177 this.playVisual(move, r);
2178 this.afterPlay(move); //user method
2179 }
2180
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];
2188 }
2189
2190 animate(move, callback) {
2191 if (this.noAnimate || move.noAnimate) {
2192 callback();
2193 return;
2194 }
2195 let initPiece = this.getDomPiece(move.start.x, move.start.y);
2196 if (!initPiece) { //TODO this shouldn't be required
2197 callback();
2198 return;
2199 }
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 };