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