1 // TODO: debug, and forbid self-capture of king.
3 import { ChessRules
} from "@/base_rules";
4 import { randInt
} from "@/utils/alea";
6 export class SynchroneRules
extends ChessRules
{
7 static get CanAnalyze() {
11 static get ShowMoves() {
15 static IsGoodFen(fen
) {
16 if (!ChessRules
.IsGoodFen(fen
)) return false;
17 const fenParsed
= V
.ParseFen(fen
);
21 fenParsed
.turn
== "w" &&
22 // NOTE: do not check really JSON stringified move...
23 (!fenParsed
.whiteMove
|| fenParsed
.whiteMove
== "-")
26 (fenParsed
.turn
== "b" && fenParsed
.whiteMove
!= "-")
33 static IsGoodEnpassant(enpassant
) {
34 const epArray
= enpassant
.split(",");
35 if (![2, 3].includes(epArray
.length
)) return false;
36 epArray
.forEach(epsq
=> {
38 const ep
= V
.SquareToCoords(epsq
);
39 if (isNaN(ep
.x
) || !V
.OnBoard(ep
)) return false;
45 static ParseFen(fen
) {
46 const fenParts
= fen
.split(" ");
48 ChessRules
.ParseFen(fen
),
49 { whiteMove: fenParts
[5] }
53 static GenRandInitFen(randomness
) {
54 return ChessRules
.GenRandInitFen(randomness
).slice(0, -1) + "-,- -";
58 return super.getFen() + " " + this.getWhitemoveFen();
62 return super.getFenForRepeat() + "_" + this.getWhitemoveFen();
65 setOtherVariables(fen
) {
66 const parsedFen
= V
.ParseFen(fen
);
67 this.setFlags(parsedFen
.flags
);
68 const epArray
= parsedFen
.enpassant
.split(",");
70 epArray
.forEach(epsq
=> this.epSquares
.push(this.getEpSquare(epsq
)));
72 // Also init whiteMove
74 parsedFen
.whiteMove
!= "-"
75 ? JSON
.parse(parsedFen
.whiteMove
)
79 // After undo(): no need to re-set INIT_COL_KING
81 this.kingPos
= { w: [-1, -1], b: [-1, -1] };
82 for (let i
= 0; i
< V
.size
.x
; i
++) {
83 for (let j
= 0; j
< V
.size
.y
; j
++) {
84 if (this.getPiece(i
, j
) == V
.KING
)
85 this.kingPos
[this.getColor(i
, j
)] = [i
, j
];
91 const L
= this.epSquares
.length
;
93 const start
= L
- 2 - (this.turn
== 'b' ? 1 : 0);
94 for (let i
=start
; i
< L
; i
++) {
95 if (!this.epSquares
[i
]) res
+= "-,";
96 else res
+= V
.CoordsToSquare(this.epSquares
[i
]) + ",";
98 return res
.slice(0, -1);
102 if (!this.whiteMove
) return "-";
103 return JSON
.stringify({
104 start: this.whiteMove
.start
,
105 end: this.whiteMove
.end
,
106 appear: this.whiteMove
.appear
,
107 vanish: this.whiteMove
.vanish
111 // NOTE: lazy unefficient implementation (for now. TODO?)
112 getPossibleMovesFrom([x
, y
]) {
113 const moves
= this.getAllValidMoves();
114 return moves
.filter(m
=> {
115 return m
.start
.x
== x
&& m
.start
.y
== y
;
120 const color
= this.turn
;
121 const sliderAttack
= (xx
, yy
, allowedSteps
) => {
122 const deltaX
= xx
- x
,
123 absDeltaX
= Math
.abs(deltaX
);
124 const deltaY
= yy
- y
,
125 absDeltaY
= Math
.abs(deltaY
);
126 const step
= [ deltaX
/ absDeltaX
|| 0, deltaY
/ absDeltaY
|| 0 ];
128 // Check that the step is a priori valid:
129 (absDeltaX
!= absDeltaY
&& deltaX
!= 0 && deltaY
!= 0) ||
130 allowedSteps
.every(st
=> st
[0] != step
[0] || st
[1] != step
[1])
134 let sq
= [ x
+ step
[0], y
+ step
[1] ];
135 while (sq
[0] != xx
|| sq
[1] != yy
) {
136 // NOTE: no need to check OnBoard in this special case
137 if (this.board
[sq
[0]][sq
[1]] != V
.EMPTY
) return null;
141 return this.getBasicMove([xx
, yy
], [x
, y
]);
143 // Can I take on the square [x, y] ?
144 // If yes, return the (list of) capturing move(s)
146 for (let i
=0; i
<8; i
++) {
147 for (let j
=0; j
<8; j
++) {
148 if (this.getColor(i
, j
) == color
) {
149 switch (this.getPiece(i
, j
)) {
151 // Pushed pawns move as enemy pawns
152 const shift
= (color
== 'w' ? 1 : -1);
153 if (x
+ shift
== i
&& Math
.abs(y
- j
) == 1)
154 moves
.push(this.getBasicMove([i
, j
], [x
, y
]));
158 const deltaX
= Math
.abs(i
- x
);
159 const deltaY
= Math
.abs(j
- y
);
161 deltaX
+ deltaY
== 3 &&
162 [1, 2].includes(deltaX
) &&
163 [1, 2].includes(deltaY
)
165 moves
.push(this.getBasicMove([i
, j
], [x
, y
]));
170 if (Math
.abs(i
- x
) <= 1 && Math
.abs(j
- y
) <= 1)
171 moves
.push(this.getBasicMove([i
, j
], [x
, y
]));
174 const mv
= sliderAttack(i
, j
, V
.steps
[V
.ROOK
]);
175 if (!!mv
) moves
.push(mv
);
179 const mv
= sliderAttack(i
, j
, V
.steps
[V
.BISHOP
]);
180 if (!!mv
) moves
.push(mv
);
184 const mv
= sliderAttack(
185 i
, j
, V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]));
186 if (!!mv
) moves
.push(mv
);
193 return this.filterValid(moves
);
197 const color
= this.turn
;
198 // 0) Generate our possible moves
199 let myMoves
= super.getAllValidMoves();
200 // Lookup table to quickly decide if a move is already in list:
202 const getMoveHash
= (move) => {
204 "m" + move.start
.x
+ move.start
.y
+
205 move.end
.x
+ move.end
.y
+
206 // Also use m.appear[0].p for pawn promotions
210 myMoves
.forEach(m
=> moveSet
[getMoveHash(m
)] = true);
211 // 1) Generate all opponent's moves
212 this.turn
= V
.GetOppCol(color
);
213 const oppMoves
= super.getAllValidMoves();
215 // 2) Play each opponent's move, and see if captures are possible:
216 // --> capturing moving unit only (otherwise some issues)
217 oppMoves
.forEach(m
=> {
218 V
.PlayOnBoard(this.board
, m
);
219 // Can I take on [m.end.x, m.end.y] ?
220 // If yes and not already in list, add it (without the capturing part)
221 let capturingMoves
= this.getCaptures(m
.end
.x
, m
.end
.y
);
222 capturingMoves
.forEach(cm
=> {
223 const cmHash
= getMoveHash(cm
);
224 if (!moveSet
[cmHash
]) {
225 // The captured unit hasn't moved yet, so temporarily cancel capture
227 // If m is itself a capturing move: then replace by self-capture
228 if (m
.vanish
.length
== 2) cm
.vanish
.push(m
.vanish
[1]);
230 moveSet
[cmHash
] = true;
233 V
.UndoOnBoard(this.board
, m
);
239 if (moves
.length
== 0) return [];
240 // filterValid can be called when it's "not our turn":
241 const color
= moves
[0].vanish
[0].c
;
242 return moves
.filter(m
=> {
243 const piece
= m
.vanish
[0].p
;
244 if (piece
== V
.KING
) {
245 this.kingPos
[color
][0] = m
.appear
[0].x
;
246 this.kingPos
[color
][1] = m
.appear
[0].y
;
248 V
.PlayOnBoard(this.board
, m
);
249 let res
= !this.underCheck(color
);
250 V
.UndoOnBoard(this.board
, m
);
251 if (piece
== V
.KING
) this.kingPos
[color
] = [m
.start
.x
, m
.start
.y
];
256 atLeastOneMove(color
) {
257 const curTurn
= this.turn
;
259 const res
= super.atLeastOneMove();
264 // White and black (partial) moves were played: merge
265 resolveSynchroneMove(move) {
266 const m1
= this.whiteMove
;
268 // For PlayOnBoard (no need for start / end, irrelevant)
276 if ((m1
.end
.x
!= m2
.end
.x
) || (m1
.end
.y
!= m2
.end
.y
)) {
277 // Easy case: two independant moves (which may (self-)capture)
278 smove
.appear
.push(m1
.appear
[0]);
279 smove
.appear
.push(m2
.appear
[0]);
280 // "Captured" pieces may have moved:
282 m1
.vanish
.length
== 2 &&
284 m2
.end
.x
!= m1
.vanish
[1].x
||
285 m2
.end
.y
!= m1
.vanish
[1].y
288 smove
.vanish
.push(m1
.vanish
[1]);
291 m2
.vanish
.length
== 2 &&
293 m1
.end
.x
!= m2
.vanish
[1].x
||
294 m1
.end
.y
!= m2
.vanish
[1].y
297 smove
.vanish
.push(m2
.vanish
[1]);
301 if (m1
.vanish
.length
== 1 && m2
.vanish
.length
== 1) {
302 // Easy case: both disappear except if one is a king
303 const p1
= m1
.vanish
[0].p
;
304 const p2
= m2
.vanish
[0].p
;
305 if ([p1
, p2
].includes(V
.KING
)) {
310 c: (p1
== V
.KING
? 'w' : 'b')
314 // One move is a self-capture and the other a normal capture:
315 // only the self-capture appears
318 const selfCaptureMove
=
319 m1
.vanish
[1].c
== m1
.vanish
[0].c
325 p: selfCaptureMove
.appear
[0].p
,
326 c: selfCaptureMove
.vanish
[0].c
334 move.flags
= JSON
.stringify(this.aggregateFlags()); //save flags (for undo)
335 this.epSquares
.push(this.getEpSquare(move));
336 // Do not play on board (would reveal the move...)
337 this.turn
= V
.GetOppCol(this.turn
);
342 updateCastleFlags(move) {
343 const firstRank
= { 'w': V
.size
.x
- 1, 'b': 0 };
344 move.appear
.concat(move.vanish
).forEach(av
=> {
345 for (let c
of ['w', 'b']) {
346 if (av
.x
== firstRank
[c
] && this.castleFlags
[c
].includes(av
.y
)) {
347 const flagIdx
= (av
.y
== this.castleFlags
[c
][0] ? 0 : 1);
348 this.castleFlags
[c
][flagIdx
] = 8;
355 if (this.turn
== 'b') {
356 // NOTE: whiteMove is used read-only, so no need to copy
357 this.whiteMove
= move;
361 // A full turn just ended:
362 const smove
= this.resolveSynchroneMove(move);
363 V
.PlayOnBoard(this.board
, smove
);
364 this.whiteMove
= null;
366 // Update king position + flags
367 let kingAppear
= { 'w': false, 'b': false };
368 for (let i
=0; i
<smove
.appear
.length
; i
++) {
369 if (smove
.appear
[i
].p
== V
.KING
) {
370 const c
= smove
.appear
[i
].c
;
371 kingAppear
[c
] = true;
372 this.kingPos
[c
][0] = smove
.appear
[i
].x
;
373 this.kingPos
[c
][1] = smove
.appear
[i
].y
;
376 for (let i
=0; i
<smove
.vanish
.length
; i
++) {
377 if (smove
.vanish
[i
].p
== V
.KING
) {
378 const c
= smove
.vanish
[i
].c
;
379 if (!kingAppear
[c
]) {
380 this.kingPos
[c
][0] = -1;
381 this.kingPos
[c
][1] = -1;
386 this.updateCastleFlags(smove
);
391 this.epSquares
.pop();
392 this.disaggregateFlags(JSON
.parse(move.flags
));
393 if (this.turn
== 'w')
394 // Back to the middle of the move
395 V
.UndoOnBoard(this.board
, move.smove
);
396 this.turn
= V
.GetOppCol(this.turn
);
402 if (this.turn
== 'w')
403 // Reset king positions: scan board
407 getCheckSquares(color
) {
408 if (color
== 'b') return [];
410 if (this.underCheck('w'))
411 res
.push(JSON
.parse(JSON
.stringify(this.kingPos
['w'])));
412 if (this.underCheck('b'))
413 res
.push(JSON
.parse(JSON
.stringify(this.kingPos
['b'])));
418 if (this.turn
== 'b')
419 // Turn (white + black) not over yet
421 // Was a king captured?
422 if (this.kingPos
['w'][0] < 0) return "0-1";
423 if (this.kingPos
['b'][0] < 0) return "1-0";
424 const whiteCanMove
= this.atLeastOneMove('w');
425 const blackCanMove
= this.atLeastOneMove('b');
426 if (whiteCanMove
&& blackCanMove
) return "*";
428 const whiteInCheck
= this.underCheck('w');
429 const blackInCheck
= this.underCheck('b');
431 (whiteCanMove
&& !this.underCheck('b')) ||
432 (blackCanMove
&& !this.underCheck('w'))
436 // Checkmate: could be mutual
437 if (!whiteCanMove
&& !blackCanMove
) return "1/2";
438 return (whiteCanMove
? "1-0" : "0-1");
442 const maxeval
= V
.INFINITY
;
443 const color
= this.turn
;
444 let moves
= this.getAllValidMoves();
445 if (moves
.length
== 0)
446 // TODO: this situation should not happen
449 if (Math
.random() < 0.5)
450 // Return a random move
451 return moves
[randInt(moves
.length
)];
453 // Rank moves at depth 1:
454 // try to capture something (not re-capturing)
456 V
.PlayOnBoard(this.board
, m
);
457 m
.eval
= this.evalPosition();
458 V
.UndoOnBoard(this.board
, m
);
460 moves
.sort((a
, b
) => {
461 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
463 let candidates
= [0];
464 for (let i
= 1; i
< moves
.length
&& moves
[i
].eval
== moves
[0].eval
; i
++)
466 return moves
[candidates
[randInt(candidates
.length
)]];