1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
2 import { randInt
} from "@/utils/alea";
4 export class DynamoRules
extends ChessRules
{
5 // TODO: later, allow to push out pawns on a and h files
6 static get HasEnpassant() {
10 canIplay(side
, [x
, y
]) {
11 // Sometimes opponent's pieces can be moved directly
12 return this.turn
== side
;
15 setOtherVariables(fen
) {
16 super.setOtherVariables(fen
);
18 // Local stack of "action moves"
20 const amove
= V
.ParseFen(fen
).amove
;
22 const amoveParts
= amove
.split("/");
24 // No need for start & end
29 if (amoveParts
[i
] != "-") {
30 amoveParts
[i
].split(".").forEach(av
=> {
32 const xy
= V
.SquareToCoords(av
.substr(2));
33 move[i
== 0 ? "appear" : "vanish"].push(
44 this.amoves
.push(move);
47 // Stack "first moves" (on subTurn 1) to merge and check opposite moves
51 static ParseFen(fen
) {
53 ChessRules
.ParseFen(fen
),
54 { amove: fen
.split(" ")[4] }
58 static IsGoodFen(fen
) {
59 if (!ChessRules
.IsGoodFen(fen
)) return false;
60 const fenParts
= fen
.split(" ");
61 if (fenParts
.length
!= 5) return false;
62 if (fenParts
[4] != "-") {
63 // TODO: a single regexp instead.
64 // Format is [bpa2[.wpd3]] || '-'/[bbc3[.wrd5]] || '-'
65 const amoveParts
= fenParts
[4].split("/");
66 if (amoveParts
.length
!= 2) return false;
67 for (let part
of amoveParts
) {
69 for (let psq
of part
.split("."))
70 if (!psq
.match(/^[a-r]{3}[1-8]$/)) return false;
78 return super.getFen() + " " + this.getAmoveFen();
82 return super.getFenForRepeat() + "_" + this.getAmoveFen();
86 const L
= this.amoves
.length
;
87 if (L
== 0) return "-";
89 ["appear","vanish"].map(
91 if (this.amoves
[L
-1][mpart
].length
== 0) return "-";
93 this.amoves
[L
-1][mpart
].map(
95 const square
= V
.CoordsToSquare({ x: av
.x
, y: av
.y
});
96 return av
.c
+ av
.p
+ square
;
106 // Captures don't occur (only pulls & pushes)
110 // Step is right, just add (push/pull) moves in this direction
111 // Direction is assumed normalized.
112 getMovesInDirection([x
, y
], [dx
, dy
], nbSteps
) {
113 nbSteps
= nbSteps
|| 8; //max 8 steps anyway
114 let [i
, j
] = [x
+ dx
, y
+ dy
];
116 const color
= this.getColor(x
, y
);
117 const piece
= this.getPiece(x
, y
);
118 const lastRank
= (color
== 'w' ? 0 : 7);
120 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
121 if (i
== lastRank
&& piece
== V
.PAWN
) {
122 // Promotion by push or pull
123 V
.PawnSpecs
.promotions
.forEach(p
=> {
124 let move = super.getBasicMove([x
, y
], [i
, j
], { c: color
, p: p
});
128 else moves
.push(super.getBasicMove([x
, y
], [i
, j
]));
129 if (++counter
> nbSteps
) break;
133 if (!V
.OnBoard(i
, j
) && piece
!= V
.KING
) {
134 // Add special "exit" move, by "taking king"
137 start: { x: x
, y: y
},
138 end: { x: this.kingPos
[color
][0], y: this.kingPos
[color
][1] },
140 vanish: [{ x: x
, y: y
, c: color
, p: piece
}]
147 // Normalize direction to know the step
148 getNormalizedDirection([dx
, dy
]) {
149 const absDir
= [Math
.abs(dx
), Math
.abs(dy
)];
151 if (absDir
[0] != 0 && absDir
[1] != 0 && absDir
[0] != absDir
[1])
153 divisor
= Math
.min(absDir
[0], absDir
[1]);
155 // Standard slider (or maybe a pawn or king: same)
156 divisor
= Math
.max(absDir
[0], absDir
[1]);
157 return [dx
/ divisor
, dy
/ divisor
];
160 // There was something on x2,y2, maybe our color, pushed/pulled.
161 // Also, the pushed/pulled piece must exit the board.
162 isAprioriValidExit([x1
, y1
], [x2
, y2
], color2
) {
163 const color1
= this.getColor(x1
, y1
);
164 const pawnShift
= (color1
== 'w' ? -1 : 1);
165 const lastRank
= (color1
== 'w' ? 0 : 7);
166 const deltaX
= Math
.abs(x1
- x2
);
167 const deltaY
= Math
.abs(y1
- y2
);
168 const checkSlider
= () => {
169 const dir
= this.getNormalizedDirection([x2
- x1
, y2
- y1
]);
170 let [i
, j
] = [x1
+ dir
[0], y1
+ dir
[1]];
171 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
175 return !V
.OnBoard(i
, j
);
177 switch (this.getPiece(x1
, y1
)) {
180 x1
+ pawnShift
== x2
&&
182 (color1
== color2
&& x2
== lastRank
&& y1
== y2
) ||
183 (color1
!= color2
&& deltaY
== 1 && !V
.OnBoard(x2
, 2 * y2
- y1
))
187 if (x1
!= x2
&& y1
!= y2
) return false;
188 return checkSlider();
191 deltaX
+ deltaY
== 3 &&
192 (deltaX
== 1 || deltaY
== 1) &&
193 !V
.OnBoard(2 * x2
- x1
, 2 * y2
- y1
)
196 if (deltaX
!= deltaY
) return false;
197 return checkSlider();
199 if (deltaX
!= 0 && deltaY
!= 0 && deltaX
!= deltaY
) return false;
200 return checkSlider();
205 !V
.OnBoard(2 * x2
- x1
, 2 * y2
- y1
)
211 // NOTE: for pushes, play the pushed piece first.
212 // for pulls: play the piece doing the action first
213 // NOTE: to push a piece out of the board, make it slide until its king
214 getPotentialMovesFrom([x
, y
]) {
215 const color
= this.turn
;
216 if (this.subTurn
== 1) {
217 const getMoveHash
= (m
) => {
218 return V
.CoordsToSquare(m
.start
) + V
.CoordsToSquare(m
.end
);
220 const addMoves
= (dir
, nbSteps
) => {
222 this.getMovesInDirection([x
, y
], [-dir
[0], -dir
[1]], nbSteps
)
223 .filter(m
=> !movesHash
[getMoveHash(m
)]);
224 newMoves
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
225 Array
.prototype.push
.apply(moves
, newMoves
);
227 // Free to play any move (if piece of my color):
229 this.getColor(x
, y
) == color
230 ? super.getPotentialMovesFrom([x
, y
])
232 // There may be several suicide moves: keep only one
234 moves
= moves
.filter(m
=> {
235 const suicide
= (m
.appear
.length
== 0);
237 if (hasExit
) return false;
242 const pawnShift
= (color
== 'w' ? -1 : 1);
243 const pawnStartRank
= (color
== 'w' ? 6 : 1);
244 // Structure to avoid adding moves twice (can be action & move)
246 moves
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
247 // [x, y] is pushed by 'color'
248 for (let step
of V
.steps
[V
.KNIGHT
]) {
249 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
252 this.board
[i
][j
] != V
.EMPTY
&&
253 this.getColor(i
, j
) == color
&&
254 this.getPiece(i
, j
) == V
.KNIGHT
259 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
260 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
261 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
267 this.board
[i
][j
] != V
.EMPTY
&&
268 this.getColor(i
, j
) == color
270 const deltaX
= Math
.abs(i
- x
);
271 const deltaY
= Math
.abs(j
- y
);
272 switch (this.getPiece(i
, j
)) {
275 (x
- i
) / deltaX
== pawnShift
&&
279 const pColor
= this.getColor(x
, y
);
280 if (pColor
== color
&& deltaY
== 0) {
282 const maxSteps
= (i
== pawnStartRank
&& deltaX
== 1 ? 2 : 1);
283 addMoves(step
, maxSteps
);
285 else if (pColor
!= color
&& deltaY
== 1 && deltaX
== 1)
291 if (deltaX
== 0 || deltaY
== 0) addMoves(step
);
294 if (deltaX
== deltaY
) addMoves(step
);
297 // All steps are valid for a queen:
301 if (deltaX
<= 1 && deltaY
<= 1) addMoves(step
, 1);
308 // If subTurn == 2 then we should have a first move,
309 // which restrict what we can play now: only in the first move direction
310 // NOTE: no need for knight or pawn checks, because the move will be
311 // naturally limited in those cases.
312 const L
= this.firstMove
.length
;
313 const fm
= this.firstMove
[L
-1];
314 if (fm
.appear
.length
== 2 && fm
.vanish
.length
== 2)
315 // Castle: no real move playable then.
317 if (fm
.appear
.length
== 0) {
318 // Piece at subTurn 1 just exited the board.
319 // Can I be a piece which caused the exit?
321 this.isAprioriValidExit(
323 [fm
.start
.x
, fm
.start
.y
],
328 const dir
= this.getNormalizedDirection(
329 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
330 return this.getMovesInDirection([x
, y
], dir
);
334 const dirM
= this.getNormalizedDirection(
335 [fm
.end
.x
- fm
.start
.x
, fm
.end
.y
- fm
.start
.y
]);
336 const dir
= this.getNormalizedDirection(
337 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
338 // Normalized directions should match
339 if (dir
[0] == dirM
[0] && dir
[1] == dirM
[1]) {
340 // And nothing should stand between [x, y] and the square fm.start
341 let [i
, j
] = [x
+ dir
[0], y
+ dir
[1]];
343 (i
!= fm
.start
.x
|| j
!= fm
.start
.y
) &&
344 this.board
[i
][j
] == V
.EMPTY
349 if (i
== fm
.start
.x
&& j
== fm
.start
.y
)
350 return this.getMovesInDirection([x
, y
], dir
);
356 getSlideNJumpMoves([x
, y
], steps
, oneStep
) {
358 outerLoop: for (let step
of steps
) {
361 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
362 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
363 if (oneStep
) continue outerLoop
;
367 if (V
.OnBoard(i
, j
)) {
368 if (this.canTake([x
, y
], [i
, j
]))
369 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
372 // Add potential board exit (suicide), except for the king
373 const piece
= this.getPiece(x
, y
);
374 if (piece
!= V
.KING
) {
375 const c
= this.getColor(x
, y
);
377 start: { x: x
, y: y
},
378 end: { x: this.kingPos
[c
][0], y: this.kingPos
[c
][1] },
395 // Does m2 un-do m1 ? (to disallow undoing actions)
396 oppositeMoves(m1
, m2
) {
397 const isEqual
= (av1
, av2
) => {
398 // Precondition: av1 and av2 length = 2
399 for (let av
of av1
) {
400 const avInAv2
= av2
.find(elt
=> {
408 if (!avInAv2
) return false;
413 m1
.appear
.length
== 2 &&
414 m2
.appear
.length
== 2 &&
415 m1
.vanish
.length
== 2 &&
416 m2
.vanish
.length
== 2 &&
417 isEqual(m1
.appear
, m2
.vanish
) &&
418 isEqual(m1
.vanish
, m2
.appear
)
422 getAmove(move1
, move2
) {
423 // Just merge (one is action one is move, one may be empty)
425 appear: move1
.appear
.concat(move2
.appear
),
426 vanish: move1
.vanish
.concat(move2
.vanish
)
431 const color
= this.turn
;
432 if (this.subTurn
== 1) {
433 return moves
.filter(m
=> {
434 // A move is valid either if it doesn't result in a check,
435 // or if a second move is possible to counter the check
436 // (not undoing a potential move + action of the opponent)
438 let res
= this.underCheck(color
);
440 const moves2
= this.getAllPotentialMoves();
441 for (let m2
of moves2
) {
443 const res2
= this.underCheck(color
);
455 const Lf
= this.firstMove
.length
;
456 const La
= this.amoves
.length
;
457 if (La
== 0) return super.filterValid(moves
);
461 // Move shouldn't undo another:
462 const amove
= this.getAmove(this.firstMove
[Lf
-1], m
);
463 return !this.oppositeMoves(this.amoves
[La
-1], amove
);
469 isAttackedBySlideNJump([x
, y
], color
, piece
, steps
, oneStep
) {
470 for (let step
of steps
) {
471 let rx
= x
+ step
[0],
473 while (V
.OnBoard(rx
, ry
) && this.board
[rx
][ry
] == V
.EMPTY
&& !oneStep
) {
479 this.getPiece(rx
, ry
) == piece
&&
480 this.getColor(rx
, ry
) == color
482 // Continue some steps in the same direction (pull)
487 this.board
[rx
][ry
] == V
.EMPTY
&&
493 if (!V
.OnBoard(rx
, ry
)) return true;
494 // Step in the other direction (push)
499 this.board
[rx
][ry
] == V
.EMPTY
&&
505 if (!V
.OnBoard(rx
, ry
)) return true;
511 isAttackedByPawn([x
, y
], color
) {
512 const lastRank
= (color
== 'w' ? 0 : 7);
514 // The king can be pushed out by a pawn only on last rank
516 const pawnShift
= (color
== "w" ? 1 : -1);
517 for (let i
of [-1, 1]) {
521 this.getPiece(x
+ pawnShift
, y
+ i
) == V
.PAWN
&&
522 this.getColor(x
+ pawnShift
, y
+ i
) == color
530 // No consideration of color: all pieces could be played
531 getAllPotentialMoves() {
532 let potentialMoves
= [];
533 for (let i
= 0; i
< V
.size
.x
; i
++) {
534 for (let j
= 0; j
< V
.size
.y
; j
++) {
535 if (this.board
[i
][j
] != V
.EMPTY
) {
536 Array
.prototype.push
.apply(
538 this.getPotentialMovesFrom([i
, j
])
543 return potentialMoves
;
547 if (this.subTurn
== 2)
550 return super.getCurrentScore();
554 // If subTurn == 2 && square is empty && !underCheck,
555 // then return an empty move, allowing to "pass" subTurn2
558 this.board
[square
[0]][square
[1]] == V
.EMPTY
&&
559 !this.underCheck(this.turn
)
562 start: { x: -1, y: -1 },
563 end: { x: -1, y: -1 },
572 move.flags
= JSON
.stringify(this.aggregateFlags());
573 V
.PlayOnBoard(this.board
, move);
574 if (this.subTurn
== 2) {
575 const L
= this.firstMove
.length
;
576 this.amoves
.push(this.getAmove(this.firstMove
[L
-1], move));
577 this.turn
= V
.GetOppCol(this.turn
);
580 else this.firstMove
.push(move);
581 this.subTurn
= 3 - this.subTurn
;
586 if (move.start
.x
< 0) return;
587 for (let a
of move.appear
)
588 if (a
.p
== V
.KING
) this.kingPos
[a
.c
] = [a
.x
, a
.y
];
589 this.updateCastleFlags(move);
592 updateCastleFlags(move) {
593 const firstRank
= { 'w': V
.size
.x
- 1, 'b': 0 };
594 for (let v
of move.vanish
) {
595 if (v
.p
== V
.KING
) this.castleFlags
[v
.c
] = [V
.size
.y
, V
.size
.y
];
596 else if (v
.x
== firstRank
[v
.c
] && this.castleFlags
[v
.c
].includes(v
.y
)) {
597 const flagIdx
= (v
.y
== this.castleFlags
[v
.c
][0] ? 0 : 1);
598 this.castleFlags
[v
.c
][flagIdx
] = V
.size
.y
;
604 this.disaggregateFlags(JSON
.parse(move.flags
));
605 V
.UndoOnBoard(this.board
, move);
606 if (this.subTurn
== 1) {
607 this.turn
= V
.GetOppCol(this.turn
);
610 else this.firstMove
.pop();
611 this.subTurn
= 3 - this.subTurn
;
616 // (Potentially) Reset king position
617 for (let v
of move.vanish
)
618 if (v
.p
== V
.KING
) this.kingPos
[v
.c
] = [v
.x
, v
.y
];
622 let moves
= this.getAllValidMoves();
623 if (moves
.length
== 0) return null;
624 // "Search" at depth 1 for now
625 const maxeval
= V
.INFINITY
;
626 const color
= this.turn
;
628 start: { x: -1, y: -1 },
629 end: { x: -1, y: -1 },
635 m
.eval
= (color
== "w" ? -1 : 1) * maxeval
;
636 const moves2
= this.getAllValidMoves().concat([emptyMove
]);
638 moves2
.forEach(m2
=> {
640 const score
= this.getCurrentScore();
642 if (score
!= "1/2") {
643 if (score
!= "*") mvEval
= (score
== "1-0" ? 1 : -1) * maxeval
;
644 else mvEval
= this.evalPosition();
647 (color
== 'w' && mvEval
> m
.eval
) ||
648 (color
== 'b' && mvEval
< m
.eval
)
657 moves
.sort((a
, b
) => {
658 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
660 let candidates
= [0];
661 for (let i
= 1; i
< moves
.length
&& moves
[i
].eval
== moves
[0].eval
; i
++)
663 const mIdx
= candidates
[randInt(candidates
.length
)];
664 const move2
= moves
[mIdx
].next
;
665 delete moves
[mIdx
]["next"];
666 return [moves
[mIdx
], move2
];
670 if (move.start
.x
< 0)
671 // A second move is always required, but may be empty
673 const initialSquare
= V
.CoordsToSquare(move.start
);
674 const finalSquare
= V
.CoordsToSquare(move.end
);
675 if (move.appear
.length
== 0)
676 // Pushed or pulled out of the board
677 return initialSquare
+ "R";
678 return move.appear
[0].p
.toUpperCase() + initialSquare
+ finalSquare
;