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