Experimental change: options replacing randomness (more general)
[vchess.git] / client / src / base_rules.js
1 // (Orthodox) Chess rules are defined in ChessRules class.
2 // Variants generally inherit from it, and modify some parts.
3
4 import { ArrayFun } from "@/utils/array";
5 import { randInt, shuffle } from "@/utils/alea";
6
7 // class "PiPo": Piece + Position
8 export const PiPo = class PiPo {
9 // o: {piece[p], color[c], posX[x], posY[y]}
10 constructor(o) {
11 this.p = o.p;
12 this.c = o.c;
13 this.x = o.x;
14 this.y = o.y;
15 }
16 };
17
18 export const Move = class Move {
19 // o: {appear, vanish, [start,] [end,]}
20 // appear,vanish = arrays of PiPo
21 // start,end = coordinates to apply to trigger move visually (think castle)
22 constructor(o) {
23 this.appear = o.appear;
24 this.vanish = o.vanish;
25 this.start = o.start || { x: o.vanish[0].x, y: o.vanish[0].y };
26 this.end = o.end || { x: o.appear[0].x, y: o.appear[0].y };
27 }
28 };
29
30 // NOTE: x coords = top to bottom; y = left to right
31 // (from white player perspective)
32 export const ChessRules = class ChessRules {
33
34 //////////////
35 // MISC UTILS
36
37 static get Options() {
38 return {
39 select: [
40 {
41 label: "Randomness",
42 variable: "randomness",
43 defaut: 2,
44 options: [
45 { label: "Deterministic", value: 0 },
46 { label: "Symmetric random", value: 1 },
47 { label: "Asymmetric random", value: 2 }
48 ]
49 }
50 ],
51 check: []
52 };
53 }
54
55 static AbbreviateOptions(opts) {
56 return "";
57 // Randomness is a special option: (TODO?)
58 //return "R" + opts.randomness;
59 }
60
61 // Some variants don't have flags:
62 static get HasFlags() {
63 return true;
64 }
65
66 // Or castle
67 static get HasCastle() {
68 return V.HasFlags;
69 }
70
71 // Pawns specifications
72 static get PawnSpecs() {
73 return {
74 directions: { 'w': -1, 'b': 1 },
75 initShift: { w: 1, b: 1 },
76 twoSquares: true,
77 threeSquares: false,
78 promotions: [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN],
79 canCapture: true,
80 captureBackward: false,
81 bidirectional: false
82 };
83 }
84
85 // En-passant captures need a stack of squares:
86 static get HasEnpassant() {
87 return true;
88 }
89
90 // Some variants cannot have analyse mode
91 static get CanAnalyze() {
92 return true;
93 }
94 // Patch: issues with javascript OOP, objects can't access static fields.
95 get canAnalyze() {
96 return V.CanAnalyze;
97 }
98
99 // Some variants show incomplete information,
100 // and thus show only a partial moves list or no list at all.
101 static get ShowMoves() {
102 return "all";
103 }
104 get showMoves() {
105 return V.ShowMoves;
106 }
107
108 // Sometimes moves must remain hidden until game ends
109 static get SomeHiddenMoves() {
110 return false;
111 }
112 get someHiddenMoves() {
113 return V.SomeHiddenMoves;
114 }
115
116 // Generally true, unless the variant includes random effects
117 static get CorrConfirm() {
118 return true;
119 }
120
121 // Used for Monochrome variant (TODO: harmonize: !canFlip ==> showFirstTurn)
122 get showFirstTurn() {
123 return false;
124 }
125
126 // Some variants always show the same orientation
127 static get CanFlip() {
128 return true;
129 }
130 get canFlip() {
131 return V.CanFlip;
132 }
133
134 // For (generally old) variants without checkered board
135 static get Monochrome() {
136 return false;
137 }
138
139 // Some games are drawn unusually (bottom right corner is black)
140 static get DarkBottomRight() {
141 return false;
142 }
143
144 // Some variants require lines drawing
145 static get Lines() {
146 if (V.Monochrome) {
147 let lines = [];
148 // Draw all inter-squares lines
149 for (let i = 0; i <= V.size.x; i++)
150 lines.push([[i, 0], [i, V.size.y]]);
151 for (let j = 0; j <= V.size.y; j++)
152 lines.push([[0, j], [V.size.x, j]]);
153 return lines;
154 }
155 return null;
156 }
157
158 // In some variants, the player who repeat a position loses
159 static get LoseOnRepetition() {
160 return false;
161 }
162 // And in some others (Iceage), repetitions should be ignored:
163 static get IgnoreRepetition() {
164 return false;
165 }
166 loseOnRepetition() {
167 // In some variants, result depends on the position:
168 return V.LoseOnRepetition;
169 }
170
171 // At some stages, some games could wait clicks only:
172 onlyClick() {
173 return false;
174 }
175
176 // Some variants use click infos:
177 doClick() {
178 return null;
179 }
180
181 // Some variants may need to highlight squares on hover (Hamilton, Weiqi...)
182 hoverHighlight() {
183 return false;
184 }
185
186 static get IMAGE_EXTENSION() {
187 // All pieces should be in the SVG format
188 return ".svg";
189 }
190
191 // Turn "wb" into "B" (for FEN)
192 static board2fen(b) {
193 return b[0] == "w" ? b[1].toUpperCase() : b[1];
194 }
195
196 // Turn "p" into "bp" (for board)
197 static fen2board(f) {
198 return f.charCodeAt(0) <= 90 ? "w" + f.toLowerCase() : "b" + f;
199 }
200
201 // Check if FEN describes a board situation correctly
202 static IsGoodFen(fen) {
203 const fenParsed = V.ParseFen(fen);
204 // 1) Check position
205 if (!V.IsGoodPosition(fenParsed.position)) return false;
206 // 2) Check turn
207 if (!fenParsed.turn || !V.IsGoodTurn(fenParsed.turn)) return false;
208 // 3) Check moves count
209 if (!fenParsed.movesCount || !(parseInt(fenParsed.movesCount, 10) >= 0))
210 return false;
211 // 4) Check flags
212 if (V.HasFlags && (!fenParsed.flags || !V.IsGoodFlags(fenParsed.flags)))
213 return false;
214 // 5) Check enpassant
215 if (
216 V.HasEnpassant &&
217 (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant))
218 ) {
219 return false;
220 }
221 return true;
222 }
223
224 // Is position part of the FEN a priori correct?
225 static IsGoodPosition(position) {
226 if (position.length == 0) return false;
227 const rows = position.split("/");
228 if (rows.length != V.size.x) return false;
229 let kings = { "k": 0, "K": 0 };
230 for (let row of rows) {
231 let sumElts = 0;
232 for (let i = 0; i < row.length; i++) {
233 if (['K','k'].includes(row[i])) kings[row[i]]++;
234 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
235 else {
236 const num = parseInt(row[i], 10);
237 if (isNaN(num) || num <= 0) return false;
238 sumElts += num;
239 }
240 }
241 if (sumElts != V.size.y) return false;
242 }
243 // Both kings should be on board. Exactly one per color.
244 if (Object.values(kings).some(v => v != 1)) return false;
245 return true;
246 }
247
248 // For FEN checking
249 static IsGoodTurn(turn) {
250 return ["w", "b"].includes(turn);
251 }
252
253 // For FEN checking
254 static IsGoodFlags(flags) {
255 // NOTE: a little too permissive to work with more variants
256 return !!flags.match(/^[a-z]{4,4}$/);
257 }
258
259 // NOTE: not with regexp to adapt to different board sizes. (TODO?)
260 static IsGoodEnpassant(enpassant) {
261 if (enpassant != "-") {
262 const ep = V.SquareToCoords(enpassant);
263 if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
264 }
265 return true;
266 }
267
268 // 3 --> d (column number to letter)
269 static CoordToColumn(colnum) {
270 return String.fromCharCode(97 + colnum);
271 }
272
273 // d --> 3 (column letter to number)
274 static ColumnToCoord(column) {
275 return column.charCodeAt(0) - 97;
276 }
277
278 // a4 --> {x:3,y:0}
279 static SquareToCoords(sq) {
280 return {
281 // NOTE: column is always one char => max 26 columns
282 // row is counted from black side => subtraction
283 x: V.size.x - parseInt(sq.substr(1), 10),
284 y: sq[0].charCodeAt() - 97
285 };
286 }
287
288 // {x:0,y:4} --> e8
289 static CoordsToSquare(coords) {
290 return V.CoordToColumn(coords.y) + (V.size.x - coords.x);
291 }
292
293 // Path to pieces (standard ones in pieces/ folder)
294 getPpath(b) {
295 return b;
296 }
297
298 // Path to promotion pieces (usually the same)
299 getPPpath(m) {
300 return this.getPpath(m.appear[0].c + m.appear[0].p);
301 }
302
303 // Aggregates flags into one object
304 aggregateFlags() {
305 return this.castleFlags;
306 }
307
308 // Reverse operation
309 disaggregateFlags(flags) {
310 this.castleFlags = flags;
311 }
312
313 // En-passant square, if any
314 getEpSquare(moveOrSquare) {
315 if (!moveOrSquare) return undefined; //TODO: necessary line?!
316 if (typeof moveOrSquare === "string") {
317 const square = moveOrSquare;
318 if (square == "-") return undefined;
319 return V.SquareToCoords(square);
320 }
321 // Argument is a move:
322 const move = moveOrSquare;
323 const s = move.start,
324 e = move.end;
325 if (
326 s.y == e.y &&
327 Math.abs(s.x - e.x) == 2 &&
328 // Next conditions for variants like Atomic or Rifle, Recycle...
329 (move.appear.length > 0 && move.appear[0].p == V.PAWN) &&
330 (move.vanish.length > 0 && move.vanish[0].p == V.PAWN)
331 ) {
332 return {
333 x: (s.x + e.x) / 2,
334 y: s.y
335 };
336 }
337 return undefined; //default
338 }
339
340 // Can thing on square1 take thing on square2
341 canTake([x1, y1], [x2, y2]) {
342 return this.getColor(x1, y1) !== this.getColor(x2, y2);
343 }
344
345 // Is (x,y) on the chessboard?
346 static OnBoard(x, y) {
347 return x >= 0 && x < V.size.x && y >= 0 && y < V.size.y;
348 }
349
350 // Used in interface: 'side' arg == player color
351 canIplay(side, [x, y]) {
352 return this.turn == side && this.getColor(x, y) == side;
353 }
354
355 // On which squares is color under check ? (for interface)
356 getCheckSquares() {
357 const color = this.turn;
358 return (
359 this.underCheck(color)
360 // kingPos must be duplicated, because it may change:
361 ? [JSON.parse(JSON.stringify(this.kingPos[color]))]
362 : []
363 );
364 }
365
366 /////////////
367 // FEN UTILS
368
369 // Setup the initial random (asymmetric) position
370 static GenRandInitFen(options) {
371 if (!options.randomness || options.randomness == 0)
372 // Deterministic:
373 return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 ahah -";
374
375 let pieces = { w: new Array(8), b: new Array(8) };
376 let flags = "";
377 // Shuffle pieces on first (and last rank if randomness == 2)
378 for (let c of ["w", "b"]) {
379 if (c == 'b' && options.randomness == 1) {
380 pieces['b'] = pieces['w'];
381 flags += flags;
382 break;
383 }
384
385 let positions = ArrayFun.range(8);
386
387 // Get random squares for bishops
388 let randIndex = 2 * randInt(4);
389 const bishop1Pos = positions[randIndex];
390 // The second bishop must be on a square of different color
391 let randIndex_tmp = 2 * randInt(4) + 1;
392 const bishop2Pos = positions[randIndex_tmp];
393 // Remove chosen squares
394 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
395 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
396
397 // Get random squares for knights
398 randIndex = randInt(6);
399 const knight1Pos = positions[randIndex];
400 positions.splice(randIndex, 1);
401 randIndex = randInt(5);
402 const knight2Pos = positions[randIndex];
403 positions.splice(randIndex, 1);
404
405 // Get random square for queen
406 randIndex = randInt(4);
407 const queenPos = positions[randIndex];
408 positions.splice(randIndex, 1);
409
410 // Rooks and king positions are now fixed,
411 // because of the ordering rook-king-rook
412 const rook1Pos = positions[0];
413 const kingPos = positions[1];
414 const rook2Pos = positions[2];
415
416 // Finally put the shuffled pieces in the board array
417 pieces[c][rook1Pos] = "r";
418 pieces[c][knight1Pos] = "n";
419 pieces[c][bishop1Pos] = "b";
420 pieces[c][queenPos] = "q";
421 pieces[c][kingPos] = "k";
422 pieces[c][bishop2Pos] = "b";
423 pieces[c][knight2Pos] = "n";
424 pieces[c][rook2Pos] = "r";
425 flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(rook2Pos);
426 }
427 // Add turn + flags + enpassant
428 return (
429 pieces["b"].join("") +
430 "/pppppppp/8/8/8/8/PPPPPPPP/" +
431 pieces["w"].join("").toUpperCase() +
432 " w 0 " + flags + " -"
433 );
434 }
435
436 // "Parse" FEN: just return untransformed string data
437 static ParseFen(fen) {
438 const fenParts = fen.split(" ");
439 let res = {
440 position: fenParts[0],
441 turn: fenParts[1],
442 movesCount: fenParts[2]
443 };
444 let nextIdx = 3;
445 if (V.HasFlags) Object.assign(res, { flags: fenParts[nextIdx++] });
446 if (V.HasEnpassant) Object.assign(res, { enpassant: fenParts[nextIdx] });
447 return res;
448 }
449
450 // Return current fen (game state)
451 getFen() {
452 return (
453 this.getBaseFen() + " " +
454 this.getTurnFen() + " " +
455 this.movesCount +
456 (V.HasFlags ? " " + this.getFlagsFen() : "") +
457 (V.HasEnpassant ? " " + this.getEnpassantFen() : "")
458 );
459 }
460
461 getFenForRepeat() {
462 // Omit movesCount, only variable allowed to differ
463 return (
464 this.getBaseFen() + "_" +
465 this.getTurnFen() +
466 (V.HasFlags ? "_" + this.getFlagsFen() : "") +
467 (V.HasEnpassant ? "_" + this.getEnpassantFen() : "")
468 );
469 }
470
471 // Position part of the FEN string
472 getBaseFen() {
473 const format = (count) => {
474 // if more than 9 consecutive free spaces, break the integer,
475 // otherwise FEN parsing will fail.
476 if (count <= 9) return count;
477 // Most boards of size < 18:
478 if (count <= 18) return "9" + (count - 9);
479 // Except Gomoku:
480 return "99" + (count - 18);
481 };
482 let position = "";
483 for (let i = 0; i < V.size.x; i++) {
484 let emptyCount = 0;
485 for (let j = 0; j < V.size.y; j++) {
486 if (this.board[i][j] == V.EMPTY) emptyCount++;
487 else {
488 if (emptyCount > 0) {
489 // Add empty squares in-between
490 position += format(emptyCount);
491 emptyCount = 0;
492 }
493 position += V.board2fen(this.board[i][j]);
494 }
495 }
496 if (emptyCount > 0) {
497 // "Flush remainder"
498 position += format(emptyCount);
499 }
500 if (i < V.size.x - 1) position += "/"; //separate rows
501 }
502 return position;
503 }
504
505 getTurnFen() {
506 return this.turn;
507 }
508
509 // Flags part of the FEN string
510 getFlagsFen() {
511 let flags = "";
512 // Castling flags
513 for (let c of ["w", "b"])
514 flags += this.castleFlags[c].map(V.CoordToColumn).join("");
515 return flags;
516 }
517
518 // Enpassant part of the FEN string
519 getEnpassantFen() {
520 const L = this.epSquares.length;
521 if (!this.epSquares[L - 1]) return "-"; //no en-passant
522 return V.CoordsToSquare(this.epSquares[L - 1]);
523 }
524
525 // Turn position fen into double array ["wb","wp","bk",...]
526 static GetBoard(position) {
527 const rows = position.split("/");
528 let board = ArrayFun.init(V.size.x, V.size.y, "");
529 for (let i = 0; i < rows.length; i++) {
530 let j = 0;
531 for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
532 const character = rows[i][indexInRow];
533 const num = parseInt(character, 10);
534 // If num is a number, just shift j:
535 if (!isNaN(num)) j += num;
536 // Else: something at position i,j
537 else board[i][j++] = V.fen2board(character);
538 }
539 }
540 return board;
541 }
542
543 // Extract (relevant) flags from fen
544 setFlags(fenflags) {
545 // white a-castle, h-castle, black a-castle, h-castle
546 this.castleFlags = { w: [-1, -1], b: [-1, -1] };
547 for (let i = 0; i < 4; i++) {
548 this.castleFlags[i < 2 ? "w" : "b"][i % 2] =
549 V.ColumnToCoord(fenflags.charAt(i));
550 }
551 }
552
553 //////////////////
554 // INITIALIZATION
555
556 // Fen string fully describes the game state
557 constructor(fen) {
558 if (!fen)
559 // In printDiagram() fen isn't supply because only getPpath() is used
560 // TODO: find a better solution!
561 return;
562 const fenParsed = V.ParseFen(fen);
563 this.board = V.GetBoard(fenParsed.position);
564 this.turn = fenParsed.turn;
565 this.movesCount = parseInt(fenParsed.movesCount, 10);
566 this.setOtherVariables(fen);
567 }
568
569 // Scan board for kings positions
570 // TODO: should be done from board, no need for the complete FEN
571 scanKings(fen) {
572 // Squares of white and black king:
573 this.kingPos = { w: [-1, -1], b: [-1, -1] };
574 const fenRows = V.ParseFen(fen).position.split("/");
575 for (let i = 0; i < fenRows.length; i++) {
576 let k = 0; //column index on board
577 for (let j = 0; j < fenRows[i].length; j++) {
578 switch (fenRows[i].charAt(j)) {
579 case "k":
580 this.kingPos["b"] = [i, k];
581 break;
582 case "K":
583 this.kingPos["w"] = [i, k];
584 break;
585 default: {
586 const num = parseInt(fenRows[i].charAt(j), 10);
587 if (!isNaN(num)) k += num - 1;
588 }
589 }
590 k++;
591 }
592 }
593 }
594
595 // Some additional variables from FEN (variant dependant)
596 setOtherVariables(fen) {
597 // Set flags and enpassant:
598 const parsedFen = V.ParseFen(fen);
599 if (V.HasFlags) this.setFlags(parsedFen.flags);
600 if (V.HasEnpassant) {
601 const epSq =
602 parsedFen.enpassant != "-"
603 ? this.getEpSquare(parsedFen.enpassant)
604 : undefined;
605 this.epSquares = [epSq];
606 }
607 // Search for kings positions:
608 this.scanKings(fen);
609 }
610
611 /////////////////////
612 // GETTERS & SETTERS
613
614 static get size() {
615 return { x: 8, y: 8 };
616 }
617
618 // Color of thing on square (i,j). 'undefined' if square is empty
619 getColor(i, j) {
620 return this.board[i][j].charAt(0);
621 }
622
623 // Piece type on square (i,j). 'undefined' if square is empty
624 getPiece(i, j) {
625 return this.board[i][j].charAt(1);
626 }
627
628 // Get opponent color
629 static GetOppCol(color) {
630 return color == "w" ? "b" : "w";
631 }
632
633 // Pieces codes (for a clearer code)
634 static get PAWN() {
635 return "p";
636 }
637 static get ROOK() {
638 return "r";
639 }
640 static get KNIGHT() {
641 return "n";
642 }
643 static get BISHOP() {
644 return "b";
645 }
646 static get QUEEN() {
647 return "q";
648 }
649 static get KING() {
650 return "k";
651 }
652
653 // For FEN checking:
654 static get PIECES() {
655 return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.KING];
656 }
657
658 // Empty square
659 static get EMPTY() {
660 return "";
661 }
662
663 // Some pieces movements
664 static get steps() {
665 return {
666 r: [
667 [-1, 0],
668 [1, 0],
669 [0, -1],
670 [0, 1]
671 ],
672 n: [
673 [-1, -2],
674 [-1, 2],
675 [1, -2],
676 [1, 2],
677 [-2, -1],
678 [-2, 1],
679 [2, -1],
680 [2, 1]
681 ],
682 b: [
683 [-1, -1],
684 [-1, 1],
685 [1, -1],
686 [1, 1]
687 ]
688 };
689 }
690
691 ////////////////////
692 // MOVES GENERATION
693
694 // All possible moves from selected square
695 getPotentialMovesFrom(sq) {
696 switch (this.getPiece(sq[0], sq[1])) {
697 case V.PAWN: return this.getPotentialPawnMoves(sq);
698 case V.ROOK: return this.getPotentialRookMoves(sq);
699 case V.KNIGHT: return this.getPotentialKnightMoves(sq);
700 case V.BISHOP: return this.getPotentialBishopMoves(sq);
701 case V.QUEEN: return this.getPotentialQueenMoves(sq);
702 case V.KING: return this.getPotentialKingMoves(sq);
703 }
704 return []; //never reached (but some variants may use it: Bario...)
705 }
706
707 // Build a regular move from its initial and destination squares.
708 // tr: transformation
709 getBasicMove([sx, sy], [ex, ey], tr) {
710 const initColor = this.getColor(sx, sy);
711 const initPiece = this.board[sx][sy].charAt(1);
712 let mv = new Move({
713 appear: [
714 new PiPo({
715 x: ex,
716 y: ey,
717 c: !!tr ? tr.c : initColor,
718 p: !!tr ? tr.p : initPiece
719 })
720 ],
721 vanish: [
722 new PiPo({
723 x: sx,
724 y: sy,
725 c: initColor,
726 p: initPiece
727 })
728 ]
729 });
730
731 // The opponent piece disappears if we take it
732 if (this.board[ex][ey] != V.EMPTY) {
733 mv.vanish.push(
734 new PiPo({
735 x: ex,
736 y: ey,
737 c: this.getColor(ex, ey),
738 p: this.board[ex][ey].charAt(1)
739 })
740 );
741 }
742
743 return mv;
744 }
745
746 // Generic method to find possible moves of non-pawn pieces:
747 // "sliding or jumping"
748 getSlideNJumpMoves([x, y], steps, oneStep) {
749 let moves = [];
750 outerLoop: for (let step of steps) {
751 let i = x + step[0];
752 let j = y + step[1];
753 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
754 moves.push(this.getBasicMove([x, y], [i, j]));
755 if (!!oneStep) continue outerLoop;
756 i += step[0];
757 j += step[1];
758 }
759 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
760 moves.push(this.getBasicMove([x, y], [i, j]));
761 }
762 return moves;
763 }
764
765 // Special case of en-passant captures: treated separately
766 getEnpassantCaptures([x, y], shiftX) {
767 const Lep = this.epSquares.length;
768 const epSquare = this.epSquares[Lep - 1]; //always at least one element
769 let enpassantMove = null;
770 if (
771 !!epSquare &&
772 epSquare.x == x + shiftX &&
773 Math.abs(epSquare.y - y) == 1
774 ) {
775 enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
776 enpassantMove.vanish.push({
777 x: x,
778 y: epSquare.y,
779 p: this.board[x][epSquare.y].charAt(1),
780 c: this.getColor(x, epSquare.y)
781 });
782 }
783 return !!enpassantMove ? [enpassantMove] : [];
784 }
785
786 // Consider all potential promotions:
787 addPawnMoves([x1, y1], [x2, y2], moves, promotions) {
788 let finalPieces = [V.PAWN];
789 const color = this.turn; //this.getColor(x1, y1);
790 const lastRank = (color == "w" ? 0 : V.size.x - 1);
791 if (x2 == lastRank) {
792 // promotions arg: special override for Hiddenqueen variant
793 if (!!promotions) finalPieces = promotions;
794 else if (!!V.PawnSpecs.promotions) finalPieces = V.PawnSpecs.promotions;
795 }
796 for (let piece of finalPieces) {
797 const tr = (piece != V.PAWN ? { c: color, p: piece } : null);
798 moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
799 }
800 }
801
802 // What are the pawn moves from square x,y ?
803 getPotentialPawnMoves([x, y], promotions) {
804 const color = this.turn; //this.getColor(x, y);
805 const [sizeX, sizeY] = [V.size.x, V.size.y];
806 const pawnShiftX = V.PawnSpecs.directions[color];
807 const firstRank = (color == "w" ? sizeX - 1 : 0);
808 const forward = (color == 'w' ? -1 : 1);
809
810 // Pawn movements in shiftX direction:
811 const getPawnMoves = (shiftX) => {
812 let moves = [];
813 // NOTE: next condition is generally true (no pawn on last rank)
814 if (x + shiftX >= 0 && x + shiftX < sizeX) {
815 if (this.board[x + shiftX][y] == V.EMPTY) {
816 // One square forward (or backward)
817 this.addPawnMoves([x, y], [x + shiftX, y], moves, promotions);
818 // Next condition because pawns on 1st rank can generally jump
819 if (
820 V.PawnSpecs.twoSquares &&
821 (
822 (color == 'w' && x >= V.size.x - 1 - V.PawnSpecs.initShift['w'])
823 ||
824 (color == 'b' && x <= V.PawnSpecs.initShift['b'])
825 )
826 ) {
827 if (
828 shiftX == forward &&
829 this.board[x + 2 * shiftX][y] == V.EMPTY
830 ) {
831 // Two squares jump
832 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
833 if (
834 V.PawnSpecs.threeSquares &&
835 this.board[x + 3 * shiftX][y] == V.EMPTY
836 ) {
837 // Three squares jump
838 moves.push(this.getBasicMove([x, y], [x + 3 * shiftX, y]));
839 }
840 }
841 }
842 }
843 // Captures
844 if (V.PawnSpecs.canCapture) {
845 for (let shiftY of [-1, 1]) {
846 if (y + shiftY >= 0 && y + shiftY < sizeY) {
847 if (
848 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
849 this.canTake([x, y], [x + shiftX, y + shiftY])
850 ) {
851 this.addPawnMoves(
852 [x, y], [x + shiftX, y + shiftY],
853 moves, promotions
854 );
855 }
856 if (
857 V.PawnSpecs.captureBackward && shiftX == forward &&
858 x - shiftX >= 0 && x - shiftX < V.size.x &&
859 this.board[x - shiftX][y + shiftY] != V.EMPTY &&
860 this.canTake([x, y], [x - shiftX, y + shiftY])
861 ) {
862 this.addPawnMoves(
863 [x, y], [x - shiftX, y + shiftY],
864 moves, promotions
865 );
866 }
867 }
868 }
869 }
870 }
871 return moves;
872 }
873
874 let pMoves = getPawnMoves(pawnShiftX);
875 if (V.PawnSpecs.bidirectional)
876 pMoves = pMoves.concat(getPawnMoves(-pawnShiftX));
877
878 if (V.HasEnpassant) {
879 // NOTE: backward en-passant captures are not considered
880 // because no rules define them (for now).
881 Array.prototype.push.apply(
882 pMoves,
883 this.getEnpassantCaptures([x, y], pawnShiftX)
884 );
885 }
886
887 return pMoves;
888 }
889
890 // What are the rook moves from square x,y ?
891 getPotentialRookMoves(sq) {
892 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]);
893 }
894
895 // What are the knight moves from square x,y ?
896 getPotentialKnightMoves(sq) {
897 return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep");
898 }
899
900 // What are the bishop moves from square x,y ?
901 getPotentialBishopMoves(sq) {
902 return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]);
903 }
904
905 // What are the queen moves from square x,y ?
906 getPotentialQueenMoves(sq) {
907 return this.getSlideNJumpMoves(
908 sq,
909 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
910 );
911 }
912
913 // What are the king moves from square x,y ?
914 getPotentialKingMoves(sq) {
915 // Initialize with normal moves
916 let moves = this.getSlideNJumpMoves(
917 sq,
918 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
919 "oneStep"
920 );
921 if (V.HasCastle && this.castleFlags[this.turn].some(v => v < V.size.y))
922 moves = moves.concat(this.getCastleMoves(sq));
923 return moves;
924 }
925
926 // "castleInCheck" arg to let some variants castle under check
927 getCastleMoves([x, y], finalSquares, castleInCheck, castleWith) {
928 const c = this.getColor(x, y);
929
930 // Castling ?
931 const oppCol = V.GetOppCol(c);
932 let moves = [];
933 // King, then rook:
934 finalSquares = finalSquares || [ [2, 3], [V.size.y - 2, V.size.y - 3] ];
935 const castlingKing = this.board[x][y].charAt(1);
936 castlingCheck: for (
937 let castleSide = 0;
938 castleSide < 2;
939 castleSide++ //large, then small
940 ) {
941 if (this.castleFlags[c][castleSide] >= V.size.y) continue;
942 // If this code is reached, rook and king are on initial position
943
944 // NOTE: in some variants this is not a rook
945 const rookPos = this.castleFlags[c][castleSide];
946 const castlingPiece = this.board[x][rookPos].charAt(1);
947 if (
948 this.board[x][rookPos] == V.EMPTY ||
949 this.getColor(x, rookPos) != c ||
950 (!!castleWith && !castleWith.includes(castlingPiece))
951 ) {
952 // Rook is not here, or changed color (see Benedict)
953 continue;
954 }
955
956 // Nothing on the path of the king ? (and no checks)
957 const finDist = finalSquares[castleSide][0] - y;
958 let step = finDist / Math.max(1, Math.abs(finDist));
959 let i = y;
960 do {
961 if (
962 (!castleInCheck && this.isAttacked([x, i], oppCol)) ||
963 (
964 this.board[x][i] != V.EMPTY &&
965 // NOTE: next check is enough, because of chessboard constraints
966 (this.getColor(x, i) != c || ![y, rookPos].includes(i))
967 )
968 ) {
969 continue castlingCheck;
970 }
971 i += step;
972 } while (i != finalSquares[castleSide][0]);
973
974 // Nothing on the path to the rook?
975 step = castleSide == 0 ? -1 : 1;
976 for (i = y + step; i != rookPos; i += step) {
977 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
978 }
979
980 // Nothing on final squares, except maybe king and castling rook?
981 for (i = 0; i < 2; i++) {
982 if (
983 finalSquares[castleSide][i] != rookPos &&
984 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
985 (
986 finalSquares[castleSide][i] != y ||
987 this.getColor(x, finalSquares[castleSide][i]) != c
988 )
989 ) {
990 continue castlingCheck;
991 }
992 }
993
994 // If this code is reached, castle is valid
995 moves.push(
996 new Move({
997 appear: [
998 new PiPo({
999 x: x,
1000 y: finalSquares[castleSide][0],
1001 p: castlingKing,
1002 c: c
1003 }),
1004 new PiPo({
1005 x: x,
1006 y: finalSquares[castleSide][1],
1007 p: castlingPiece,
1008 c: c
1009 })
1010 ],
1011 vanish: [
1012 // King might be initially disguised (Titan...)
1013 new PiPo({ x: x, y: y, p: castlingKing, c: c }),
1014 new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
1015 ],
1016 end:
1017 Math.abs(y - rookPos) <= 2
1018 ? { x: x, y: rookPos }
1019 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
1020 })
1021 );
1022 }
1023
1024 return moves;
1025 }
1026
1027 ////////////////////
1028 // MOVES VALIDATION
1029
1030 // For the interface: possible moves for the current turn from square sq
1031 getPossibleMovesFrom(sq) {
1032 return this.filterValid(this.getPotentialMovesFrom(sq));
1033 }
1034
1035 // TODO: promotions (into R,B,N,Q) should be filtered only once
1036 filterValid(moves) {
1037 if (moves.length == 0) return [];
1038 const color = this.turn;
1039 return moves.filter(m => {
1040 this.play(m);
1041 const res = !this.underCheck(color);
1042 this.undo(m);
1043 return res;
1044 });
1045 }
1046
1047 getAllPotentialMoves() {
1048 const color = this.turn;
1049 let potentialMoves = [];
1050 for (let i = 0; i < V.size.x; i++) {
1051 for (let j = 0; j < V.size.y; j++) {
1052 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
1053 Array.prototype.push.apply(
1054 potentialMoves,
1055 this.getPotentialMovesFrom([i, j])
1056 );
1057 }
1058 }
1059 }
1060 return potentialMoves;
1061 }
1062
1063 // Search for all valid moves considering current turn
1064 // (for engine and game end)
1065 getAllValidMoves() {
1066 return this.filterValid(this.getAllPotentialMoves());
1067 }
1068
1069 // Stop at the first move found
1070 // TODO: not really, it explores all moves from a square (one is enough).
1071 // Possible fix: add extra arg "oneMove" to getPotentialMovesFrom,
1072 // and then return only boolean true at first move found
1073 // (in all getPotentialXXXMoves() ... for all variants ...)
1074 atLeastOneMove() {
1075 const color = this.turn;
1076 for (let i = 0; i < V.size.x; i++) {
1077 for (let j = 0; j < V.size.y; j++) {
1078 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
1079 const moves = this.getPotentialMovesFrom([i, j]);
1080 if (moves.length > 0) {
1081 for (let k = 0; k < moves.length; k++)
1082 if (this.filterValid([moves[k]]).length > 0) return true;
1083 }
1084 }
1085 }
1086 }
1087 return false;
1088 }
1089
1090 // Check if pieces of given color are attacking (king) on square x,y
1091 isAttacked(sq, color) {
1092 return (
1093 this.isAttackedByPawn(sq, color) ||
1094 this.isAttackedByRook(sq, color) ||
1095 this.isAttackedByKnight(sq, color) ||
1096 this.isAttackedByBishop(sq, color) ||
1097 this.isAttackedByQueen(sq, color) ||
1098 this.isAttackedByKing(sq, color)
1099 );
1100 }
1101
1102 // Generic method for non-pawn pieces ("sliding or jumping"):
1103 // is x,y attacked by a piece of given color ?
1104 isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
1105 for (let step of steps) {
1106 let rx = x + step[0],
1107 ry = y + step[1];
1108 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
1109 rx += step[0];
1110 ry += step[1];
1111 }
1112 if (
1113 V.OnBoard(rx, ry) &&
1114 this.board[rx][ry] != V.EMPTY &&
1115 this.getPiece(rx, ry) == piece &&
1116 this.getColor(rx, ry) == color
1117 ) {
1118 return true;
1119 }
1120 }
1121 return false;
1122 }
1123
1124 // Is square x,y attacked by 'color' pawns ?
1125 isAttackedByPawn(sq, color) {
1126 const pawnShift = (color == "w" ? 1 : -1);
1127 return this.isAttackedBySlideNJump(
1128 sq,
1129 color,
1130 V.PAWN,
1131 [[pawnShift, 1], [pawnShift, -1]],
1132 "oneStep"
1133 );
1134 }
1135
1136 // Is square x,y attacked by 'color' rooks ?
1137 isAttackedByRook(sq, color) {
1138 return this.isAttackedBySlideNJump(sq, color, V.ROOK, V.steps[V.ROOK]);
1139 }
1140
1141 // Is square x,y attacked by 'color' knights ?
1142 isAttackedByKnight(sq, color) {
1143 return this.isAttackedBySlideNJump(
1144 sq,
1145 color,
1146 V.KNIGHT,
1147 V.steps[V.KNIGHT],
1148 "oneStep"
1149 );
1150 }
1151
1152 // Is square x,y attacked by 'color' bishops ?
1153 isAttackedByBishop(sq, color) {
1154 return this.isAttackedBySlideNJump(sq, color, V.BISHOP, V.steps[V.BISHOP]);
1155 }
1156
1157 // Is square x,y attacked by 'color' queens ?
1158 isAttackedByQueen(sq, color) {
1159 return this.isAttackedBySlideNJump(
1160 sq,
1161 color,
1162 V.QUEEN,
1163 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
1164 );
1165 }
1166
1167 // Is square x,y attacked by 'color' king(s) ?
1168 isAttackedByKing(sq, color) {
1169 return this.isAttackedBySlideNJump(
1170 sq,
1171 color,
1172 V.KING,
1173 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
1174 "oneStep"
1175 );
1176 }
1177
1178 // Is color under check after his move ?
1179 underCheck(color) {
1180 return this.isAttacked(this.kingPos[color], V.GetOppCol(color));
1181 }
1182
1183 /////////////////
1184 // MOVES PLAYING
1185
1186 // Apply a move on board
1187 static PlayOnBoard(board, move) {
1188 for (let psq of move.vanish) board[psq.x][psq.y] = V.EMPTY;
1189 for (let psq of move.appear) board[psq.x][psq.y] = psq.c + psq.p;
1190 }
1191 // Un-apply the played move
1192 static UndoOnBoard(board, move) {
1193 for (let psq of move.appear) board[psq.x][psq.y] = V.EMPTY;
1194 for (let psq of move.vanish) board[psq.x][psq.y] = psq.c + psq.p;
1195 }
1196
1197 prePlay() {}
1198
1199 play(move) {
1200 // DEBUG:
1201 // if (!this.states) this.states = [];
1202 // const stateFen = this.getFen() + JSON.stringify(this.kingPos);
1203 // this.states.push(stateFen);
1204
1205 this.prePlay(move);
1206 // Save flags (for undo)
1207 if (V.HasFlags) move.flags = JSON.stringify(this.aggregateFlags());
1208 if (V.HasEnpassant) this.epSquares.push(this.getEpSquare(move));
1209 V.PlayOnBoard(this.board, move);
1210 this.turn = V.GetOppCol(this.turn);
1211 this.movesCount++;
1212 this.postPlay(move);
1213 }
1214
1215 updateCastleFlags(move, piece, color) {
1216 // TODO: check flags. If already off, no need to always re-evaluate
1217 const c = color || V.GetOppCol(this.turn);
1218 const firstRank = (c == "w" ? V.size.x - 1 : 0);
1219 // Update castling flags if rooks are moved
1220 const oppCol = this.turn;
1221 const oppFirstRank = V.size.x - 1 - firstRank;
1222 if (piece == V.KING && move.appear.length > 0)
1223 this.castleFlags[c] = [V.size.y, V.size.y];
1224 else if (
1225 move.start.x == firstRank && //our rook moves?
1226 this.castleFlags[c].includes(move.start.y)
1227 ) {
1228 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
1229 this.castleFlags[c][flagIdx] = V.size.y;
1230 }
1231 // NOTE: not "else if" because a rook could take an opposing rook
1232 if (
1233 move.end.x == oppFirstRank && //we took opponent rook?
1234 this.castleFlags[oppCol].includes(move.end.y)
1235 ) {
1236 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
1237 this.castleFlags[oppCol][flagIdx] = V.size.y;
1238 }
1239 }
1240
1241 // After move is played, update variables + flags
1242 postPlay(move) {
1243 const c = V.GetOppCol(this.turn);
1244 let piece = undefined;
1245 if (move.vanish.length >= 1)
1246 // Usual case, something is moved
1247 piece = move.vanish[0].p;
1248 else
1249 // Crazyhouse-like variants
1250 piece = move.appear[0].p;
1251
1252 // Update king position + flags
1253 if (piece == V.KING && move.appear.length > 0)
1254 this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
1255 if (V.HasCastle) this.updateCastleFlags(move, piece);
1256 }
1257
1258 preUndo() {}
1259
1260 undo(move) {
1261 this.preUndo(move);
1262 if (V.HasEnpassant) this.epSquares.pop();
1263 if (V.HasFlags) this.disaggregateFlags(JSON.parse(move.flags));
1264 V.UndoOnBoard(this.board, move);
1265 this.turn = V.GetOppCol(this.turn);
1266 this.movesCount--;
1267 this.postUndo(move);
1268
1269 // DEBUG:
1270 // const stateFen = this.getFen() + JSON.stringify(this.kingPos);
1271 // if (stateFen != this.states[this.states.length-1]) debugger;
1272 // this.states.pop();
1273 }
1274
1275 // After move is undo-ed *and flags resetted*, un-update other variables
1276 // TODO: more symmetry, by storing flags increment in move (?!)
1277 postUndo(move) {
1278 // (Potentially) Reset king position
1279 const c = this.getColor(move.start.x, move.start.y);
1280 if (this.getPiece(move.start.x, move.start.y) == V.KING)
1281 this.kingPos[c] = [move.start.x, move.start.y];
1282 }
1283
1284 ///////////////
1285 // END OF GAME
1286
1287 // What is the score ? (Interesting if game is over)
1288 getCurrentScore() {
1289 if (this.atLeastOneMove()) return "*";
1290 // Game over
1291 const color = this.turn;
1292 // No valid move: stalemate or checkmate?
1293 if (!this.underCheck(color)) return "1/2";
1294 // OK, checkmate
1295 return (color == "w" ? "0-1" : "1-0");
1296 }
1297
1298 ///////////////
1299 // ENGINE PLAY
1300
1301 // Pieces values
1302 static get VALUES() {
1303 return {
1304 p: 1,
1305 r: 5,
1306 n: 3,
1307 b: 3,
1308 q: 9,
1309 k: 1000
1310 };
1311 }
1312
1313 // "Checkmate" (unreachable eval)
1314 static get INFINITY() {
1315 return 9999;
1316 }
1317
1318 // At this value or above, the game is over
1319 static get THRESHOLD_MATE() {
1320 return V.INFINITY;
1321 }
1322
1323 // Search depth: 1,2 for e.g. higher branching factor, 4 for smaller
1324 static get SEARCH_DEPTH() {
1325 return 3;
1326 }
1327
1328 // 'movesList' arg for some variants to provide a custom list
1329 getComputerMove(movesList) {
1330 const maxeval = V.INFINITY;
1331 const color = this.turn;
1332 let moves1 = movesList || this.getAllValidMoves();
1333
1334 if (moves1.length == 0)
1335 // TODO: this situation should not happen
1336 return null;
1337
1338 // Rank moves using a min-max at depth 2 (if search_depth >= 2!)
1339 for (let i = 0; i < moves1.length; i++) {
1340 this.play(moves1[i]);
1341 const score1 = this.getCurrentScore();
1342 if (score1 != "*") {
1343 moves1[i].eval =
1344 score1 == "1/2"
1345 ? 0
1346 : (score1 == "1-0" ? 1 : -1) * maxeval;
1347 }
1348 if (V.SEARCH_DEPTH == 1 || score1 != "*") {
1349 if (!moves1[i].eval) moves1[i].eval = this.evalPosition();
1350 this.undo(moves1[i]);
1351 continue;
1352 }
1353 // Initial self evaluation is very low: "I'm checkmated"
1354 moves1[i].eval = (color == "w" ? -1 : 1) * maxeval;
1355 // Initial enemy evaluation is very low too, for him
1356 let eval2 = (color == "w" ? 1 : -1) * maxeval;
1357 // Second half-move:
1358 let moves2 = this.getAllValidMoves();
1359 for (let j = 0; j < moves2.length; j++) {
1360 this.play(moves2[j]);
1361 const score2 = this.getCurrentScore();
1362 let evalPos = 0; //1/2 value
1363 switch (score2) {
1364 case "*":
1365 evalPos = this.evalPosition();
1366 break;
1367 case "1-0":
1368 evalPos = maxeval;
1369 break;
1370 case "0-1":
1371 evalPos = -maxeval;
1372 break;
1373 }
1374 if (
1375 (color == "w" && evalPos < eval2) ||
1376 (color == "b" && evalPos > eval2)
1377 ) {
1378 eval2 = evalPos;
1379 }
1380 this.undo(moves2[j]);
1381 }
1382 if (
1383 (color == "w" && eval2 > moves1[i].eval) ||
1384 (color == "b" && eval2 < moves1[i].eval)
1385 ) {
1386 moves1[i].eval = eval2;
1387 }
1388 this.undo(moves1[i]);
1389 }
1390 moves1.sort((a, b) => {
1391 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
1392 });
1393 // console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
1394
1395 // Skip depth 3+ if we found a checkmate (or if we are checkmated in 1...)
1396 if (V.SEARCH_DEPTH >= 3 && Math.abs(moves1[0].eval) < V.THRESHOLD_MATE) {
1397 for (let i = 0; i < moves1.length; i++) {
1398 this.play(moves1[i]);
1399 // 0.1 * oldEval : heuristic to avoid some bad moves (not all...)
1400 moves1[i].eval =
1401 0.1 * moves1[i].eval +
1402 this.alphabeta(V.SEARCH_DEPTH - 1, -maxeval, maxeval);
1403 this.undo(moves1[i]);
1404 }
1405 moves1.sort((a, b) => {
1406 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
1407 });
1408 }
1409
1410 let candidates = [0];
1411 for (let i = 1; i < moves1.length && moves1[i].eval == moves1[0].eval; i++)
1412 candidates.push(i);
1413 return moves1[candidates[randInt(candidates.length)]];
1414 }
1415
1416 alphabeta(depth, alpha, beta) {
1417 const maxeval = V.INFINITY;
1418 const color = this.turn;
1419 const score = this.getCurrentScore();
1420 if (score != "*")
1421 return score == "1/2" ? 0 : (score == "1-0" ? 1 : -1) * maxeval;
1422 if (depth == 0) return this.evalPosition();
1423 const moves = this.getAllValidMoves();
1424 let v = color == "w" ? -maxeval : maxeval;
1425 if (color == "w") {
1426 for (let i = 0; i < moves.length; i++) {
1427 this.play(moves[i]);
1428 v = Math.max(v, this.alphabeta(depth - 1, alpha, beta));
1429 this.undo(moves[i]);
1430 alpha = Math.max(alpha, v);
1431 if (alpha >= beta) break; //beta cutoff
1432 }
1433 }
1434 else {
1435 // color=="b"
1436 for (let i = 0; i < moves.length; i++) {
1437 this.play(moves[i]);
1438 v = Math.min(v, this.alphabeta(depth - 1, alpha, beta));
1439 this.undo(moves[i]);
1440 beta = Math.min(beta, v);
1441 if (alpha >= beta) break; //alpha cutoff
1442 }
1443 }
1444 return v;
1445 }
1446
1447 evalPosition() {
1448 let evaluation = 0;
1449 // Just count material for now
1450 for (let i = 0; i < V.size.x; i++) {
1451 for (let j = 0; j < V.size.y; j++) {
1452 if (this.board[i][j] != V.EMPTY) {
1453 const sign = this.getColor(i, j) == "w" ? 1 : -1;
1454 evaluation += sign * V.VALUES[this.getPiece(i, j)];
1455 }
1456 }
1457 }
1458 return evaluation;
1459 }
1460
1461 /////////////////////////
1462 // MOVES + GAME NOTATION
1463 /////////////////////////
1464
1465 // Context: just before move is played, turn hasn't changed
1466 // TODO: un-ambiguous notation (switch on piece type, check directions...)
1467 getNotation(move) {
1468 if (move.appear.length == 2 && move.appear[0].p == V.KING)
1469 // Castle
1470 return move.end.y < move.start.y ? "0-0-0" : "0-0";
1471
1472 // Translate final square
1473 const finalSquare = V.CoordsToSquare(move.end);
1474
1475 const piece = this.getPiece(move.start.x, move.start.y);
1476 if (piece == V.PAWN) {
1477 // Pawn move
1478 let notation = "";
1479 if (move.vanish.length > move.appear.length) {
1480 // Capture
1481 const startColumn = V.CoordToColumn(move.start.y);
1482 notation = startColumn + "x" + finalSquare;
1483 }
1484 else notation = finalSquare;
1485 if (move.appear.length > 0 && move.appear[0].p != V.PAWN)
1486 // Promotion
1487 notation += "=" + move.appear[0].p.toUpperCase();
1488 return notation;
1489 }
1490 // Piece movement
1491 return (
1492 piece.toUpperCase() +
1493 (move.vanish.length > move.appear.length ? "x" : "") +
1494 finalSquare
1495 );
1496 }
1497
1498 static GetUnambiguousNotation(move) {
1499 // Machine-readable format with all the informations about the move
1500 return (
1501 (!!move.start && V.OnBoard(move.start.x, move.start.y)
1502 ? V.CoordsToSquare(move.start)
1503 : "-"
1504 ) + "." +
1505 (!!move.end && V.OnBoard(move.end.x, move.end.y)
1506 ? V.CoordsToSquare(move.end)
1507 : "-"
1508 ) + " " +
1509 (!!move.appear && move.appear.length > 0
1510 ? move.appear.map(a =>
1511 a.c + a.p + V.CoordsToSquare({ x: a.x, y: a.y })).join(".")
1512 : "-"
1513 ) + "/" +
1514 (!!move.vanish && move.vanish.length > 0
1515 ? move.vanish.map(a =>
1516 a.c + a.p + V.CoordsToSquare({ x: a.x, y: a.y })).join(".")
1517 : "-"
1518 )
1519 );
1520 }
1521
1522 };