05f2aed956d2fbd9b51e54631147323621204bfd
1 import ChessRules
from "/base_rules.js";
3 export default class DynamoRules
extends ChessRules
{
10 return this.options
["enpassant"];
14 // Sometimes opponent's pieces can be moved directly
15 return this.playerColor
== this.turn
;
19 // Captures don't occur (only pulls & pushes)
23 setOtherVariables(fenParsed
) {
24 super.setOtherVariables(fenParsed
);
26 // Last action format: e2h5/d1g4 for queen on d1 pushing pawn to h5
27 // for example, and moving herself to g4. If just move: e2h5
29 if (fenParsed
.amove
!= '-') {
30 this.lastAction
= fenParsed
.amove
.split('/').map(a
=> {
32 c1: C
.SquareToCoords(C
.SquareFromUsual(a
.substr(0, 2))),
33 c2: C
.SquareToCoords(C
.SquareFromUsual(a
.substr(2, 2)))
40 let res
= super.getPartFen(o
);
44 res
["amove"] = this.lastAction
.map(a
=> {
45 C
.UsualFromSquare(C
.CoordsToSquare(a
.c1
)) +
46 C
.UsualFromSquare(C
.CoordsToSquare(a
.c2
))
52 // Step is right, just add (push/pull) moves in this direction
53 // Direction is assumed normalized.
54 getMovesInDirection([x
, y
], [dx
, dy
], nbSteps
) {
55 nbSteps
= nbSteps
|| 8; //max 8 steps anyway
56 let [i
, j
] = [x
+ dx
, y
+ dy
];
58 const color
= this.getColor(x
, y
);
59 const piece
= this.getPiece(x
, y
);
60 const lastRank
= (color
== 'w' ? 0 : 7);
62 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
63 if (i
== lastRank
&& piece
== V
.PAWN
) {
64 // Promotion by push or pull
65 V
.PawnSpecs
.promotions
.forEach(p
=> {
66 let move = super.getBasicMove([x
, y
], [i
, j
], { c: color
, p: p
});
70 else moves
.push(super.getBasicMove([x
, y
], [i
, j
]));
71 if (++counter
> nbSteps
) break;
75 if (!V
.OnBoard(i
, j
) && piece
!= V
.KING
) {
76 // Add special "exit" move, by "taking king"
79 start: { x: x
, y: y
},
80 end: { x: this.kingPos
[color
][0], y: this.kingPos
[color
][1] },
82 vanish: [{ x: x
, y: y
, c: color
, p: piece
}]
89 // Normalize direction to know the step
90 getNormalizedDirection([dx
, dy
]) {
91 const absDir
= [Math
.abs(dx
), Math
.abs(dy
)];
93 if (absDir
[0] != 0 && absDir
[1] != 0 && absDir
[0] != absDir
[1])
95 divisor
= Math
.min(absDir
[0], absDir
[1]);
97 // Standard slider (or maybe a pawn or king: same)
98 divisor
= Math
.max(absDir
[0], absDir
[1]);
99 return [dx
/ divisor
, dy
/ divisor
];
102 // There was something on x2,y2, maybe our color, pushed or (self)pulled
103 isAprioriValidExit([x1
, y1
], [x2
, y2
], color2
, piece2
) {
104 const color1
= this.getColor(x1
, y1
);
105 const pawnShift
= (color1
== 'w' ? -1 : 1);
106 const lastRank
= (color1
== 'w' ? 0 : 7);
107 const deltaX
= Math
.abs(x1
- x2
);
108 const deltaY
= Math
.abs(y1
- y2
);
109 const checkSlider
= () => {
110 const dir
= this.getNormalizedDirection([x2
- x1
, y2
- y1
]);
111 let [i
, j
] = [x1
+ dir
[0], y1
+ dir
[1]];
112 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
116 return !V
.OnBoard(i
, j
);
118 switch (piece2
|| this.getPiece(x1
, y1
)) {
121 x1
+ pawnShift
== x2
&&
123 (color1
== color2
&& x2
== lastRank
&& y1
== y2
) ||
127 !V
.OnBoard(2 * x2
- x1
, 2 * y2
- y1
)
132 if (x1
!= x2
&& y1
!= y2
) return false;
133 return checkSlider();
136 deltaX
+ deltaY
== 3 &&
137 (deltaX
== 1 || deltaY
== 1) &&
138 !V
.OnBoard(2 * x2
- x1
, 2 * y2
- y1
)
141 if (deltaX
!= deltaY
) return false;
142 return checkSlider();
144 if (deltaX
!= 0 && deltaY
!= 0 && deltaX
!= deltaY
) return false;
145 return checkSlider();
150 !V
.OnBoard(2 * x2
- x1
, 2 * y2
- y1
)
156 isAprioriValidVertical([x1
, y1
], x2
) {
157 const piece
= this.getPiece(x1
, y1
);
158 const deltaX
= Math
.abs(x1
- x2
);
159 const startRank
= (this.getColor(x1
, y1
) == 'w' ? 6 : 1);
161 [V
.QUEEN
, V
.ROOK
].includes(piece
) ||
163 [V
.KING
, V
.PAWN
].includes(piece
) &&
166 (deltaX
== 2 && piece
== V
.PAWN
&& x1
== startRank
)
172 // NOTE: for pushes, play the pushed piece first.
173 // for pulls: play the piece doing the action first
174 // NOTE: to push a piece out of the board, make it slide until its king
175 getPotentialMovesFrom([x
, y
]) {
176 const color
= this.turn
;
177 const sqCol
= this.getColor(x
, y
);
178 const pawnShift
= (color
== 'w' ? -1 : 1);
179 const pawnStartRank
= (color
== 'w' ? 6 : 1);
180 const getMoveHash
= (m
) => {
181 return V
.CoordsToSquare(m
.start
) + V
.CoordsToSquare(m
.end
);
183 if (this.subTurn
== 1) {
184 const addMoves
= (dir
, nbSteps
) => {
186 this.getMovesInDirection([x
, y
], [-dir
[0], -dir
[1]], nbSteps
)
187 .filter(m
=> !movesHash
[getMoveHash(m
)]);
188 newMoves
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
189 Array
.prototype.push
.apply(moves
, newMoves
);
191 // Free to play any move (if piece of my color):
194 ? super.getPotentialMovesFrom([x
, y
])
196 // There may be several suicide moves: keep only one
198 moves
= moves
.filter(m
=> {
199 const suicide
= (m
.appear
.length
== 0);
201 if (hasExit
) return false;
206 // Structure to avoid adding moves twice (can be action & move)
208 moves
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
209 // [x, y] is pushed by 'color'
210 for (let step
of V
.steps
[V
.KNIGHT
]) {
211 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
214 this.board
[i
][j
] != V
.EMPTY
&&
215 this.getColor(i
, j
) == color
&&
216 this.getPiece(i
, j
) == V
.KNIGHT
221 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
222 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
223 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
229 this.board
[i
][j
] != V
.EMPTY
&&
230 this.getColor(i
, j
) == color
232 const deltaX
= Math
.abs(i
- x
);
233 const deltaY
= Math
.abs(j
- y
);
234 switch (this.getPiece(i
, j
)) {
237 (x
- i
) / deltaX
== pawnShift
&&
241 if (sqCol
== color
&& deltaY
== 0) {
243 const maxSteps
= (i
== pawnStartRank
&& deltaX
== 1 ? 2 : 1);
244 addMoves(step
, maxSteps
);
246 else if (sqCol
!= color
&& deltaY
== 1 && deltaX
== 1)
252 if (deltaX
== 0 || deltaY
== 0) addMoves(step
);
255 if (deltaX
== deltaY
) addMoves(step
);
258 // All steps are valid for a queen:
262 if (deltaX
<= 1 && deltaY
<= 1) addMoves(step
, 1);
269 // If subTurn == 2 then we should have a first move,
270 // which restrict what we can play now: only in the first move direction
271 const L
= this.firstMove
.length
;
272 const fm
= this.firstMove
[L
-1];
274 (fm
.appear
.length
== 2 && fm
.vanish
.length
== 2) ||
275 (fm
.vanish
[0].c
== sqCol
&& sqCol
!= color
)
277 // Castle or again opponent color: no move playable then.
280 const piece
= this.getPiece(x
, y
);
281 const getPushExit
= () => {
282 // Piece at subTurn 1 exited: can I have caused the exit?
284 this.isAprioriValidExit(
286 [fm
.start
.x
, fm
.start
.y
],
291 const dir
= this.getNormalizedDirection(
292 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
294 [V
.PAWN
, V
.KING
, V
.KNIGHT
].includes(piece
)
297 return this.getMovesInDirection([x
, y
], dir
, nbSteps
);
301 const getPushMoves
= () => {
302 // Piece from subTurn 1 is still on board:
303 const dirM
= this.getNormalizedDirection(
304 [fm
.end
.x
- fm
.start
.x
, fm
.end
.y
- fm
.start
.y
]);
305 const dir
= this.getNormalizedDirection(
306 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
307 // Normalized directions should match
308 if (dir
[0] == dirM
[0] && dir
[1] == dirM
[1]) {
309 // We don't know if first move is a pushed piece or normal move,
310 // so still must check if the push is valid.
311 const deltaX
= Math
.abs(fm
.start
.x
- x
);
312 const deltaY
= Math
.abs(fm
.start
.y
- y
);
315 if (x
== pawnStartRank
) {
317 (fm
.start
.x
- x
) * pawnShift
< 0 ||
320 (fm
.vanish
[0].c
== color
&& deltaY
> 0) ||
321 (fm
.vanish
[0].c
!= color
&& deltaY
== 0) ||
322 Math
.abs(fm
.end
.x
- fm
.start
.x
) > deltaX
||
323 fm
.end
.y
- fm
.start
.y
!= fm
.start
.y
- y
330 fm
.start
.x
- x
!= pawnShift
||
332 (fm
.vanish
[0].c
== color
&& deltaY
== 1) ||
333 (fm
.vanish
[0].c
!= color
&& deltaY
== 0) ||
334 fm
.end
.x
- fm
.start
.x
!= pawnShift
||
335 fm
.end
.y
- fm
.start
.y
!= fm
.start
.y
- y
343 (deltaX
+ deltaY
!= 3 || (deltaX
== 0 && deltaY
== 0)) ||
344 (fm
.end
.x
- fm
.start
.x
!= fm
.start
.x
- x
) ||
345 (fm
.end
.y
- fm
.start
.y
!= fm
.start
.y
- y
)
352 (deltaX
>= 2 || deltaY
>= 2) ||
353 (fm
.end
.x
- fm
.start
.x
!= fm
.start
.x
- x
) ||
354 (fm
.end
.y
- fm
.start
.y
!= fm
.start
.y
- y
)
360 if (deltaX
!= deltaY
) return [];
363 if (deltaX
!= 0 && deltaY
!= 0) return [];
366 if (deltaX
!= deltaY
&& deltaX
!= 0 && deltaY
!= 0) return [];
369 // Nothing should stand between [x, y] and the square fm.start
370 let [i
, j
] = [x
+ dir
[0], y
+ dir
[1]];
372 (i
!= fm
.start
.x
|| j
!= fm
.start
.y
) &&
373 this.board
[i
][j
] == V
.EMPTY
378 if (i
== fm
.start
.x
&& j
== fm
.start
.y
)
379 return this.getMovesInDirection([x
, y
], dir
);
383 const getPullExit
= () => {
384 // Piece at subTurn 1 exited: can I be pulled?
385 // Note: kings cannot suicide, so fm.vanish[0].p is not KING.
386 // Could be PAWN though, if a pawn was pushed out of board.
388 fm
.vanish
[0].p
!= V
.PAWN
&& //pawns cannot pull
389 this.isAprioriValidExit(
391 [fm
.start
.x
, fm
.start
.y
],
397 const dir
= this.getNormalizedDirection(
398 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
399 const nbSteps
= (fm
.vanish
[0].p
== V
.KNIGHT
? 1 : null);
400 return this.getMovesInDirection([x
, y
], dir
, nbSteps
);
404 const getPullMoves
= () => {
405 if (fm
.vanish
[0].p
== V
.PAWN
)
408 const dirM
= this.getNormalizedDirection(
409 [fm
.end
.x
- fm
.start
.x
, fm
.end
.y
- fm
.start
.y
]);
410 const dir
= this.getNormalizedDirection(
411 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
412 // Normalized directions should match
413 if (dir
[0] == dirM
[0] && dir
[1] == dirM
[1]) {
414 // Am I at the right distance?
415 const deltaX
= Math
.abs(x
- fm
.start
.x
);
416 const deltaY
= Math
.abs(y
- fm
.start
.y
);
418 (fm
.vanish
[0].p
== V
.KING
&& (deltaX
> 1 || deltaY
> 1)) ||
419 (fm
.vanish
[0].p
== V
.KNIGHT
&&
420 (deltaX
+ deltaY
!= 3 || deltaX
== 0 || deltaY
== 0))
424 // Nothing should stand between [x, y] and the square fm.start
425 let [i
, j
] = [x
+ dir
[0], y
+ dir
[1]];
427 (i
!= fm
.start
.x
|| j
!= fm
.start
.y
) &&
428 this.board
[i
][j
] == V
.EMPTY
433 if (i
== fm
.start
.x
&& j
== fm
.start
.y
)
434 return this.getMovesInDirection([x
, y
], dir
);
438 if (fm
.vanish
[0].c
!= color
) {
439 // Only possible action is a push:
440 if (fm
.appear
.length
== 0) return getPushExit();
441 return getPushMoves();
443 else if (sqCol
!= color
) {
444 // Only possible action is a pull, considering moving piece abilities
445 if (fm
.appear
.length
== 0) return getPullExit();
446 return getPullMoves();
449 // My color + my color: both actions possible
450 // Structure to avoid adding moves twice (can be action & move)
452 if (fm
.appear
.length
== 0) {
453 const pushes
= getPushExit();
454 pushes
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
456 pushes
.concat(getPullExit().filter(m
=> !movesHash
[getMoveHash(m
)]))
459 const pushes
= getPushMoves();
460 pushes
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
462 pushes
.concat(getPullMoves().filter(m
=> !movesHash
[getMoveHash(m
)]))
468 getSlideNJumpMoves([x
, y
], steps
, oneStep
) {
470 const c
= this.getColor(x
, y
);
471 const piece
= this.getPiece(x
, y
);
472 outerLoop: for (let step
of steps
) {
475 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
476 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
477 if (oneStep
) continue outerLoop
;
481 if (V
.OnBoard(i
, j
)) {
482 if (this.canTake([x
, y
], [i
, j
]))
483 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
486 // Add potential board exit (suicide), except for the king
487 if (piece
!= V
.KING
) {
489 start: { x: x
, y: y
},
490 end: { x: this.kingPos
[c
][0], y: this.kingPos
[c
][1] },
507 // Does m2 un-do m1 ? (to disallow undoing actions)
508 oppositeMoves(m1
, m2
) {
509 const isEqual
= (av1
, av2
) => {
510 for (let av
of av1
) {
511 const avInAv2
= av2
.find(elt
=> {
519 if (!avInAv2
) return false;
523 // All appear and vanish arrays must have the same length
524 const mL
= m1
.appear
.length
;
526 m2
.appear
.length
== mL
&&
527 m1
.vanish
.length
== mL
&&
528 m2
.vanish
.length
== mL
&&
529 isEqual(m1
.appear
, m2
.vanish
) &&
530 isEqual(m1
.vanish
, m2
.appear
)
534 // TODO: just stack in this.lastAction instead
535 getAmove(move1
, move2
) {
536 // Just merge (one is action one is move, one may be empty)
538 appear: move1
.appear
.concat(move2
.appear
),
539 vanish: move1
.vanish
.concat(move2
.vanish
)
544 const color
= this.turn
;
545 const La
= this.amoves
.length
;
546 if (this.subTurn
== 1) {
547 return moves
.filter(m
=> {
548 // A move is valid either if it doesn't result in a check,
549 // or if a second move is possible to counter the check
550 // (not undoing a potential move + action of the opponent)
552 let res
= this.underCheck(color
);
553 if (this.subTurn
== 2) {
554 let isOpposite
= La
> 0 && this.oppositeMoves(this.amoves
[La
-1], m
);
555 if (res
|| isOpposite
) {
556 const moves2
= this.getAllPotentialMoves();
557 for (let m2
of moves2
) {
559 const res2
= this.underCheck(color
);
560 const amove
= this.getAmove(m
, m2
);
562 La
> 0 && this.oppositeMoves(this.amoves
[La
-1], amove
);
564 if (!res2
&& !isOpposite
) {
575 if (La
== 0) return super.filterValid(moves
);
576 const Lf
= this.firstMove
.length
;
580 // Move shouldn't undo another:
581 const amove
= this.getAmove(this.firstMove
[Lf
-1], m
);
582 return !this.oppositeMoves(this.amoves
[La
-1], amove
);
590 start: { x: -1, y: -1 },
591 end: { x: -1, y: -1 },
598 // A click to promote a piece on subTurn 2 would trigger this.
599 // For now it would then return [NaN, NaN] because surrounding squares
600 // have no IDs in the promotion modal. TODO: improve this?
601 if (isNaN(square
[0])) return null;
602 // If subTurn == 2 && square is empty && !underCheck && !isOpposite,
603 // then return an empty move, allowing to "pass" subTurn2
604 const La
= this.amoves
.length
;
605 const Lf
= this.firstMove
.length
;
608 this.board
[square
[0]][square
[1]] == V
.EMPTY
&&
609 !this.underCheck(this.turn
) &&
610 (La
== 0 || !this.oppositeMoves(this.amoves
[La
-1], this.firstMove
[Lf
-1]))
612 return this.getEmptyMove();
618 if (this.subTurn
== 1 && move.vanish
.length
== 0) {
619 // Patch to work with old format: (TODO: remove later)
623 const color
= this.turn
;
624 move.subTurn
= this.subTurn
; //for undo
625 const gotoNext
= (mv
) => {
626 const L
= this.firstMove
.length
;
627 this.amoves
.push(this.getAmove(this.firstMove
[L
-1], mv
));
628 this.turn
= V
.GetOppCol(color
);
632 move.flags
= JSON
.stringify(this.aggregateFlags());
633 V
.PlayOnBoard(this.board
, move);
634 if (this.subTurn
== 2) gotoNext(move);
637 this.firstMove
.push(move);
638 this.toNewKingPos(move);
640 // Condition is true on empty arrays:
641 this.getAllPotentialMoves().every(m
=> {
642 V
.PlayOnBoard(this.board
, m
);
643 this.toNewKingPos(m
);
644 const res
= this.underCheck(color
);
645 V
.UndoOnBoard(this.board
, m
);
646 this.toOldKingPos(m
);
650 // No valid move at subTurn 2
651 gotoNext(this.getEmptyMove());
653 this.toOldKingPos(move);
659 for (let a
of move.appear
)
660 if (a
.p
== V
.KING
) this.kingPos
[a
.c
] = [a
.x
, a
.y
];
664 if (move.start
.x
< 0) return;
665 this.toNewKingPos(move);
666 this.updateCastleFlags(move);
669 updateCastleFlags(move) {
670 const firstRank
= { 'w': V
.size
.x
- 1, 'b': 0 };
671 for (let v
of move.vanish
) {
672 if (v
.p
== V
.KING
) this.castleFlags
[v
.c
] = [V
.size
.y
, V
.size
.y
];
673 else if (v
.x
== firstRank
[v
.c
] && this.castleFlags
[v
.c
].includes(v
.y
)) {
674 const flagIdx
= (v
.y
== this.castleFlags
[v
.c
][0] ? 0 : 1);
675 this.castleFlags
[v
.c
][flagIdx
] = V
.size
.y
;
681 if (!!move.ignore
) return; //TODO: remove that later
682 this.disaggregateFlags(JSON
.parse(move.flags
));
683 V
.UndoOnBoard(this.board
, move);
684 if (this.subTurn
== 1) {
686 this.turn
= V
.GetOppCol(this.turn
);
689 if (move.subTurn
== 1) this.firstMove
.pop();
690 this.subTurn
= move.subTurn
;
691 this.toOldKingPos(move);
695 // (Potentially) Reset king position
696 for (let v
of move.vanish
)
697 if (v
.p
== V
.KING
) this.kingPos
[v
.c
] = [v
.x
, v
.y
];