caf55c9ccecb6888137594672a0dec52c19dc641
1 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
2 import { randInt
} from "@/utils/alea";
3 import { ArrayFun
} from "@/utils/array";
5 export class OtageRules
extends ChessRules
{
7 static get IMAGE_EXTENSION() {
11 // Hostage / Capturer combinations
12 // + letter among a, b, v, w to indicate colors + config:
13 // a: black first, black controls
14 // b: white first, black controls
15 // v: black first, white controls
16 // w: white first, white controls
44 if (ChessRules
.PIECES
.includes(b
[1])) return ChessRules
.board2fen(b
);
45 // Show symbol first (no collisions)
50 if (f
.length
== 1) return ChessRules
.fen2board(f
);
51 return f
[1] + f
[0]; //"color" first
54 static IsGoodPosition(position
) {
55 if (position
.length
== 0) return false;
56 const rows
= position
.split("/");
57 if (rows
.length
!= V
.size
.x
) return false;
58 let kings
= { 'k': 0, 'K': 0 };
59 for (let row
of rows
) {
61 for (let i
= 0; i
< row
.length
; i
++) {
62 const lowR
= row
[i
].toLowerCase();
63 const readNext
= !(ChessRules
.PIECES
.includes(lowR
));
64 if (!!(lowR
.match(/[a-z@]/))) {
66 if (lowR
== 'k') kings
[row
[i
]]++;
68 const up
= this.getUnionPieces(row
[++i
], lowR
);
69 if (up
.w
== V
.KING
) kings
['K']++;
70 // NOTE: not "else if" because two kings might be in union
71 if (up
.b
== V
.KING
) kings
['k']++;
75 const num
= parseInt(row
[i
], 10);
76 if (isNaN(num
) || num
<= 0) return false;
80 if (sumElts
!= V
.size
.y
) return false;
82 // Both kings should be on board. Exactly one per color.
83 if (Object
.values(kings
).some(v
=> v
!= 1)) return false;
87 static GetBoard(position
) {
88 const rows
= position
.split("/");
89 let board
= ArrayFun
.init(V
.size
.x
, V
.size
.y
, "");
90 for (let i
= 0; i
< rows
.length
; i
++) {
92 for (let indexInRow
= 0; indexInRow
< rows
[i
].length
; indexInRow
++) {
93 const character
= rows
[i
][indexInRow
];
94 const num
= parseInt(character
, 10);
95 // If num is a number, just shift j:
96 if (!isNaN(num
)) j
+= num
;
98 // Something at position i,j
99 const lowC
= character
.toLowerCase();
100 if (ChessRules
.PIECES
.includes(lowC
))
101 board
[i
][j
++] = V
.fen2board(character
);
103 board
[i
][j
++] = V
.fen2board(lowC
+ rows
[i
][++indexInRow
]);
115 if (ChessRules
.PIECES
.includes(m
.appear
[0].p
)) return super.getPPpath(m
);
116 // For an "union", show only relevant piece:
117 // The color must be deduced from the move: reaching final rank of who?
118 const color
= (m
.appear
[0].x
== 0 ? 'w' : 'b');
119 const up
= this.getUnionPieces(m
.appear
[0].c
, m
.appear
[0].p
);
120 return "Pacosako/" + color
+ up
[color
];
123 canTake([x1
, y1
], [x2
, y2
]) {
124 const p1
= this.board
[x1
][y1
].charAt(1);
125 if (!(ChessRules
.PIECES
.includes(p1
))) return false;
126 const p2
= this.board
[x2
][y2
].charAt(1);
127 if (!(ChessRules
.PIECES
.includes(p2
))) return true;
128 const c1
= this.board
[x1
][y1
].charAt(0);
129 const c2
= this.board
[x2
][y2
].charAt(0);
133 canIplay(side
, [x
, y
]) {
134 const c
= this.board
[x
][y
].charAt(0);
135 const compSide
= (side
== 'w' ? 'v' : 'a');
136 return (this.turn
== side
&& [side
, compSide
].includes(c
));
140 this.kingPos
= { w: [-1, -1], b: [-1, -1] };
141 const fenRows
= V
.ParseFen(fen
).position
.split("/");
142 const startRow
= { 'w': V
.size
.x
- 1, 'b': 0 };
143 for (let i
= 0; i
< fenRows
.length
; i
++) {
145 for (let j
= 0; j
< fenRows
[i
].length
; j
++) {
146 const c
= fenRows
[i
].charAt(j
);
147 const lowR
= c
.toLowerCase();
148 const readNext
= !(ChessRules
.PIECES
.includes(lowR
));
149 if (!!(lowR
.match(/[a-z@]/))) {
150 if (lowR
== 'k') this.kingPos
[c
== 'k' ? 'b' : 'w'] = [i
, k
];
152 const up
= this.getUnionPieces(fenRows
[i
][++j
], lowR
);
153 if (up
.w
== V
.KING
) this.kingPos
['w'] = [i
, k
];
154 if (up
.b
== V
.KING
) this.kingPos
['b'] = [i
, k
];
158 const num
= parseInt(fenRows
[i
].charAt(j
), 10);
159 if (!isNaN(num
)) k
+= num
- 1;
166 setOtherVariables(fen
) {
167 super.setOtherVariables(fen
);
168 // Stack of "last move" only for intermediate chaining
169 this.lastMoveEnd
= [null];
170 this.repetitions
= [];
173 static IsGoodFlags(flags
) {
174 // 4 for castle + 16 for pawns
175 return !!flags
.match(/^[a-z]{4,4}[01]{16,16}$/);
179 super.setFlags(fenflags
); //castleFlags
181 w: [...Array(8)], //pawns can move 2 squares?
184 const flags
= fenflags
.substr(4); //skip first 4 letters, for castle
185 for (let c
of ["w", "b"]) {
186 for (let i
= 0; i
< 8; i
++)
187 this.pawnFlags
[c
][i
] = flags
.charAt((c
== "w" ? 0 : 8) + i
) == "1";
192 return [this.castleFlags
, this.pawnFlags
];
195 disaggregateFlags(flags
) {
196 this.castleFlags
= flags
[0];
197 this.pawnFlags
= flags
[1];
200 static GenRandInitFen(options
) {
201 // Add 16 pawns flags:
202 return ChessRules
.GenRandInitFen(options
)
203 .slice(0, -2) + "1111111111111111 -";
207 let fen
= super.getFlagsFen();
209 for (let c
of ["w", "b"])
210 for (let i
= 0; i
< 8; i
++) fen
+= (this.pawnFlags
[c
][i
] ? "1" : "0");
215 const p
= this.board
[i
][j
].charAt(1);
216 if (ChessRules
.PIECES
.includes(p
)) return p
;
217 const c
= this.board
[i
][j
].charAt(0);
218 const idx
= (['a', 'w'].includes(c
) ? 0 : 1);
219 return V
.UNIONS
[p
][idx
];
222 getUnionPieces(color
, code
) {
223 const pieces
= V
.UNIONS
[code
];
225 w: pieces
[ ['b', 'w'].includes(color
) ? 0 : 1 ],
226 b: pieces
[ ['a', 'v'].includes(color
) ? 0 : 1 ]
230 // p1: white piece, p2: black piece, capturer: (temporary) owner
231 getUnionCode(p1
, p2
, capturer
) {
233 Object
.values(V
.UNIONS
).findIndex(v
=> v
[0] == p1
&& v
[1] == p2
)
236 if (capturer
== 'w') c
= (uIdx
>= 0 ? 'w' : 'v');
237 else c
= (uIdx
>= 0 ? 'b' : 'a');
240 Object
.values(V
.UNIONS
).findIndex(v
=> v
[0] == p2
&& v
[1] == p1
)
243 return { c: c
, p: Object
.keys(V
.UNIONS
)[uIdx
] };
246 getBasicMove([sx
, sy
], [ex
, ey
], tr
) {
247 const L
= this.lastMoveEnd
.length
;
248 const lm
= this.lastMoveEnd
[L
-1];
249 const piece
= (!!lm
? lm
.p : null);
250 const initColor
= (!!piece
? this.turn : this.board
[sx
][sy
].charAt(0));
251 const initPiece
= (piece
|| this.board
[sx
][sy
].charAt(1));
253 const oppCol
= V
.GetOppCol(c
);
254 if (!!tr
&& !(ChessRules
.PIECES
.includes(initPiece
))) {
255 // Transformation computed without taking union into account
256 const up
= this.getUnionPieces(initColor
, initPiece
);
257 let args
= [tr
.p
, up
[oppCol
]];
259 ['a', 'v'].includes(initColor
) ||
260 // HACK: "ba" piece = two pawns, black controling.
261 // If promoting, must artificially imagine color was 'a':
262 (initPiece
== 'a' && initColor
== 'b')
264 args
= args
.reverse();
266 const capturer
= (['a', 'b'].includes(initColor
) ? 'b' : 'w');
267 const cp
= this.getUnionCode(args
[0], args
[1], capturer
);
272 // - union to free square (other cases are illegal: return null)
273 // - normal piece to free square,
274 // to enemy normal piece, or
275 // to union (releasing our piece)
277 start: { x: sx
, y: sy
},
278 end: { x: ex
, y: ey
},
291 // Treat free square cases first:
292 if (this.board
[ex
][ey
] == V
.EMPTY
) {
297 c: !!tr
? tr
.c : initColor
,
298 p: !!tr
? tr
.p : initPiece
303 // Now the two cases with union / release:
304 const destColor
= this.board
[ex
][ey
].charAt(0);
305 const destPiece
= this.board
[ex
][ey
].charAt(1);
314 if (ChessRules
.PIECES
.includes(destPiece
)) {
315 // Normal piece: just create union
316 let args
= [!!tr
? tr
.p : initPiece
, destPiece
];
317 if (c
== 'b') args
= args
.reverse();
318 const cp
= this.getUnionCode(args
[0], args
[1], c
);
329 // Releasing a piece in an union: keep track of released piece
330 const up
= this.getUnionPieces(destColor
, destPiece
);
331 let args
= [!!tr
? tr
.p : initPiece
, up
[oppCol
]];
332 if (c
== 'b') args
= args
.reverse();
333 const cp
= this.getUnionCode(args
[0], args
[1], c
);
342 mv
.end
.released
= up
[c
];
346 // noCastle arg: when detecting king attacks
347 getPotentialMovesFrom([x
, y
], noCastle
) {
348 const L
= this.lastMoveEnd
.length
;
349 const lm
= this.lastMoveEnd
[L
-1];
350 if (!!lm
&& (x
!= lm
.x
|| y
!= lm
.y
)) return [];
351 const piece
= (!!lm
? lm
.p : this.getPiece(x
, y
));
353 var saveSquare
= this.board
[x
][y
];
354 this.board
[x
][y
] = this.turn
+ piece
;
360 const firstRank
= (c
== 'w' ? 7 : 0);
361 baseMoves
= this.getPotentialPawnMoves([x
, y
]).filter(m
=> {
362 // Skip forbidden 2-squares jumps (except from first rank)
363 // Also skip unions capturing en-passant (not allowed).
366 m
.start
.x
== firstRank
||
367 Math
.abs(m
.end
.x
- m
.start
.x
) == 1 ||
368 this.pawnFlags
[c
][m
.start
.y
]
372 this.board
[x
][y
].charAt(1) == V
.PAWN
||
380 baseMoves
= this.getPotentialRookMoves([x
, y
]);
383 baseMoves
= this.getPotentialKnightMoves([x
, y
]);
386 baseMoves
= this.getPotentialBishopMoves([x
, y
]);
389 baseMoves
= this.getPotentialQueenMoves([x
, y
]);
392 baseMoves
= this.getSlideNJumpMoves(
393 [x
, y
], V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), 1);
394 if (!noCastle
&& this.castleFlags
[this.turn
].some(v
=> v
< V
.size
.y
))
395 baseMoves
= baseMoves
.concat(this.getCastleMoves([x
, y
]));
398 // When a pawn in an union reaches final rank with a non-standard
399 // promotion move: apply promotion anyway
401 const oppCol
= V
.GetOppCol(c
);
402 const oppLastRank
= (c
== 'w' ? 7 : 0);
403 baseMoves
.forEach(m
=> {
405 m
.end
.x
== oppLastRank
&&
406 ['c', 'd', 'e', 'f', 'g'].includes(m
.appear
[0].p
)
408 // Move to first rank, which is last rank for opponent's pawn.
409 // => Show promotion choices.
410 // Find our piece in union (not a pawn)
411 const up
= this.getUnionPieces(m
.appear
[0].c
, m
.appear
[0].p
);
412 // merge with all potential promotion pieces + push (loop)
413 for (let promotionPiece
of [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
]) {
414 let args
= [up
[c
], promotionPiece
];
415 if (c
== 'b') args
= args
.reverse();
416 const cp
= this.getUnionCode(args
[0], args
[1], c
);
417 let cpMove
= JSON
.parse(JSON
.stringify(m
));
418 cpMove
.appear
[0].c
= cp
.c
;
419 cpMove
.appear
[0].p
= cp
.p
;
425 m
.vanish
.length
> 0 &&
426 m
.vanish
[0].p
== V
.PAWN
&&
427 m
.start
.y
!= m
.end
.y
&&
428 this.board
[m
.end
.x
][m
.end
.y
] == V
.EMPTY
431 // No en-passant inside a chaining
433 // Fix en-passant capture: union type, maybe released piece too
434 const cs
= [m
.end
.x
+ (c
== 'w' ? 1 : -1), m
.end
.y
];
435 const code
= this.board
[cs
[0]][cs
[1]].charAt(1);
436 if (code
== V
.PAWN
) {
437 // Simple en-passant capture (usual: just form union)
442 // An union pawn + something just moved two squares
443 const color
= this.board
[cs
[0]][cs
[1]].charAt(0);
444 const up
= this.getUnionPieces(color
, code
);
445 m
.end
.released
= up
[c
];
446 let args
= [V
.PAWN
, up
[oppCol
]];
447 if (c
== 'b') args
= args
.reverse();
448 const cp
= this.getUnionCode(args
[0], args
[1], c
);
449 m
.appear
[0].c
= cp
.c
;
450 m
.appear
[0].p
= cp
.p
;
456 if (!!lm
) this.board
[x
][y
] = saveSquare
;
460 getEpSquare(moveOrSquare
) {
461 if (typeof moveOrSquare
=== "string") {
462 const square
= moveOrSquare
;
463 if (square
== "-") return undefined;
464 return V
.SquareToCoords(square
);
466 const move = moveOrSquare
;
467 const s
= move.start
,
471 Math
.abs(s
.x
- e
.x
) == 2 &&
472 this.getPiece(s
.x
, s
.y
) == V
.PAWN
482 getCastleMoves([x
, y
]) {
484 const accepted
= (c
== 'w' ? ['v', 'w'] : ['a', 'b']);
485 const oppCol
= V
.GetOppCol(c
);
487 const finalSquares
= [ [2, 3], [6, 5] ];
488 castlingCheck: for (let castleSide
= 0; castleSide
< 2; castleSide
++) {
489 if (this.castleFlags
[c
][castleSide
] >= 8) continue;
490 const rookPos
= this.castleFlags
[c
][castleSide
];
491 const castlingColor
= this.board
[x
][rookPos
].charAt(0);
492 const castlingPiece
= this.board
[x
][rookPos
].charAt(1);
494 // Nothing on the path of the king ?
495 const finDist
= finalSquares
[castleSide
][0] - y
;
496 let step
= finDist
/ Math
.max(1, Math
.abs(finDist
));
498 let kingSquares
= [y
];
501 this.board
[x
][i
] != V
.EMPTY
&&
502 !accepted
.includes(this.getColor(x
, i
))
504 continue castlingCheck
;
508 } while (i
!= finalSquares
[castleSide
][0]);
509 // No checks on the path of the king ?
510 if (this.isAttacked(kingSquares
, oppCol
)) continue castlingCheck
;
512 // Nothing on the path to the rook?
513 step
= castleSide
== 0 ? -1 : 1;
514 for (i
= y
+ step
; i
!= rookPos
; i
+= step
) {
515 if (this.board
[x
][i
] != V
.EMPTY
) continue castlingCheck
;
518 // Nothing on final squares, except maybe king and castling rook?
519 for (i
= 0; i
< 2; i
++) {
521 finalSquares
[castleSide
][i
] != rookPos
&&
522 this.board
[x
][finalSquares
[castleSide
][i
]] != V
.EMPTY
&&
524 finalSquares
[castleSide
][i
] != y
||
525 // TODO: next test seems superflu
526 !accepted
.includes(this.getColor(x
, finalSquares
[castleSide
][i
]))
529 continue castlingCheck
;
538 y: finalSquares
[castleSide
][0],
544 y: finalSquares
[castleSide
][1],
550 // King might be initially disguised (Titan...)
551 new PiPo({ x: x
, y: y
, p: V
.KING
, c: c
}),
552 new PiPo({ x: x
, y: rookPos
, p: castlingPiece
, c: castlingColor
})
555 Math
.abs(y
- rookPos
) <= 2
556 ? { x: x
, y: rookPos
}
557 : { x: x
, y: y
+ 2 * (castleSide
== 0 ? -1 : 1) }
565 getEnpassantCaptures(sq
, shiftX
) {
566 // HACK: when artificially change turn, do not consider en-passant
567 const mcMod2
= this.movesCount
% 2;
569 if ((c
== 'w' && mcMod2
== 1) || (c
== 'b' && mcMod2
== 0)) return [];
570 return super.getEnpassantCaptures(sq
, shiftX
);
573 isAttacked_aux(files
, color
, positions
, fromSquare
, released
) {
574 // "positions" = array of FENs to detect infinite loops. Example:
575 // r1q1k2r/p1Pb1ppp/5n2/1f1p4/AV5P/P1eDP3/3B1PP1/R3K1NR,
576 // Bxd2 Bxc3 Bxb4 Bxc3 Bxb4 etc.
577 const newPos
= { fen: super.getBaseFen(), piece: released
};
578 if (positions
.some(p
=> p
.piece
== newPos
.piece
&& p
.fen
== newPos
.fen
))
579 // Start of an infinite loop: exit
581 positions
.push(newPos
);
582 const rank
= (color
== 'w' ? 0 : 7);
583 const moves
= this.getPotentialMovesFrom(fromSquare
);
584 if (moves
.some(m
=> m
.end
.x
== rank
&& files
.includes(m
.end
.y
)))
587 for (let m
of moves
) {
588 if (!!m
.end
.released
) {
589 // Turn won't change since !!m.released
591 const res
= this.isAttacked_aux(
592 files
, color
, positions
, [m
.end
.x
, m
.end
.y
], m
.end
.released
);
594 if (res
) return true;
600 isAttacked(files
, color
) {
601 const rank
= (color
== 'w' ? 0 : 7);
602 // Since it's too difficult (impossible?) to search from the square itself,
603 // let's adopt a suboptimal but working strategy: find all attacks.
605 // Artificial turn change is required:
608 outerLoop: for (let i
=0; i
<8; i
++) {
609 for (let j
=0; j
<8; j
++) {
610 // Attacks must start from a normal piece, not an union.
611 // Therefore, the following test is correct.
613 this.board
[i
][j
] != V
.EMPTY
&&
614 [V
.KING
, V
.PAWN
, V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
].includes(
615 this.board
[i
][j
].charAt(1)) &&
616 this.board
[i
][j
].charAt(0) == color
619 const moves
= this.getPotentialMovesFrom([i
, j
], "noCastle");
620 if (moves
.some(m
=> m
.end
.x
== rank
&& files
.includes(m
.end
.y
))) {
624 for (let m
of moves
) {
625 if (!!m
.end
.released
) {
626 // Turn won't change since !!m.released
629 res
= this.isAttacked_aux(
630 files
, color
, positions
, [m
.end
.x
, m
.end
.y
], m
.end
.released
);
632 if (res
) break outerLoop
;
642 // Do not consider checks, except to forbid castling
648 if (moves
.length
== 0) return [];
649 return moves
.filter(m
=> {
650 if (!m
.end
.released
) return true;
651 // Check for repetitions:
652 V
.PlayOnBoard(this.board
, m
);
654 piece: m
.end
.released
,
655 square: { x: m
.end
.x
, y: m
.end
.y
},
656 position: this.getBaseFen()
659 this.repetitions
.some(r
=> {
661 r
.piece
== newState
.piece
&&
663 r
.square
.x
== newState
.square
.x
&&
664 r
.square
.y
== newState
.square
.y
666 r
.position
== newState
.position
669 V
.UndoOnBoard(this.board
, m
);
674 updateCastleFlags(move, piece
) {
676 const firstRank
= (c
== "w" ? 7 : 0);
677 if (piece
== V
.KING
&& move.appear
.length
> 0)
678 this.castleFlags
[c
] = [V
.size
.y
, V
.size
.y
];
680 move.start
.x
== firstRank
&&
681 this.castleFlags
[c
].includes(move.start
.y
)
683 const flagIdx
= (move.start
.y
== this.castleFlags
[c
][0] ? 0 : 1);
684 this.castleFlags
[c
][flagIdx
] = V
.size
.y
;
687 move.end
.x
== firstRank
&&
688 this.castleFlags
[c
].includes(move.end
.y
)
690 // Move to our rook: necessary normal piece, to union, releasing
691 // (or the rook was moved before!)
692 const flagIdx
= (move.end
.y
== this.castleFlags
[c
][0] ? 0 : 1);
693 this.castleFlags
[c
][flagIdx
] = V
.size
.y
;
698 // Easier before move is played in this case (flags are saved)
700 const L
= this.lastMoveEnd
.length
;
701 const lm
= this.lastMoveEnd
[L
-1];
705 : this.getPiece(move.vanish
[0].x
, move.vanish
[0].y
);
707 this.kingPos
[c
] = [move.appear
[0].x
, move.appear
[0].y
];
708 this.updateCastleFlags(move, piece
);
709 const pawnFirstRank
= (c
== 'w' ? 6 : 1);
711 move.start
.x
== pawnFirstRank
&&
713 Math
.abs(move.end
.x
- move.start
.x
) == 2
715 // This move turns off a 2-squares pawn flag
716 this.pawnFlags
[c
][move.start
.y
] = false;
721 move.flags
= JSON
.stringify(this.aggregateFlags());
723 this.epSquares
.push(this.getEpSquare(move));
724 // Check if the move is the last of the turn: all cases except releases
725 if (!move.end
.released
) {
726 // No more union releases available
727 this.turn
= V
.GetOppCol(this.turn
);
729 this.lastMoveEnd
.push(null);
732 this.lastMoveEnd
.push(Object
.assign({ p: move.end
.released
}, move.end
));
733 V
.PlayOnBoard(this.board
, move);
734 if (!move.end
.released
) this.repetitions
= [];
736 this.repetitions
.push(
738 piece: move.end
.released
,
739 square: { x: move.end
.x
, y: move.end
.y
},
740 position: this.getBaseFen()
747 this.epSquares
.pop();
748 this.disaggregateFlags(JSON
.parse(move.flags
));
749 V
.UndoOnBoard(this.board
, move);
750 this.lastMoveEnd
.pop();
751 if (!move.end
.released
) {
752 this.turn
= V
.GetOppCol(this.turn
);
755 if (!!move.end
.released
) this.repetitions
.pop();
760 if (this.getPiece(move.start
.x
, move.start
.y
) == V
.KING
)
761 this.kingPos
[this.turn
] = [move.start
.x
, move.start
.y
];
763 // Check if a king is being released: put it on releasing square
764 const L
= this.lastMoveEnd
.length
;
765 const lm
= this.lastMoveEnd
[L
-1];
766 if (!!lm
&& lm
.p
== V
.KING
)
767 this.kingPos
[this.turn
] = [move.start
.x
, move.start
.y
];
772 // Check kings: if one is captured, the side lost
773 for (let c
of ['w', 'b']) {
774 const kp
= this.kingPos
[c
];
775 const cell
= this.board
[kp
[0]][kp
[1]];
779 (c
== 'w' && ['a', 'b'].includes(cell
[0])) ||
780 (c
== 'b' && ['v', 'w'].includes(cell
[0]))
784 return (c
== 'w' ? "0-1" : "1-0");
791 let initMoves
= this.getAllValidMoves();
792 if (initMoves
.length
== 0) return null;
793 // Loop until valid move is found (no blocked pawn released...)
795 let moves
= JSON
.parse(JSON
.stringify(initMoves
));
798 // Just play random moves (for now at least. TODO?)
799 while (moves
.length
> 0) {
800 mv
= moves
[randInt(moves
.length
)];
803 if (!!mv
.end
.released
)
804 // A piece was just released from an union
805 moves
= this.getPotentialMovesFrom([mv
.end
.x
, mv
.end
.y
]);
808 for (let i
= mvArray
.length
- 1; i
>= 0; i
--) this.undo(mvArray
[i
]);
809 if (!mv
.end
.released
) return (mvArray
.length
> 1 ? mvArray : mvArray
[0]);
811 return null; //never reached
814 // NOTE: evalPosition() is wrong, but unused since bot plays at random
817 if (move.appear
.length
== 2 && move.appear
[0].p
== V
.KING
)
818 return (move.end
.y
< move.start
.y
? "0-0-0" : "0-0");
821 const L
= this.lastMoveEnd
.length
;
822 const lm
= this.lastMoveEnd
[L
-1];
824 if (!lm
&& move.vanish
.length
== 0)
825 // When importing a game, the info move.released is lost
826 piece
= move.appear
[0].p
;
827 else piece
= (!!lm
? lm
.p : move.vanish
[0].p
);
828 if (!(ChessRules
.PIECES
.includes(piece
))) {
829 // Decode (moving) union
830 const up
= this.getUnionPieces(
831 move.vanish
.length
> 0 ? move.vanish
[0].c : move.appear
[0].c
, piece
);
835 // Basic move notation:
836 let notation
= piece
.toUpperCase();
838 this.board
[move.end
.x
][move.end
.y
] != V
.EMPTY
||
839 (piece
== V
.PAWN
&& move.start
.y
!= move.end
.y
)
843 const finalSquare
= V
.CoordsToSquare(move.end
);
844 notation
+= finalSquare
;
846 // Add potential promotion indications:
847 const firstLastRank
= (c
== 'w' ? [7, 0] : [0, 7]);
848 if (move.end
.x
== firstLastRank
[1] && piece
== V
.PAWN
) {
850 if (ChessRules
.PIECES
.includes(move.appear
[0].p
))
851 notation
+= move.appear
[0].p
.toUpperCase();
853 const up
= this.getUnionPieces(move.appear
[0].c
, move.appear
[0].p
);
854 notation
+= up
[c
].toUpperCase();
858 move.end
.x
== firstLastRank
[0] &&
859 move.vanish
.length
> 0 &&
860 ['c', 'd', 'e', 'f', 'g'].includes(move.vanish
[0].p
)
862 // We promoted an opponent's pawn
863 const oppCol
= V
.GetOppCol(c
);
864 const up
= this.getUnionPieces(move.appear
[0].c
, move.appear
[0].p
);
865 notation
+= "=" + up
[oppCol
].toUpperCase();