1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
3 export class DynamoRules
extends ChessRules
{
4 // TODO: later, allow to push out pawns on a and h files
5 static get HasEnpassant() {
9 canIplay(side
, [x
, y
]) {
10 // Sometimes opponent's pieces can be moved directly
14 setOtherVariables(fen
) {
15 super.setOtherVariables(fen
);
17 // Local stack of "action moves"
19 const amove
= V
.ParseFen(fen
).amove
;
21 const amoveParts
= amove
.split("/");
23 // No need for start & end
28 amoveParts
[i
].split(".").forEach(av
=> {
30 const xy
= V
.SquareToCoords(av
.substr(2));
31 move[i
== 0 ? "appear" : "vanish"].push(
41 this.amoves
.push(move);
44 // Stack "first moves" (on subTurn 1) to merge and check opposite moves
48 static ParseFen(fen
) {
50 ChessRules
.ParseFen(fen
),
51 { amove: fen
.split(" ")[4] }
55 static IsGoodFen(fen
) {
56 if (!ChessRules
.IsGoodFen(fen
)) return false;
57 const fenParts
= fen
.split(" ");
58 if (fenParts
.length
!= 6) return false;
59 if (fenParts
[5] != "-" && !fenParts
[5].match(/^([a-h][1-8]){2}$/))
65 return super.getFen() + " " + this.getAmoveFen();
69 return super.getFenForRepeat() + "_" + this.getAmoveFen();
73 const L
= this.amoves
.length
;
74 if (L
== 0) return "-";
76 ["appear","vanish"].map(
79 this.amoves
[L
-1][mpart
].map(
81 const square
= V
.CoordsToSquare({ x: av
.x
, y: av
.y
});
82 return av
.c
+ av
.p
+ square
;
92 // Captures don't occur (only pulls & pushes)
96 // Step is right, just add (push/pull) moves in this direction
97 // Direction is assumed normalized.
98 getMovesInDirection([x
, y
], [dx
, dy
], nbSteps
) {
99 nbSteps
= nbSteps
|| 8; //max 8 steps anyway
100 let [i
, j
] = [x
+ dx
, y
+ dy
];
102 const color
= this.getColor(x
, y
);
103 const piece
= this.getPiece(x
, y
);
104 const lastRank
= (color
== 'w' ? 0 : 7);
106 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
107 if (i
== lastRank
&& piece
== V
.PAWN
) {
108 // Promotion by push or pull
109 V
.PawnSpecs
.promotions
.forEach(p
=> {
110 let move = super.getBasicMove([x
, y
], [i
, j
], { c: color
, p: p
});
114 else moves
.push(super.getBasicMove([x
, y
], [i
, j
]));
115 if (++counter
> nbSteps
) break;
119 if (!V
.OnBoard(i
, j
) && piece
!= V
.KING
) {
120 // Add special "exit" move, by "taking king"
123 start: { x: x
, y: y
},
124 end: { x: this.kingPos
[color
][0], y: this.kingPos
[color
][1] },
126 vanish: [{ x: x
, y: y
, c: color
, p: piece
}]
133 // Normalize direction to know the step
134 getNormalizedDirection([dx
, dy
]) {
135 const absDir
= [Math
.abs(dx
), Math
.abs(dy
)];
137 if (absDir
[0] != 0 && absDir
[1] != 0 && absDir
[0] != absDir
[1])
139 divisor
= Math
.min(absDir
[0], absDir
[1]);
141 // Standard slider (or maybe a pawn or king: same)
142 divisor
= Math
.max(absDir
[0], absDir
[1]);
143 return [dx
/ divisor
, dy
/ divisor
];
146 // There is something on x2,y2, maybe our color, pushed/pulled
147 static IsAprioriValidMove([x1
, y1
], [x2
, y2
]) {
148 const color1
= this.getColor(x1
, y1
);
149 const color2
= this.getColor(x2
, y2
);
150 const pawnShift
= (color1
== 'w' ? -1 : 1);
151 const pawnStartRank
= (color1
== 'w' ? 6 : 1);
152 const deltaX
= Math
.abs(x1
- x2
);
153 const deltaY
= Math
.abs(y1
- y2
);
154 switch (this.getPiece(x1
, y1
)) {
161 x1
+ pawnShift
== x2
||
162 x1
== pawnStartRank
&& x1
+ 2 * pawnShift
== x2
173 return (x1
== x2
|| y1
== y2
);
175 return (deltaX
+ deltaY
== 3 && (deltaX
== 1 || deltaY
== 1));
178 return (deltaX
== deltaY
);
181 (deltaX
== 0 || deltaY
== 0 || deltaX
== deltaY
)
184 return (deltaX
<= 1 && deltaY
<= 1);
189 // NOTE: for pushes, play the pushed piece first.
190 // for pulls: play the piece doing the action first
191 // NOTE: to push a piece out of the board, make it slide until its king
192 getPotentialMovesFrom([x
, y
]) {
193 const color
= this.turn
;
194 if (this.subTurn
== 1) {
195 const getMoveHash
= (m
) => {
196 return V
.CoordsToSquare(m
.start
) + V
.CoordsToSquare(m
.end
);
198 const addMoves
= (dir
, nbSteps
) => {
200 this.getMovesInDirection([x
, y
], [-dir
[0], -dir
[1]], nbSteps
)
201 .filter(m
=> !movesHash
[getMoveHash(m
)]);
202 newMoves
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
203 Array
.prototype.push
.apply(moves
, newMoves
);
205 // Free to play any move:
206 const moves
= super.getPotentialMovesFrom([x
, y
])
207 const pawnShift
= (color
== 'w' ? -1 : 1);
208 const pawnStartRank
= (color
== 'w' ? 6 : 1);
209 // Structure to avoid adding moves twice (can be action & move)
211 moves
.forEach(m
=> { movesHash
[getMoveHash(m
)] = true; });
212 // [x, y] is pushed by 'color'
213 for (let step
of V
.steps
[V
.KNIGHT
]) {
214 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
217 this.board
[i
][j
] != V
.EMPTY
&&
218 this.getColor(i
, j
) == color
&&
219 this.getPiece(i
, j
) == V
.KNIGHT
224 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
225 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
226 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
232 this.board
[i
][j
] != V
.EMPTY
&&
233 this.getColor(i
, j
) == color
235 const deltaX
= Math
.abs(i
- x
);
236 const deltaY
= Math
.abs(j
- y
);
237 // Can a priori go both ways, except with pawns
238 switch (this.getPiece(i
, j
)) {
240 if (deltaX
<= 2 && deltaY
<= 1) {
241 const pColor
= this.getColor(x
, y
);
242 if (pColor
== color
&& deltaY
== 0) {
244 const maxSteps
= (i
== pawnStartRank
&& deltaX
== 1 ? 2 : 1);
245 addMoves(step
, maxSteps
);
247 else if (pColor
!= color
&& deltaY
== 1 && deltaX
== 1)
253 if (deltaX
== 0 || deltaY
== 0) addMoves(step
);
256 if (deltaX
== deltaY
) addMoves(step
);
259 if (deltaX
== 0 || deltaY
== 0 || deltaX
== deltaY
)
263 if (deltaX
<= 1 && deltaY
<= 1) addMoves(step
, 1);
270 // If subTurn == 2 then we should have a first move,
271 // which restrict what we can play now: only in the first move direction
272 // NOTE: no need for knight or pawn checks, because the move will be
273 // naturally limited in those cases.
274 const L
= this.firstMove
.length
;
275 const fm
= this.firstMove
[L
-1];
276 if (fm
.appear
.length
== 2 && fm
.vanish
.length
== 2)
277 // Castle: no real move playable then.
279 if (fm
.appear
.length
== 0) {
280 // Piece at subTurn 1 just exited the board.
281 // Can I be a piece which caused the exit?
283 const moveOk
= V
.IsAprioriValidMove([x
, y
], [fm
.start
.x
, fm
.start
.y
]);
287 const dir
= this.getNormalizedDirection(
288 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
289 return this.getMovesInDirection([x
, y
], dir
);
293 const dirM
= this.getNormalizedDirection(
294 [fm
.end
.x
- fm
.start
.x
, fm
.end
.y
- fm
.start
.y
]);
295 const dir
= this.getNormalizedDirection(
296 [fm
.start
.x
- x
, fm
.start
.y
- y
]);
297 // Normalized directions should match:
298 if (dir
[0] == dirM
[0] && dir
[1] == dirM
[1])
299 return this.getMovesInDirection([x
, y
], dir
);
304 // Does m2 un-do m1 ? (to disallow undoing actions)
305 oppositeMoves(m1
, m2
) {
306 const isEqual
= (av1
, av2
) => {
307 // Precondition: av1 and av2 length = 2
308 for (let av
of av1
) {
309 const avInAv2
= av2
.find(elt
=> {
317 if (!avInAv2
) return false;
322 m1
.appear
.length
== 2 &&
323 m2
.appear
.length
== 2 &&
324 m1
.vanish
.length
== 2 &&
325 m2
.vanish
.length
== 2 &&
326 isEqual(m1
.appear
, m2
.vanish
) &&
327 isEqual(m1
.vanish
, m2
.appear
)
331 getAmove(move1
, move2
) {
332 // Just merge (one is action one is move, one may be empty)
334 appear: move1
.appear
.concat(move2
.appear
),
335 vanish: move1
.vanish
.concat(move2
.vanish
)
340 const color
= this.turn
;
341 if (this.subTurn
== 1) {
342 return moves
.filter(m
=> {
343 // A move is valid either if it doesn't result in a check,
344 // or if a second move is possible to counter the check
345 // (not undoing a potential move + action of the opponent)
347 let res
= this.underCheck(color
);
349 const moves2
= this.getAllPotentialMoves();
352 const res2
= this.underCheck(color
);
364 const Lf
= this.firstMove
.length
;
365 const La
= this.amoves
.length
;
366 if (La
== 0) return super.filterValid(moves
);
370 // Move shouldn't undo another:
371 const amove
= this.getAmove(this.firstMove
[Lf
-1], m
);
372 return !this.oppositeMoves(this.amoves
[La
-1], amove
);
378 isAttackedBySlideNJump([x
, y
], color
, piece
, steps
, oneStep
) {
379 for (let step
of steps
) {
380 let rx
= x
+ step
[0],
382 while (V
.OnBoard(rx
, ry
) && this.board
[rx
][ry
] == V
.EMPTY
&& !oneStep
) {
388 this.getPiece(rx
, ry
) == piece
&&
389 this.getColor(rx
, ry
) == color
391 // Now step in the other direction: if end of the world, then attacked
396 this.board
[rx
][ry
] == V
.EMPTY
&&
402 if (!V
.OnBoard(rx
, ry
)) return true;
408 isAttackedByPawn([x
, y
], color
) {
409 const lastRank
= (color
== 'w' ? 0 : 7);
411 // The king can be pushed out by a pawn only on last rank
413 const pawnShift
= (color
== "w" ? 1 : -1);
414 for (let i
of [-1, 1]) {
418 this.getPiece(x
+ pawnShift
, y
+ i
) == V
.PAWN
&&
419 this.getColor(x
+ pawnShift
, y
+ i
) == color
428 if (this.subTurn
== 2)
431 return super.getCurrentScore();
435 // If subTurn == 2 && square is empty && !underCheck,
436 // then return an empty move, allowing to "pass" subTurn2
439 this.board
[square
[0]][square
[1]] == V
.EMPTY
&&
440 !this.underCheck(this.turn
)
443 start: { x: -1, y: -1 },
444 end: { x: -1, y: -1 },
453 move.flags
= JSON
.stringify(this.aggregateFlags());
454 V
.PlayOnBoard(this.board
, move);
455 if (this.subTurn
== 2) {
456 const L
= this.firstMove
.length
;
457 this.amoves
.push(this.getAmove(this.firstMove
[L
-1], move));
458 this.turn
= V
.GetOppCol(this.turn
);
461 else this.firstMove
.push(move);
462 this.subTurn
= 3 - this.subTurn
;
467 if (move.start
.x
< 0) return;
468 for (let a
of move.appear
)
469 if (a
.p
== V
.KING
) this.kingPos
[a
.c
] = [a
.x
, a
.y
];
470 this.updateCastleFlags(move);
473 updateCastleFlags(move) {
474 const firstRank
= { 'w': V
.size
.x
- 1, 'b': 0 };
475 for (let v
of move.vanish
) {
476 if (v
.p
== V
.KING
) this.castleFlags
[v
.c
] = [V
.size
.y
, V
.size
.y
];
477 else if (v
.x
== firstRank
[v
.c
] && this.castleFlags
[v
.c
].includes(v
.y
)) {
478 const flagIdx
= (v
.y
== this.castleFlags
[v
.c
][0] ? 0 : 1);
479 this.castleFlags
[v
.c
][flagIdx
] = V
.size
.y
;
485 this.disaggregateFlags(JSON
.parse(move.flags
));
486 V
.UndoOnBoard(this.board
, move);
487 if (this.subTurn
== 1) {
488 this.turn
= V
.GetOppCol(this.turn
);
491 else this.firstMove
.pop();
492 this.subTurn
= 3 - this.subTurn
;
497 // (Potentially) Reset king position
498 for (let v
of move.vanish
)
499 if (v
.p
== V
.KING
) this.kingPos
[v
.c
] = [v
.x
, v
.y
];
503 if (move.start
.x
< 0)
504 // A second move is always required, but may be empty
506 const initialSquare
= V
.CoordsToSquare(move.start
);
507 const finalSquare
= V
.CoordsToSquare(move.end
);
508 if (move.appear
.length
== 0)
509 // Pushed or pulled out of the board
510 return initialSquare
+ "R";
511 return move.appear
[0].p
.toUpperCase() + initialSquare
+ finalSquare
;