1 import { ChessRules
} from "@/base_rules";
2 import { randInt
} from "@/utils/alea";
4 export class ApocalypseRules
extends ChessRules
{
6 static get PawnSpecs() {
12 promotions: [V
.KNIGHT
]
17 static get SomeHiddenMoves() {
21 static get HasCastle() {
25 static get HasEnpassant() {
29 static get CanAnalyze() {
33 static get ShowMoves() {
38 // Show the piece taken, if any, and not multiple pawns:
39 if (m
.vanish
.length
== 1) return "Apocalypse/empty";
40 return m
.vanish
[1].c
+ m
.vanish
[1].p
;
44 return [V
.PAWN
, V
.KNIGHT
];
47 static IsGoodPosition(position
) {
48 if (position
.length
== 0) return false;
49 const rows
= position
.split("/");
50 if (rows
.length
!= V
.size
.x
) return false;
51 // At least one pawn per color
52 let pawns
= { "p": 0, "P": 0 };
53 for (let row
of rows
) {
55 for (let i
= 0; i
< row
.length
; i
++) {
56 if (['P','p'].includes(row
[i
])) pawns
[row
[i
]]++;
57 if (V
.PIECES
.includes(row
[i
].toLowerCase())) sumElts
++;
59 const num
= parseInt(row
[i
], 10);
60 if (isNaN(num
)) return false;
64 if (sumElts
!= V
.size
.y
) return false;
66 if (Object
.values(pawns
).some(v
=> v
== 0))
71 static IsGoodFen(fen
) {
72 if (!ChessRules
.IsGoodFen(fen
)) return false;
73 const fenParsed
= V
.ParseFen(fen
);
77 fenParsed
.turn
== "b" &&
78 // NOTE: do not check really JSON stringified move...
79 (!fenParsed
.whiteMove
|| fenParsed
.whiteMove
== "-")
82 (fenParsed
.turn
== "w" && fenParsed
.whiteMove
!= "-")
89 static IsGoodFlags(flags
) {
90 return !!flags
.match(/^[0-2]{2,2}$/);
94 return this.penaltyFlags
;
97 disaggregateFlags(flags
) {
98 this.penaltyFlags
= flags
;
101 static ParseFen(fen
) {
102 const fenParts
= fen
.split(" ");
103 return Object
.assign(
104 ChessRules
.ParseFen(fen
),
105 { whiteMove: fenParts
[4] }
110 return { x: 5, y: 5 };
113 static GenRandInitFen() {
114 return "npppn/p3p/5/P3P/NPPPN w 0 00 -";
118 return super.getFen() + " " + this.getWhitemoveFen();
122 return super.getFenForRepeat() + "_" + this.getWhitemoveFen();
127 this.penaltyFlags
['w'].toString() + this.penaltyFlags
['b'].toString()
131 setOtherVariables(fen
) {
132 const parsedFen
= V
.ParseFen(fen
);
133 this.setFlags(parsedFen
.flags
);
134 // Also init whiteMove
136 parsedFen
.whiteMove
!= "-"
137 ? JSON
.parse(parsedFen
.whiteMove
)
142 this.penaltyFlags
= {
143 'w': parseInt(fenflags
[0], 10),
144 'b': parseInt(fenflags
[1], 10)
149 if (!this.whiteMove
) return "-";
150 return JSON
.stringify({
151 start: this.whiteMove
.start
,
152 end: this.whiteMove
.end
,
153 appear: this.whiteMove
.appear
,
154 vanish: this.whiteMove
.vanish
158 getSpeculations(moves
, sq
) {
161 const mHash
= "m" + m
.start
.x
+ m
.start
.y
+ m
.end
.x
+ m
.end
.y
;
162 moveSet
[mHash
] = true;
164 const color
= this.turn
;
165 this.turn
= V
.GetOppCol(color
);
166 const oppMoves
= super.getAllValidMoves();
168 // For each opponent's move, generate valid moves [from sq if same color]
169 let speculations
= [];
170 oppMoves
.forEach(m
=> {
171 V
.PlayOnBoard(this.board
, m
);
172 const newValidMoves
=
175 this.getColor(sq
[0], sq
[1]) == color
176 ? super.getPotentialMovesFrom(sq
)
179 : super.getAllValidMoves();
180 newValidMoves
.forEach(vm
=> {
181 const mHash
= "m" + vm
.start
.x
+ vm
.start
.y
+ vm
.end
.x
+ vm
.end
.y
;
182 if (!moveSet
[mHash
]) {
183 moveSet
[mHash
] = true;
184 vm
.end
.illegal
= true; //potentially illegal!
185 speculations
.push(vm
);
188 V
.UndoOnBoard(this.board
, m
);
193 getPossibleMovesFrom([x
, y
]) {
194 const possibleMoves
= super.getPotentialMovesFrom([x
, y
])
195 // Augment potential moves with opponent's moves speculation:
196 return possibleMoves
.concat(this.getSpeculations(possibleMoves
, [x
, y
]));
200 // Return possible moves + potentially valid moves
201 const validMoves
= super.getAllValidMoves();
202 return validMoves
.concat(this.getSpeculations(validMoves
));
205 addPawnMoves([x1
, y1
], [x2
, y2
], moves
) {
206 let finalPieces
= [V
.PAWN
];
207 const color
= this.turn
;
208 const lastRank
= (color
== "w" ? 0 : V
.size
.x
- 1);
209 if (x2
== lastRank
) {
210 // If 0 or 1 horsemen, promote in knight
211 let knightCounter
= 0;
212 let emptySquares
= [];
213 for (let i
= 0; i
< V
.size
.x
; i
++) {
214 for (let j
= 0; j
< V
.size
.y
; j
++) {
215 if (this.board
[i
][j
] == V
.EMPTY
) emptySquares
.push([i
, j
]);
217 this.getColor(i
, j
) == color
&&
218 this.getPiece(i
, j
) == V
.KNIGHT
224 if (knightCounter
<= 1) finalPieces
= [V
.KNIGHT
];
226 // Generate all possible landings, maybe capturing something on the way
227 let capture
= undefined;
228 if (this.board
[x2
][y2
] != V
.EMPTY
) {
229 capture
= JSON
.parse(JSON
.stringify({
232 c: this.getColor(x2
, y2
),
233 p: this.getPiece(x2
, y2
)
236 emptySquares
.forEach(sq
=> {
237 if (sq
[0] != lastRank
) {
238 let newMove
= this.getBasicMove([x1
, y1
], [sq
[0], sq
[1]]);
239 if (!!capture
) newMove
.vanish
.push(capture
);
247 for (let piece
of finalPieces
) {
248 tr
= (piece
!= V
.PAWN
? { c: color
, p: piece
} : null);
249 moves
.push(this.getBasicMove([x1
, y1
], [x2
, y2
], tr
));
258 atLeastOneMove(color
) {
259 const curTurn
= this.turn
;
261 const res
= super.atLeastOneMove();
266 // White and black (partial) moves were played: merge
267 resolveSynchroneMove(move) {
268 let m1
= this.whiteMove
;
270 const movingLikeCapture
= (m
) => {
271 const shift
= (m
.vanish
[0].c
== 'w' ? -1 : 1);
273 m
.start
.x
+ shift
== m
.end
.x
&&
274 Math
.abs(m
.end
.y
- m
.start
.y
) == 1
277 const isPossible
= (m
, other
) => {
280 m
.vanish
[0].p
== V
.KNIGHT
&&
282 m
.vanish
.length
== 1 ||
283 m
.vanish
[1].c
!= m
.vanish
[0].c
||
284 // Self-capture attempt
286 !other
.end
.illegal
&&
287 other
.end
.x
== m
.end
.x
&&
288 other
.end
.y
== m
.end
.y
294 m
.vanish
[0].p
== V
.PAWN
&&
295 !other
.end
.illegal
&&
299 m
.end
.x
== (m
.vanish
[0].c
== "w" ? 0 : V
.size
.x
- 1) &&
300 other
.vanish
.length
== 2 &&
301 other
.vanish
[1].p
== V
.KNIGHT
&&
302 other
.vanish
[1].c
== m
.vanish
[0].c
307 !movingLikeCapture(m
) &&
308 other
.start
.x
== m
.end
.x
&&
309 other
.start
.y
== m
.end
.y
314 movingLikeCapture(m
) &&
315 other
.end
.x
== m
.end
.x
&&
316 other
.end
.y
== m
.end
.y
322 if (!!m1
.end
.illegal
&& !isPossible(m1
, m2
)) {
323 // Either an anticipated capture of something which didn't move
324 // (or not to the right square), or a push through blocus.
325 // ==> Just discard the move, and add a penalty point
326 this.penaltyFlags
[m1
.vanish
[0].c
]++;
329 if (!!m2
.end
.illegal
&& !isPossible(m2
, m1
)) {
330 this.penaltyFlags
[m2
.vanish
[0].c
]++;
333 if (!!m1
.isNull
) m1
= null;
334 if (!!m2
.isNull
) m2
= null;
335 // If one move is illegal, just execute the other
336 if (!m1
&& !!m2
) return m2
;
337 if (!m2
&& !!m1
) return m1
;
338 // For PlayOnBoard (no need for start / end, irrelevant)
343 if (!m1
&& !m2
) return smove
;
344 // Both moves are now legal or at least possible:
345 smove
.vanish
.push(m1
.vanish
[0]);
346 smove
.vanish
.push(m2
.vanish
[0]);
347 if ((m1
.end
.x
!= m2
.end
.x
) || (m1
.end
.y
!= m2
.end
.y
)) {
348 // Easy case: two independant moves
349 smove
.appear
.push(m1
.appear
[0]);
350 smove
.appear
.push(m2
.appear
[0]);
351 // "Captured" pieces may have moved:
353 m1
.vanish
.length
== 2 &&
355 m1
.vanish
[1].x
!= m2
.start
.x
||
356 m1
.vanish
[1].y
!= m2
.start
.y
359 smove
.vanish
.push(m1
.vanish
[1]);
362 m2
.vanish
.length
== 2 &&
364 m2
.vanish
[1].x
!= m1
.start
.x
||
365 m2
.vanish
[1].y
!= m1
.start
.y
368 smove
.vanish
.push(m2
.vanish
[1]);
371 // Collision: priority to the anticipated capture, if any.
372 // If ex-aequo: knight wins (higher risk), or both disappears.
373 // Then, priority to the knight vs pawn: remains.
374 // Finally: both disappears.
376 const p1
= m1
.vanish
[0].p
;
377 const p2
= m2
.vanish
[0].p
;
378 if (!!m1
.end
.illegal
&& !m2
.end
.illegal
) remain
= { c: 'w', p: p1
};
379 else if (!!m2
.end
.illegal
&& !m1
.end
.illegal
) remain
= { c: 'b', p: p2
};
381 // Either both are illegal or both are legal
382 if (p1
== V
.KNIGHT
&& p2
== V
.PAWN
) remain
= { c: 'w', p: p1
};
383 else if (p2
== V
.KNIGHT
&& p1
== V
.PAWN
) remain
= { c: 'b', p: p2
};
384 // If remain is still null: same type same risk, both disappear
399 // Do not play on board (would reveal the move...)
400 move.flags
= JSON
.stringify(this.aggregateFlags());
401 this.turn
= V
.GetOppCol(this.turn
);
407 if (this.turn
== 'b') {
408 // NOTE: whiteMove is used read-only, so no need to copy
409 this.whiteMove
= move;
412 // A full turn just ended:
413 const smove
= this.resolveSynchroneMove(move);
414 V
.PlayOnBoard(this.board
, smove
);
415 move.whiteMove
= this.whiteMove
; //for undo
416 this.whiteMove
= null;
421 this.disaggregateFlags(JSON
.parse(move.flags
));
422 if (this.turn
== 'w')
423 // Back to the middle of the move
424 V
.UndoOnBoard(this.board
, move.smove
);
425 this.turn
= V
.GetOppCol(this.turn
);
431 if (this.turn
== 'w') this.whiteMove
= null;
432 else this.whiteMove
= move.whiteMove
;
440 if (this.turn
== 'b')
441 // Turn (white + black) not over yet
443 // Count footmen: if a side has none, it loses
444 let fmCount
= { 'w': 0, 'b': 0 };
445 for (let i
=0; i
<5; i
++) {
446 for (let j
=0; j
<5; j
++) {
447 if (this.board
[i
][j
] != V
.EMPTY
&& this.getPiece(i
, j
) == V
.PAWN
)
448 fmCount
[this.getColor(i
, j
)]++;
451 if (Object
.values(fmCount
).some(v
=> v
== 0)) {
452 if (fmCount
['w'] == 0 && fmCount
['b'] == 0)
455 if (fmCount
['w'] == 0) return "0-1";
456 return "1-0"; //fmCount['b'] == 0
458 // Check penaltyFlags: if a side has 2 or more, it loses
459 if (Object
.values(this.penaltyFlags
).every(v
=> v
== 2)) return "1/2";
460 if (this.penaltyFlags
['w'] == 2) return "0-1";
461 if (this.penaltyFlags
['b'] == 2) return "1-0";
462 if (!this.atLeastOneMove('w') || !this.atLeastOneMove('b'))
463 // Stalemate (should be very rare)
469 const maxeval
= V
.INFINITY
;
470 const color
= this.turn
;
471 let moves
= this.getAllValidMoves();
472 if (moves
.length
== 0)
473 // TODO: this situation should not happen
476 // Rank moves at depth 1:
478 let illegalMoves
= [];
480 // Warning: m might be illegal!
481 if (!m
.end
.illegal
) {
482 V
.PlayOnBoard(this.board
, m
);
483 m
.eval
= this.evalPosition();
484 V
.UndoOnBoard(this.board
, m
);
486 } else illegalMoves
.push(m
);
489 const illegalRatio
= illegalMoves
.length
/ moves
.length
;
490 if (Math
.random() < illegalRatio
)
491 // Return a random illegal move
492 return illegalMoves
[randInt(illegalMoves
.length
)];
494 validMoves
.sort((a
, b
) => {
495 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
497 let candidates
= [0];
500 i
< validMoves
.length
&& validMoves
[i
].eval
== moves
[0].eval
;
505 return validMoves
[candidates
[randInt(candidates
.length
)]];
509 // Basic system: piece + init + dest square
511 (move.vanish
[0].p
== V
.KNIGHT
? "N" : "") +
512 V
.CoordsToSquare(move.start
) +
513 V
.CoordsToSquare(move.end
)