base_rules.js refactoring. draft state (untested)
[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 // Is (x,y) on the chessboard?
1118 onBoard(x, y) {
1119 return (x >= 0 && x < this.size.x &&
1120 y >= 0 && y < this.size.y);
1121 }
1122
1123 // Am I allowed to move thing at square x,y ?
1124 canIplay(x, y) {
1125 return (this.playerColor == this.turn && this.getColor(x, y) == this.turn);
1126 }
1127
1128 ////////////////////////
1129 // PIECES SPECIFICATIONS
1130
1131 pieces(color, x, y) {
1132 const pawnShift = (color == "w" ? -1 : 1);
1133 // NOTE: jump 2 squares from first rank (pawns can be here sometimes)
1134 const initRank = ((color == 'w' && x >= 6) || (color == 'b' && x <= 1));
1135 return {
1136 'p': {
1137 "class": "pawn",
1138 moves: [
1139 {
1140 steps: [[pawnShift, 0]],
1141 range: (initRank ? 2 : 1)
1142 }
1143 ],
1144 attack: [
1145 {
1146 steps: [[pawnShift, 1], [pawnShift, -1]],
1147 range: 1
1148 }
1149 ]
1150 },
1151 // rook
1152 'r': {
1153 "class": "rook",
1154 moves: [
1155 {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
1156 ]
1157 },
1158 // knight
1159 'n': {
1160 "class": "knight",
1161 moves: [
1162 {
1163 steps: [
1164 [1, 2], [1, -2], [-1, 2], [-1, -2],
1165 [2, 1], [-2, 1], [2, -1], [-2, -1]
1166 ],
1167 range: 1
1168 }
1169 ]
1170 },
1171 // bishop
1172 'b': {
1173 "class": "bishop",
1174 moves: [
1175 {steps: [[1, 1], [1, -1], [-1, 1], [-1, -1]]}
1176 ]
1177 },
1178 // queen
1179 'q': {
1180 "class": "queen",
1181 moves: [
1182 {
1183 steps: [
1184 [0, 1], [0, -1], [1, 0], [-1, 0],
1185 [1, 1], [1, -1], [-1, 1], [-1, -1]
1186 ]
1187 }
1188 ]
1189 },
1190 // king
1191 'k': {
1192 "class": "king",
1193 moves: [
1194 {
1195 steps: [
1196 [0, 1], [0, -1], [1, 0], [-1, 0],
1197 [1, 1], [1, -1], [-1, 1], [-1, -1]
1198 ],
1199 range: 1
1200 }
1201 ]
1202 },
1203 // Cannibal kings:
1204 '!': {"class": "king-pawn", moveas: "p"},
1205 '#': {"class": "king-rook", moveas: "r"},
1206 '$': {"class": "king-knight", moveas: "n"},
1207 '%': {"class": "king-bishop", moveas: "b"},
1208 '*': {"class": "king-queen", moveas: "q"}
1209 };
1210 }
1211
1212 // NOTE: using special symbols to not interfere with variants' pieces codes
1213 static get CannibalKings() {
1214 return {
1215 "!": "p",
1216 "#": "r",
1217 "$": "n",
1218 "%": "b",
1219 "*": "q",
1220 "k": "k"
1221 };
1222 }
1223
1224 static get CannibalKingCode() {
1225 return {
1226 "p": "!",
1227 "r": "#",
1228 "n": "$",
1229 "b": "%",
1230 "q": "*",
1231 "k": "k"
1232 };
1233 }
1234
1235 //////////////////////////
1236 // MOVES GENERATION UTILS
1237
1238 // For Cylinder: get Y coordinate
1239 getY(y) {
1240 if (!this.options["cylinder"])
1241 return y;
1242 let res = y % this.size.y;
1243 if (res < 0)
1244 res += this.size.y;
1245 return res;
1246 }
1247
1248 // Can thing on square1 capture thing on square2?
1249 canTake([x1, y1], [x2, y2]) {
1250 return this.getColor(x1, y1) !== this.getColor(x2, y2);
1251 }
1252
1253 canStepOver(i, j, p) {
1254 // In some variants, objects on boards don't stop movement (Chakart)
1255 return this.board[i][j] == "";
1256 }
1257
1258 // For Madrasi:
1259 // (redefined in Baroque etc, where Madrasi condition doesn't make sense)
1260 isImmobilized([x, y]) {
1261 if (!this.options["madrasi"])
1262 return false;
1263 const color = this.getColor(x, y);
1264 const oppCol = C.GetOppCol(color);
1265 const piece = this.getPieceType(x, y); //ok not cannibal king
1266 const stepSpec = this.getStepSpec(color, x, y);
1267 const attacks = stepSpec.attack || stepSpec.moves;
1268 for (let a of attacks) {
1269 outerLoop: for (let step of a.steps) {
1270 let [i, j] = [x + step[0], y + step[1]];
1271 let stepCounter = 1;
1272 while (this.onBoard(i, j) && this.board[i][j] == "") {
1273 if (a.range <= stepCounter++)
1274 continue outerLoop;
1275 i += step[0];
1276 j = this.getY(j + step[1]);
1277 }
1278 if (
1279 this.onBoard(i, j) &&
1280 this.getColor(i, j) == oppCol &&
1281 this.getPieceType(i, j) == piece
1282 ) {
1283 return true;
1284 }
1285 }
1286 }
1287 return false;
1288 }
1289
1290 // Stop at the first capture found
1291 atLeastOneCapture(color) {
1292 const oppCol = C.GetOppCol(color);
1293 const allowed = ([x, y]) => {
1294 this.getColor(x, y) == oppCol &&
1295 this.filterValid([this.getBasicMove([i, j], [x, y])]).length >= 1
1296 };
1297 for (let i=0; i<this.size.x; i++) {
1298 for (let j=0; j<this.size.y; j++) {
1299 if (this.getColor(i, j) == color) {
1300 if (
1301 (!this.options["zen"] && this.findDestSquares(
1302 [i, j], {attackOnly: true, one: true}, allowed).length == 1) ||
1303 (this.options["zen"] && this.findCapturesOn(
1304 [i, j], {one: true}, allowed).length == 1)
1305 ) {
1306 return true;
1307 }
1308 }
1309 }
1310 }
1311 return false;
1312 }
1313
1314 getSegments(curSeg, segStart, segEnd) {
1315 if (curSeg.length == 0)
1316 return undefined;
1317 let segments = JSON.parse(JSON.stringify(curSeg)); //not altering
1318 segments.push([[segStart[0], segStart[1]], [segEnd[0], segEnd[1]]]);
1319 return segments;
1320 }
1321
1322 getStepSpec(color, x, y) {
1323 const allSpecs = this.pieces(color, x, y);
1324 let stepSpec = allSpecs[piece];
1325 if (stepSpec.moveas)
1326 stepSpec = allSpecs[stepSpec.moveas];
1327 return stepSpec;
1328 }
1329
1330 compatibleStep([x1, y1], [x2, y2], step, range) {
1331 let shifts = [0];
1332 if (this.options["cylinder"])
1333 Array.prototype.push.apply(shifts, [-this.size.y, this.size.y]);
1334 for (let sh of shifts) {
1335 const rx = (x2 - x1) / step[0],
1336 ry = (y2 + sh - y1) / step[1];
1337 if (
1338 (!Number.isFinite(rx) && !Number.isNaN(rx)) ||
1339 (!Number.isFinite(ry) && !Number.isNaN(ry))
1340 ) {
1341 continue;
1342 }
1343 let distance = (Number.isNaN(rx) ? ry : rx);
1344 // TODO: 1e-7 here is totally arbitrary
1345 if (Math.abs(distance - Math.round(distance)) > 1e-7)
1346 continue;
1347 distance = Math.round(distance); //in case of (numerical...)
1348 if (range >= distance)
1349 return true;
1350 }
1351 return false;
1352 }
1353
1354 ////////////////////
1355 // MOVES GENERATION
1356
1357 getDropMovesFrom([c, p]) {
1358 // NOTE: by design, this.reserve[c][p] >= 1 on user click
1359 // (but not necessarily otherwise: atLeastOneMove() etc)
1360 if (this.reserve[c][p] == 0)
1361 return [];
1362 let moves = [];
1363 for (let i=0; i<this.size.x; i++) {
1364 for (let j=0; j<this.size.y; j++) {
1365 if (
1366 this.board[i][j] == "" &&
1367 (!this.enlightened || this.enlightened[i][j]) &&
1368 (
1369 p != "p" ||
1370 (c == 'w' && i < this.size.x - 1) ||
1371 (c == 'b' && i > 0)
1372 )
1373 ) {
1374 moves.push(
1375 new Move({
1376 start: {x: c, y: p},
1377 end: {x: i, y: j},
1378 appear: [new PiPo({x: i, y: j, c: c, p: p})],
1379 vanish: []
1380 })
1381 );
1382 }
1383 }
1384 }
1385 return moves;
1386 }
1387
1388 // All possible moves from selected square
1389 getPotentialMovesFrom(sq, color) {
1390 if (this.subTurnTeleport == 2)
1391 return [];
1392 if (typeof sq[0] == "string")
1393 return this.getDropMovesFrom(sq);
1394 if (this.isImmobilized(sq))
1395 return [];
1396 const piece = this.getPieceType(sq[0], sq[1]);
1397 let moves = this.getPotentialMovesOf(piece, sq);
1398 if (
1399 piece == "p" &&
1400 this.hasEnpassant &&
1401 this.epSquare
1402 ) {
1403 Array.prototype.push.apply(moves, this.getEnpassantCaptures(sq));
1404 }
1405 if (
1406 piece == "k" &&
1407 this.hasCastle &&
1408 this.castleFlags[color || this.turn].some(v => v < this.size.y)
1409 ) {
1410 Array.prototype.push.apply(moves, this.getCastleMoves(sq));
1411 }
1412 return this.postProcessPotentialMoves(moves);
1413 }
1414
1415 postProcessPotentialMoves(moves) {
1416 if (moves.length == 0)
1417 return [];
1418 const color = this.getColor(moves[0].start.x, moves[0].start.y);
1419 const oppCol = C.GetOppCol(color);
1420
1421 if (this.options["capture"] && this.atLeastOneCapture())
1422 moves = this.capturePostProcess(moves, oppCol);
1423
1424 if (this.options["atomic"])
1425 this.atomicPostProcess(moves, color, oppCol);
1426
1427 if (
1428 moves.length > 0 &&
1429 this.getPieceType(moves[0].start.x, moves[0].start.y) == "p"
1430 ) {
1431 this.pawnPostProcess(moves, color, oppCol);
1432 }
1433
1434 if (
1435 this.options["cannibal"] &&
1436 this.options["rifle"]
1437 ) {
1438 // In this case a rifle-capture from last rank may promote a pawn
1439 this.riflePromotePostProcess(moves, color);
1440 }
1441
1442 return moves;
1443 }
1444
1445 capturePostProcess(moves, oppCol) {
1446 // Filter out non-capturing moves (not using m.vanish because of
1447 // self captures of Recycle and Teleport).
1448 return moves.filter(m => {
1449 return (
1450 this.board[m.end.x][m.end.y] != "" &&
1451 this.getColor(m.end.x, m.end.y) == oppCol
1452 );
1453 });
1454 }
1455
1456 atomicPostProcess(moves, color, oppCol) {
1457 moves.forEach(m => {
1458 if (
1459 this.board[m.end.x][m.end.y] != "" &&
1460 this.getColor(m.end.x, m.end.y) == oppCol
1461 ) {
1462 // Explosion!
1463 let steps = [
1464 [-1, -1],
1465 [-1, 0],
1466 [-1, 1],
1467 [0, -1],
1468 [0, 1],
1469 [1, -1],
1470 [1, 0],
1471 [1, 1]
1472 ];
1473 let mNext = new Move({
1474 start: m.end,
1475 end: m.end,
1476 appear: [],
1477 vanish: []
1478 });
1479 for (let step of steps) {
1480 let x = m.end.x + step[0];
1481 let y = this.getY(m.end.y + step[1]);
1482 if (
1483 this.onBoard(x, y) &&
1484 this.board[x][y] != "" &&
1485 (x != m.start.x || y != m.start.y) &&
1486 this.getPieceType(x, y) != "p"
1487 ) {
1488 mNext.vanish.push(
1489 new PiPo({
1490 p: this.getPiece(x, y),
1491 c: this.getColor(x, y),
1492 x: x,
1493 y: y
1494 })
1495 );
1496 }
1497 }
1498 if (!this.options["rifle"]) {
1499 // The moving piece also vanish
1500 mNext.vanish.unshift(
1501 new PiPo({
1502 x: m.end.x,
1503 y: m.end.y,
1504 c: color,
1505 p: this.getPiece(m.start.x, m.start.y)
1506 })
1507 );
1508 }
1509 m.next = mNext;
1510 }
1511 });
1512 }
1513
1514 pawnPostProcess(moves, color, oppCol) {
1515 let moreMoves = [];
1516 const lastRank = (color == "w" ? 0 : this.size.x - 1);
1517 const initPiece = this.getPiece(moves[0].start.x, moves[0].start.y);
1518 moves.forEach(m => {
1519 const [x1, y1] = [m.start.x, m.start.y];
1520 const [x2, y2] = [m.end.x, m.end.y];
1521 const promotionOk = (
1522 x2 == lastRank &&
1523 (!this.options["rifle"] || this.board[x2][y2] == "")
1524 );
1525 if (!promotionOk)
1526 return; //nothing to do
1527 if (this.options["pawnfall"]) {
1528 m.appear.shift();
1529 return;
1530 }
1531 let finalPieces = ["p"];
1532 if (
1533 this.options["cannibal"] &&
1534 this.board[x2][y2] != "" &&
1535 this.getColor(x2, y2) == oppCol
1536 ) {
1537 finalPieces = [this.getPieceType(x2, y2)];
1538 }
1539 else
1540 finalPieces = this.pawnPromotions;
1541 m.appear[0].p = finalPieces[0];
1542 if (initPiece == "!") //cannibal king-pawn
1543 m.appear[0].p = C.CannibalKingCode[finalPieces[0]];
1544 for (let i=1; i<finalPieces.length; i++) {
1545 const piece = finalPieces[i];
1546 const tr = {
1547 c: color,
1548 p: (initPiece != "!" ? piece : C.CannibalKingCode[piece])
1549 };
1550 let newMove = this.getBasicMove([x1, y1], [x2, y2], tr);
1551 moreMoves.push(newMove);
1552 }
1553 });
1554 Array.prototype.push.apply(moves, moreMoves);
1555 }
1556
1557 riflePromotePostProcess(moves, color) {
1558 const lastRank = (color == "w" ? 0 : this.size.x - 1);
1559 let newMoves = [];
1560 moves.forEach(m => {
1561 if (
1562 m.start.x == lastRank &&
1563 m.appear.length >= 1 &&
1564 m.appear[0].p == "p" &&
1565 m.appear[0].x == m.start.x &&
1566 m.appear[0].y == m.start.y
1567 ) {
1568 m.appear[0].p = this.pawnPromotions[0];
1569 for (let i=1; i<this.pawnPromotions.length; i++) {
1570 let newMv = JSON.parse(JSON.stringify(m));
1571 newMv.appear[0].p = this.pawnSpecs.promotions[i];
1572 newMoves.push(newMv);
1573 }
1574 }
1575 });
1576 Array.prototype.push.apply(moves, newMoves);
1577 }
1578
1579 // Generic method to find possible moves of "sliding or jumping" pieces
1580 getPotentialMovesOf(piece, [x, y], color) {
1581 const color = this.getColor(x, y);
1582 const specialAttack = !!this.getStepSpec(color, x, y).attack;
1583 let squares = [];
1584 if (specialAttack) {
1585 squares = this.findDestSquares(
1586 [x, y],
1587 {
1588 attackOnly: true,
1589 segments: this.options["cylinder"]
1590 },
1591 ([i, j]) => {
1592 return (
1593 (!this.options["zen"] || this.getPieceType(i, j) == 'k') &&
1594 this.canTake([x, y], [i, j])
1595 );
1596 }
1597 );
1598 }
1599 const noSpecials = this.findDestSquares(
1600 [x, y],
1601 {
1602 moveOnly: specialAttack || this.options["zen"],
1603 segments: this.options["cylinder"]
1604 },
1605 ([i, j]) => this.board[i][j] == "" || this.canTake([x, y], [i, j])
1606 );
1607 Array.prototype.push.apply(squares, noSpecials);
1608 if (this.options["zen"]) {
1609 let zenCaptures = this.findCapturesOn(
1610 [x, y],
1611 {},
1612 ([i, j]) => this.getPieceType(i, j) != 'k'
1613 );
1614 // Technical step: segments (if any) are reversed
1615 if (this.options["cylinder"]) {
1616 zenCaptures.forEach(z => {
1617 if (z.segments)
1618 z.segments = z.segments.reverse().map(s => s.reverse())
1619 });
1620 }
1621 Array.prototype.push.apply(squares, zenCaptures);
1622 }
1623 if (
1624 this.options["recycle"] ||
1625 (this.options["teleport"] && this.subTurnTeleport == 1)
1626 ) {
1627 const selfCaptures = this.findDestSquares(
1628 [x, y],
1629 {
1630 attackOnly: true,
1631 segments: this.options["cylinder"]
1632 },
1633 ([i, j]) =>
1634 this.getColor(i, j) == color && this.getPieceType(i, j) != 'k'
1635 );
1636 Array.prototype.push.apply(squares, selfCaptures);
1637 }
1638 return squares.map(s => {
1639 let mv = this.getBasicMove([x, y], s.sq);
1640 if (this.options["cylinder"] && s.segments.length >= 2)
1641 mv.segments = s.segments;
1642 return mv;
1643 });
1644 }
1645
1646 findDestSquares([x, y], o, allowed) {
1647 if (!allowed)
1648 allowed = () => true;
1649 const apparentPiece = this.getPiece(x, y); //how it looks
1650 let res = [];
1651 // Next 3 for Cylinder mode: (unused if !o.segments)
1652 let explored = {};
1653 let segments = [];
1654 let segStart = [];
1655 const addSquare = ([i, j]) => {
1656 let elt = {sq: [i, j]};
1657 if (o.segments)
1658 elt.segments = this.getSegments(segments, segStart, end);
1659 res.push(elt);
1660 };
1661 const exploreSteps = (stepArray) => {
1662 for (let s of stepArray) {
1663 outerLoop: for (let step of s.steps) {
1664 if (o.segments) {
1665 segments = [];
1666 segStart = [x, y];
1667 }
1668 let [i, j] = [x, y];
1669 let stepCounter = 0;
1670 while (
1671 this.onBoard(i, j) &&
1672 ((i == x && j == y) || this.canStepOver(i, j, apparentPiece))
1673 ) {
1674 if (!explored[i + "." + j] && (i != x || j != y))
1675 {
1676 explored[i + "." + j] = true;
1677 if (
1678 allowed([i, j]) &&
1679 (
1680 !o.captureTarget ||
1681 (o.captureTarget[0] == i && o.captureTarget[1] == j)
1682 )
1683 ) {
1684 if (o.one && !o.attackOnly)
1685 return true;
1686 if (!o.attackOnly)
1687 addSquare(!o.captureTarget ? [i, j] : [x, y]);
1688 if (o.captureTarget)
1689 return res[0];
1690 }
1691 }
1692 if (s.range <= stepCounter++)
1693 continue outerLoop;
1694 const oldIJ = [i, j];
1695 i += step[0];
1696 j = this.getY(j + step[1]);
1697 if (o.segments && Math.abs(j - oldIJ[1]) > 1) {
1698 // Boundary between segments (cylinder mode)
1699 segments.push([[segStart[0], segStart[1]], oldIJ]);
1700 segStart = [i, j];
1701 }
1702 }
1703 if (!this.onBoard(i, j))
1704 continue;
1705 const pieceIJ = this.getPieceType(i, j);
1706 if (!explored[i + "." + j]) {
1707 explored[i + "." + j] = true;
1708 if (allowed([i, j])) {
1709 if (o.one && !o.moveOnly)
1710 return true;
1711 if (!o.moveOnly)
1712 addSquare(!o.captureTarget ? [i, j] : [x, y]);
1713 if (
1714 o.captureTarget &&
1715 o.captureTarget[0] == i && o.captureTarget[1] == j
1716 ) {
1717 return res[0];
1718 }
1719 }
1720 }
1721 }
1722 }
1723 };
1724 if (o.captureTarget)
1725 exploreSteps(o.captureSteps)
1726 else {
1727 const stepSpec = this.getStepSpec(this.getColor(x, y), x, y);
1728 if (!o.attackOnly || !stepSpec.attack)
1729 exploreSteps(stepSpec.moves);
1730 if (!o.moveOnly && !!stepSpec.attack)
1731 exploreSteps(stepSpec.attack);
1732 }
1733 return o.captureTarget ? null : (o.one ? false : res);
1734 }
1735
1736 // Search for enemy (or not) pieces attacking [x, y]
1737 findCapturesOn([x, y], o, allowed) {
1738 if (!allowed)
1739 allowed = () => true;
1740 let res = [];
1741 if (!o.byCol)
1742 o.byCol = [C.GetOppCol(this.getColor(x, y) || this.turn)];
1743 for (let i=0; i<this.size.x; i++) {
1744 for (let j=0; j<this.size.y; j++) {
1745 const colIJ = this.getColor(i, j);
1746 if (
1747 this.board[i][j] != "" &&
1748 o.byCol.includes(colIJ) &&
1749 !this.isImmobilized([i, j])
1750 ) {
1751 const apparentPiece = this.getPiece(i, j);
1752 // Quick check: does this potential attacker target x,y ?
1753 if (this.canStepOver(x, y, apparentPiece))
1754 continue;
1755 const stepSpec = this.getStepSpec(colIJ, i, j);
1756 const attacks = stepSpec.attack || stepSpec.moves;
1757 for (let a of attacks) {
1758 for (let s of a.steps) {
1759 // Quick check: if step isn't compatible, don't even try
1760 if (!this.compatibleStep([i, j], [x, y], s, a.range))
1761 continue;
1762 // Finally verify that nothing stand in-between
1763 const newSquare = this.findDestSquares(
1764 [i, j],
1765 {
1766 captureTarget: [x, y],
1767 captureSteps: [{steps: [s], range: a.range}],
1768 segments: this.options["cylinder"],
1769 attackOnly: true
1770 },
1771 ([ii, jj]) => this.canTake([ii, jj], [x, y])
1772 );
1773 if (newSquare)
1774 if (o.one)
1775 return true;
1776 res.push(newSquare);
1777 }
1778 }
1779 }
1780 }
1781 }
1782 }
1783 return (one ? false : res);
1784 }
1785
1786 // Build a regular move from its initial and destination squares.
1787 // tr: transformation
1788 getBasicMove([sx, sy], [ex, ey], tr) {
1789 const initColor = this.getColor(sx, sy);
1790 const initPiece = this.getPiece(sx, sy);
1791 const destColor = (this.board[ex][ey] != "" ? this.getColor(ex, ey) : "");
1792 let mv = new Move({
1793 appear: [],
1794 vanish: [],
1795 start: {x: sx, y: sy},
1796 end: {x: ex, y: ey}
1797 });
1798 if (
1799 !this.options["rifle"] ||
1800 this.board[ex][ey] == "" ||
1801 destColor == initColor //Recycle, Teleport
1802 ) {
1803 mv.appear = [
1804 new PiPo({
1805 x: ex,
1806 y: ey,
1807 c: !!tr ? tr.c : initColor,
1808 p: !!tr ? tr.p : initPiece
1809 })
1810 ];
1811 mv.vanish = [
1812 new PiPo({
1813 x: sx,
1814 y: sy,
1815 c: initColor,
1816 p: initPiece
1817 })
1818 ];
1819 }
1820 if (this.board[ex][ey] != "") {
1821 mv.vanish.push(
1822 new PiPo({
1823 x: ex,
1824 y: ey,
1825 c: this.getColor(ex, ey),
1826 p: this.getPiece(ex, ey)
1827 })
1828 );
1829 if (this.options["cannibal"] && destColor != initColor) {
1830 const lastIdx = mv.vanish.length - 1;
1831 let trPiece = mv.vanish[lastIdx].p;
1832 if (this.getPieceType(sx, sy) == 'k')
1833 trPiece = C.CannibalKingCode[trPiece];
1834 if (mv.appear.length >= 1)
1835 mv.appear[0].p = trPiece;
1836 else if (this.options["rifle"]) {
1837 mv.appear.unshift(
1838 new PiPo({
1839 x: sx,
1840 y: sy,
1841 c: initColor,
1842 p: trPiece
1843 })
1844 );
1845 mv.vanish.unshift(
1846 new PiPo({
1847 x: sx,
1848 y: sy,
1849 c: initColor,
1850 p: initPiece
1851 })
1852 );
1853 }
1854 }
1855 }
1856 return mv;
1857 }
1858
1859 // En-passant square, if any
1860 getEpSquare(moveOrSquare) {
1861 if (typeof moveOrSquare === "string") {
1862 const square = moveOrSquare;
1863 if (square == "-")
1864 return undefined;
1865 return C.SquareToCoords(square);
1866 }
1867 // Argument is a move:
1868 const move = moveOrSquare;
1869 const s = move.start,
1870 e = move.end;
1871 if (
1872 s.y == e.y &&
1873 Math.abs(s.x - e.x) == 2 &&
1874 // Next conditions for variants like Atomic or Rifle, Recycle...
1875 (
1876 move.appear.length > 0 &&
1877 this.getPieceType(0, 0, move.appear[0].p) == "p"
1878 )
1879 &&
1880 (
1881 move.vanish.length > 0 &&
1882 this.getPieceType(0, 0, move.vanish[0].p) == "p"
1883 )
1884 ) {
1885 return {
1886 x: (s.x + e.x) / 2,
1887 y: s.y
1888 };
1889 }
1890 return undefined; //default
1891 }
1892
1893 // Special case of en-passant captures: treated separately
1894 getEnpassantCaptures([x, y]) {
1895 const color = this.getColor(x, y);
1896 const shiftX = (color == 'w' ? -1 : 1);
1897 const oppCol = C.GetOppCol(color);
1898 let enpassantMove = null;
1899 if (
1900 !!this.epSquare &&
1901 this.epSquare.x == x + shiftX &&
1902 Math.abs(this.getY(this.epSquare.y - y)) == 1 &&
1903 this.getColor(x, this.epSquare.y) == oppCol //Doublemove guard...
1904 ) {
1905 const [epx, epy] = [this.epSquare.x, this.epSquare.y];
1906 this.board[epx][epy] = oppCol + "p";
1907 enpassantMove = this.getBasicMove([x, y], [epx, epy]);
1908 this.board[epx][epy] = "";
1909 const lastIdx = enpassantMove.vanish.length - 1; //think Rifle
1910 enpassantMove.vanish[lastIdx].x = x;
1911 }
1912 return !!enpassantMove ? [enpassantMove] : [];
1913 }
1914
1915 // "castleInCheck" arg to let some variants castle under check
1916 getCastleMoves([x, y], finalSquares, castleInCheck, castleWith) {
1917 const c = this.getColor(x, y);
1918
1919 // Castling ?
1920 const oppCol = C.GetOppCol(c);
1921 let moves = [];
1922 // King, then rook:
1923 finalSquares =
1924 finalSquares || [ [2, 3], [this.size.y - 2, this.size.y - 3] ];
1925 const castlingKing = this.getPiece(x, y);
1926 castlingCheck: for (
1927 let castleSide = 0;
1928 castleSide < 2;
1929 castleSide++ //large, then small
1930 ) {
1931 if (this.castleFlags[c][castleSide] >= this.size.y)
1932 continue;
1933 // If this code is reached, rook and king are on initial position
1934
1935 // NOTE: in some variants this is not a rook
1936 const rookPos = this.castleFlags[c][castleSide];
1937 const castlingPiece = this.getPiece(x, rookPos);
1938 if (
1939 this.board[x][rookPos] == "" ||
1940 this.getColor(x, rookPos) != c ||
1941 (!!castleWith && !castleWith.includes(castlingPiece))
1942 ) {
1943 // Rook is not here, or changed color (see Benedict)
1944 continue;
1945 }
1946 // Nothing on the path of the king ? (and no checks)
1947 const finDist = finalSquares[castleSide][0] - y;
1948 let step = finDist / Math.max(1, Math.abs(finDist));
1949 let i = y;
1950 do {
1951 if (
1952 (!castleInCheck && this.underCheck([x, i], oppCol)) ||
1953 (
1954 this.board[x][i] != "" &&
1955 // NOTE: next check is enough, because of chessboard constraints
1956 (this.getColor(x, i) != c || ![rookPos, y].includes(i))
1957 )
1958 ) {
1959 continue castlingCheck;
1960 }
1961 i += step;
1962 } while (i != finalSquares[castleSide][0]);
1963 // Nothing on the path to the rook?
1964 step = (castleSide == 0 ? -1 : 1);
1965 for (i = y + step; i != rookPos; i += step) {
1966 if (this.board[x][i] != "")
1967 continue castlingCheck;
1968 }
1969
1970 // Nothing on final squares, except maybe king and castling rook?
1971 for (i = 0; i < 2; i++) {
1972 if (
1973 finalSquares[castleSide][i] != rookPos &&
1974 this.board[x][finalSquares[castleSide][i]] != "" &&
1975 (
1976 finalSquares[castleSide][i] != y ||
1977 this.getColor(x, finalSquares[castleSide][i]) != c
1978 )
1979 ) {
1980 continue castlingCheck;
1981 }
1982 }
1983
1984 // If this code is reached, castle is valid
1985 moves.push(
1986 new Move({
1987 appear: [
1988 new PiPo({
1989 x: x,
1990 y: finalSquares[castleSide][0],
1991 p: castlingKing,
1992 c: c
1993 }),
1994 new PiPo({
1995 x: x,
1996 y: finalSquares[castleSide][1],
1997 p: castlingPiece,
1998 c: c
1999 })
2000 ],
2001 vanish: [
2002 // King might be initially disguised (Titan...)
2003 new PiPo({ x: x, y: y, p: castlingKing, c: c }),
2004 new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
2005 ],
2006 end:
2007 Math.abs(y - rookPos) <= 2
2008 ? {x: x, y: rookPos}
2009 : {x: x, y: y + 2 * (castleSide == 0 ? -1 : 1)}
2010 })
2011 );
2012 }
2013
2014 return moves;
2015 }
2016
2017 ////////////////////
2018 // MOVES VALIDATION
2019
2020 // Is piece (or square) at given position attacked by "oppCol" ?
2021 underAttack([x, y], oppCol) {
2022 const king = this.getPieceType(x, y) == 'k';
2023 return (
2024 (
2025 (!this.options["zen"] || king) &&
2026 this.findCapturesOn([x, y],
2027 {oppCol: oppCol, segments: this.options["cylinder"], one: true},
2028 ([i, j]) => this.canTake([i, j], [x, y])).length == 1
2029 )
2030 ||
2031 (
2032 (this.options["zen"] && !king) &&
2033 this.findDestSquares([x, y],
2034 {attackOnly: true, segments: this.options["cylinder"], one: true},
2035 ([i, j]) => this.canTake([i, j], [x, y])).length == 1
2036 )
2037 );
2038 }
2039
2040 underCheck([x, y], oppCol) {
2041 if (this.options["taking"] || this.options["dark"])
2042 return false;
2043 return this.underAttack([x, y], oppCol);
2044 }
2045
2046 // Stop at first king found (TODO: multi-kings)
2047 searchKingPos(color) {
2048 for (let i=0; i < this.size.x; i++) {
2049 for (let j=0; j < this.size.y; j++) {
2050 if (this.getColor(i, j) == color && this.getPieceType(i, j) == 'k')
2051 return [i, j];
2052 }
2053 }
2054 return [-1, -1]; //king not found
2055 }
2056
2057 // 'color' arg because some variants (e.g. Refusal) check opponent moves
2058 filterValid(moves, color) {
2059 if (!color)
2060 color = this.turn;
2061 const oppCol = C.GetOppCol(color);
2062 const kingPos = this.searchKingPos(color);
2063 let filtered = {}; //avoid re-checking similar moves (promotions...)
2064 return moves.filter(m => {
2065 const key = m.start.x + m.start.y + '.' + m.end.x + m.end.y;
2066 if (!filtered[key]) {
2067 this.playOnBoard(m);
2068 let square = kingPos,
2069 res = true; //a priori valid
2070 if (m.vanish.some(v => {
2071 return this.getPieceType(0, 0, v.p) == 'k' && v.c == color;
2072 })) {
2073 // Search king in appear array:
2074 const newKingIdx =
2075 m.appear.findIndex(a => {
2076 return this.getPieceType(0, 0, a.p) == 'k' && a.c == color;
2077 });
2078 if (newKingIdx >= 0)
2079 square = [m.appear[newKingIdx].x, m.appear[newKingIdx].y];
2080 else
2081 res = false;
2082 }
2083 res &&= !this.underCheck(square, oppCol);
2084 this.undoOnBoard(m);
2085 filtered[key] = res;
2086 return res;
2087 }
2088 return filtered[key];
2089 });
2090 }
2091
2092 /////////////////
2093 // MOVES PLAYING
2094
2095 // Aggregate flags into one object
2096 aggregateFlags() {
2097 return this.castleFlags;
2098 }
2099
2100 // Reverse operation
2101 disaggregateFlags(flags) {
2102 this.castleFlags = flags;
2103 }
2104
2105 // Apply a move on board
2106 playOnBoard(move) {
2107 for (let psq of move.vanish)
2108 this.board[psq.x][psq.y] = "";
2109 for (let psq of move.appear)
2110 this.board[psq.x][psq.y] = psq.c + psq.p;
2111 }
2112 // Un-apply the played move
2113 undoOnBoard(move) {
2114 for (let psq of move.appear)
2115 this.board[psq.x][psq.y] = "";
2116 for (let psq of move.vanish)
2117 this.board[psq.x][psq.y] = psq.c + psq.p;
2118 }
2119
2120 updateCastleFlags(move) {
2121 // Update castling flags if start or arrive from/at rook/king locations
2122 move.appear.concat(move.vanish).forEach(psq => {
2123 if (
2124 this.board[psq.x][psq.y] != "" &&
2125 this.getPieceType(psq.x, psq.y) == "k"
2126 ) {
2127 this.castleFlags[psq.c] = [this.size.y, this.size.y];
2128 }
2129 // NOTE: not "else if" because king can capture enemy rook...
2130 let c = "";
2131 if (psq.x == 0)
2132 c = "b";
2133 else if (psq.x == this.size.x - 1)
2134 c = "w";
2135 if (c != "") {
2136 const fidx = this.castleFlags[c].findIndex(f => f == psq.y);
2137 if (fidx >= 0)
2138 this.castleFlags[c][fidx] = this.size.y;
2139 }
2140 });
2141 }
2142
2143 prePlay(move) {
2144 if (
2145 this.hasCastle &&
2146 // If flags already off, no need to re-check:
2147 Object.keys(this.castleFlags).some(c => {
2148 return this.castleFlags[c].some(val => val < this.size.y)})
2149 ) {
2150 this.updateCastleFlags(move);
2151 }
2152 if (this.options["crazyhouse"]) {
2153 move.vanish.forEach(v => {
2154 const square = C.CoordsToSquare({x: v.x, y: v.y});
2155 if (this.ispawn[square])
2156 delete this.ispawn[square];
2157 });
2158 if (move.appear.length > 0 && move.vanish.length > 0) {
2159 // Assumption: something is moving
2160 const initSquare = C.CoordsToSquare(move.start);
2161 const destSquare = C.CoordsToSquare(move.end);
2162 if (
2163 this.ispawn[initSquare] ||
2164 (move.vanish[0].p == "p" && move.appear[0].p != "p")
2165 ) {
2166 this.ispawn[destSquare] = true;
2167 }
2168 else if (
2169 this.ispawn[destSquare] &&
2170 this.getColor(move.end.x, move.end.y) != move.vanish[0].c
2171 ) {
2172 move.vanish[1].p = "p";
2173 delete this.ispawn[destSquare];
2174 }
2175 }
2176 }
2177 const minSize = Math.min(move.appear.length, move.vanish.length);
2178 if (
2179 this.hasReserve &&
2180 // Warning; atomic pawn removal isn't a capture
2181 (!this.options["atomic"] || !this.rempawn || this.movesCount >= 1)
2182 ) {
2183 const color = this.turn;
2184 for (let i=minSize; i<move.appear.length; i++) {
2185 // Something appears = dropped on board (some exceptions, Chakart...)
2186 if (move.appear[i].c == color) {
2187 const piece = move.appear[i].p;
2188 this.updateReserve(color, piece, this.reserve[color][piece] - 1);
2189 }
2190 }
2191 for (let i=minSize; i<move.vanish.length; i++) {
2192 // Something vanish: add to reserve except if recycle & opponent
2193 if (
2194 this.options["crazyhouse"] ||
2195 (this.options["recycle"] && move.vanish[i].c == color)
2196 ) {
2197 const piece = move.vanish[i].p;
2198 this.updateReserve(color, piece, this.reserve[color][piece] + 1);
2199 }
2200 }
2201 }
2202 }
2203
2204 play(move) {
2205 this.prePlay(move);
2206 if (this.hasEnpassant)
2207 this.epSquare = this.getEpSquare(move);
2208 this.playOnBoard(move);
2209 this.postPlay(move);
2210 }
2211
2212 postPlay(move) {
2213 const color = this.turn;
2214 if (this.options["dark"])
2215 this.updateEnlightened();
2216 if (this.options["teleport"]) {
2217 if (
2218 this.subTurnTeleport == 1 &&
2219 move.vanish.length > move.appear.length &&
2220 move.vanish[1].c == color
2221 ) {
2222 const v = move.vanish[move.vanish.length - 1];
2223 this.captured = {x: v.x, y: v.y, c: v.c, p: v.p};
2224 this.subTurnTeleport = 2;
2225 return;
2226 }
2227 this.subTurnTeleport = 1;
2228 this.captured = null;
2229 }
2230 if (this.isLastMove(move)) {
2231 this.turn = oppCol;
2232 this.movesCount++;
2233 this.subTurn = 1;
2234 }
2235 else if (!move.next)
2236 this.subTurn++;
2237 }
2238
2239 isLastMove(move) {
2240 if (move.next)
2241 return false;
2242 const color = this.turn;
2243 const oppCol = C.GetOppCol(color);
2244 const oppKingPos = this.searchKingPos(oppCol);
2245 if (oppKingPos[0] < 0 || this.underCheck(oppKingPos, color))
2246 return true;
2247 return (
2248 (
2249 !this.options["balance"] ||
2250 ![1, 3].includes(this.movesCount)
2251 )
2252 &&
2253 (
2254 !this.options["doublemove"] ||
2255 this.movesCount == 0 ||
2256 this.subTurn == 2
2257 )
2258 &&
2259 (
2260 !this.options["progressive"] ||
2261 this.subTurn == this.movesCount + 1
2262 )
2263 );
2264 }
2265
2266 // "Stop at the first move found"
2267 atLeastOneMove(color) {
2268 color = color || this.turn;
2269 for (let i = 0; i < this.size.x; i++) {
2270 for (let j = 0; j < this.size.y; j++) {
2271 if (this.board[i][j] != "" && this.getColor(i, j) == color) {
2272 // NOTE: in fact searching for all potential moves from i,j.
2273 // I don't believe this is an issue, for now at least.
2274 const moves = this.getPotentialMovesFrom([i, j]);
2275 if (moves.some(m => this.filterValid([m]).length >= 1))
2276 return true;
2277 }
2278 }
2279 }
2280 if (this.hasReserve && this.reserve[color]) {
2281 for (let p of Object.keys(this.reserve[color])) {
2282 const moves = this.getDropMovesFrom([color, p]);
2283 if (moves.some(m => this.filterValid([m]).length >= 1))
2284 return true;
2285 }
2286 }
2287 return false;
2288 }
2289
2290 // What is the score ? (Interesting if game is over)
2291 getCurrentScore(move) {
2292 const color = this.turn;
2293 const oppCol = C.GetOppCol(color);
2294 const kingPos = [this.searchKingPos(color), this.searchKingPos(oppCol)];
2295 if (kingPos[0][0] < 0 && kingPos[1][0] < 0)
2296 return "1/2";
2297 if (kingPos[0][0] < 0)
2298 return (color == "w" ? "0-1" : "1-0");
2299 if (kingPos[1][0] < 0)
2300 return (color == "w" ? "1-0" : "0-1");
2301 if (this.atLeastOneMove())
2302 return "*";
2303 // No valid move: stalemate or checkmate?
2304 if (!this.underCheck(kingPos[0], color))
2305 return "1/2";
2306 // OK, checkmate
2307 return (color == "w" ? "0-1" : "1-0");
2308 }
2309
2310 playVisual(move, r) {
2311 move.vanish.forEach(v => {
2312 this.g_pieces[v.x][v.y].remove();
2313 this.g_pieces[v.x][v.y] = null;
2314 });
2315 let chessboard =
2316 document.getElementById(this.containerId).querySelector(".chessboard");
2317 if (!r)
2318 r = chessboard.getBoundingClientRect();
2319 const pieceWidth = this.getPieceWidth(r.width);
2320 move.appear.forEach(a => {
2321 this.g_pieces[a.x][a.y] = document.createElement("piece");
2322 C.AddClass_es(this.g_pieces[a.x][a.y],
2323 this.pieces(a.c, a.x, a.y)[a.p]["class"]);
2324 this.g_pieces[a.x][a.y].classList.add(C.GetColorClass(a.c));
2325 this.g_pieces[a.x][a.y].style.width = pieceWidth + "px";
2326 this.g_pieces[a.x][a.y].style.height = pieceWidth + "px";
2327 const [ip, jp] = this.getPixelPosition(a.x, a.y, r);
2328 // Translate coordinates to use chessboard as reference:
2329 this.g_pieces[a.x][a.y].style.transform =
2330 `translate(${ip - r.x}px,${jp - r.y}px)`;
2331 if (this.enlightened && !this.enlightened[a.x][a.y])
2332 this.g_pieces[a.x][a.y].classList.add("hidden");
2333 chessboard.appendChild(this.g_pieces[a.x][a.y]);
2334 });
2335 if (this.options["dark"])
2336 this.graphUpdateEnlightened();
2337 }
2338
2339 // TODO: send stack receive stack, or allow incremental? (good/bad points)
2340 buildMoveStack(move, r) {
2341 this.moveStack.push(move);
2342 this.computeNextMove(move);
2343 this.play(move);
2344 const newTurn = this.turn;
2345 if (this.moveStack.length == 1)
2346 this.playVisual(move, r);
2347 if (move.next) {
2348 this.gameState = {
2349 fen: this.getFen(),
2350 board: JSON.parse(JSON.stringify(this.board)) //easier
2351 };
2352 this.buildMoveStack(move.next, r);
2353 }
2354 else {
2355 if (this.moveStack.length == 1) {
2356 // Usual case (one normal move)
2357 this.afterPlay(this.moveStack, newTurn, {send: true, res: true});
2358 this.moveStack = []
2359 }
2360 else {
2361 this.afterPlay(this.moveStack, newTurn, {send: true, res: false});
2362 this.re_initFromFen(this.gameState.fen, this.gameState.board);
2363 this.playReceivedMove(this.moveStack.slice(1), () => {
2364 this.afterPlay(this.moveStack, newTurn, {send: false, res: true});
2365 this.moveStack = []
2366 });
2367 }
2368 }
2369 }
2370
2371 // Implemented in variants using (automatic) moveStack
2372 computeNextMove(move) {}
2373
2374 getMaxDistance(r) {
2375 // Works for all rectangular boards:
2376 return Math.sqrt(r.width ** 2 + r.height ** 2);
2377 }
2378
2379 getDomPiece(x, y) {
2380 return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y];
2381 }
2382
2383 animateMoving(start, end, drag, segments, cb) {
2384 let initPiece = this.getDomPiece(start.x, start.y);
2385 // NOTE: cloning often not required, but light enough, and simpler
2386 let movingPiece = initPiece.cloneNode();
2387 initPiece.style.opacity = "0";
2388 let container =
2389 document.getElementById(this.containerId)
2390 const r = container.querySelector(".chessboard").getBoundingClientRect();
2391 if (typeof start.x == "string") {
2392 // Need to bound width/height (was 100% for reserve pieces)
2393 const pieceWidth = this.getPieceWidth(r.width);
2394 movingPiece.style.width = pieceWidth + "px";
2395 movingPiece.style.height = pieceWidth + "px";
2396 }
2397 const maxDist = this.getMaxDistance(r);
2398 const apparentColor = this.getColor(start.x, start.y);
2399 const pieces = this.pieces(apparentColor, start.x, start.y);
2400 if (drag) {
2401 const startCode = this.getPiece(start.x, start.y);
2402 C.RemoveClass_es(movingPiece, pieces[startCode]["class"]);
2403 C.AddClass_es(movingPiece, pieces[drag.p]["class"]);
2404 if (apparentColor != drag.c) {
2405 movingPiece.classList.remove(C.GetColorClass(apparentColor));
2406 movingPiece.classList.add(C.GetColorClass(drag.c));
2407 }
2408 }
2409 container.appendChild(movingPiece);
2410 const animateSegment = (index, cb) => {
2411 // NOTE: move.drag could be generalized per-segment (usage?)
2412 const [i1, j1] = segments[index][0];
2413 const [i2, j2] = segments[index][1];
2414 const dep = this.getPixelPosition(i1, j1, r);
2415 const arr = this.getPixelPosition(i2, j2, r);
2416 movingPiece.style.transitionDuration = "0s";
2417 movingPiece.style.transform = `translate(${dep[0]}px, ${dep[1]}px)`;
2418 const distance =
2419 Math.sqrt((arr[0] - dep[0]) ** 2 + (arr[1] - dep[1]) ** 2);
2420 const duration = 0.2 + (distance / maxDist) * 0.3;
2421 // TODO: unclear why we need this new delay below:
2422 setTimeout(() => {
2423 movingPiece.style.transitionDuration = duration + "s";
2424 // movingPiece is child of container: no need to adjust coordinates
2425 movingPiece.style.transform = `translate(${arr[0]}px, ${arr[1]}px)`;
2426 setTimeout(cb, duration * 1000);
2427 }, 50);
2428 };
2429 let index = 0;
2430 const animateSegmentCallback = () => {
2431 if (index < segments.length)
2432 animateSegment(index++, animateSegmentCallback);
2433 else {
2434 movingPiece.remove();
2435 initPiece.style.opacity = "1";
2436 cb();
2437 }
2438 };
2439 animateSegmentCallback();
2440 }
2441
2442 // Input array of objects with at least fields x,y (e.g. PiPo)
2443 animateFading(arr, cb) {
2444 const animLength = 350; //TODO: 350ms? More? Less?
2445 arr.forEach(v => {
2446 let fadingPiece = this.getDomPiece(v.x, v.y);
2447 fadingPiece.style.transitionDuration = (animLength / 1000) + "s";
2448 fadingPiece.style.opacity = "0";
2449 });
2450 setTimeout(cb, animLength);
2451 }
2452
2453 animate(move, callback) {
2454 if (this.noAnimate || move.noAnimate) {
2455 callback();
2456 return;
2457 }
2458 let segments = move.segments;
2459 if (!segments)
2460 segments = [ [[move.start.x, move.start.y], [move.end.x, move.end.y]] ];
2461 let targetObj = new TargetObj(callback);
2462 if (move.start.x != move.end.x || move.start.y != move.end.y) {
2463 targetObj.target++;
2464 this.animateMoving(move.start, move.end, move.drag, segments,
2465 () => targetObj.increment());
2466 }
2467 if (move.vanish.length > move.appear.length) {
2468 const arr = move.vanish.slice(move.appear.length)
2469 // Ignore disappearing pieces hidden by some appearing ones:
2470 .filter(v => move.appear.every(a => a.x != v.x || a.y != v.y));
2471 if (arr.length > 0) {
2472 targetObj.target++;
2473 this.animateFading(arr, () => targetObj.increment());
2474 }
2475 }
2476 targetObj.target +=
2477 this.customAnimate(move, segments, () => targetObj.increment());
2478 if (targetObj.target == 0)
2479 callback();
2480 }
2481
2482 // Potential other animations (e.g. for Suction variant)
2483 customAnimate(move, segments, cb) {
2484 return 0; //nb of targets
2485 }
2486
2487 playReceivedMove(moves, callback) {
2488 const launchAnimation = () => {
2489 const r = container.querySelector(".chessboard").getBoundingClientRect();
2490 const animateRec = i => {
2491 this.animate(moves[i], () => {
2492 this.play(moves[i]);
2493 this.playVisual(moves[i], r);
2494 if (i < moves.length - 1)
2495 setTimeout(() => animateRec(i+1), 300);
2496 else
2497 callback();
2498 });
2499 };
2500 animateRec(0);
2501 };
2502 // Delay if user wasn't focused:
2503 const checkDisplayThenAnimate = (delay) => {
2504 if (container.style.display == "none") {
2505 alert("New move! Let's go back to game...");
2506 document.getElementById("gameInfos").style.display = "none";
2507 container.style.display = "block";
2508 setTimeout(launchAnimation, 700);
2509 }
2510 else
2511 setTimeout(launchAnimation, delay || 0);
2512 };
2513 let container = document.getElementById(this.containerId);
2514 if (document.hidden) {
2515 document.onvisibilitychange = () => {
2516 document.onvisibilitychange = undefined;
2517 checkDisplayThenAnimate(700);
2518 };
2519 }
2520 else
2521 checkDisplayThenAnimate();
2522 }
2523
2524 };