Fix and complete Apocalypse variant
[vchess.git] / client / src / variants / Apocalypse.js
CommitLineData
1a3cfdc0
BA
1import { ChessRules } from "@/base_rules";
2import { randInt } from "@/utils/alea";
3
4export class ApocalypseRules extends ChessRules {
5 static get PawnSpecs() {
6 return Object.assign(
7 {},
8 ChessRules.PawnSpecs,
9 {
10 twoSquares: false,
11 promotions: [V.KNIGHT]
12 }
13 );
14 }
15
16 static get HasCastle() {
17 return false;
18 }
19
20 static get HasEnpassant() {
21 return false;
22 }
23
24 static get CanAnalyze() {
6ec2feb2 25 return true; //false;
1a3cfdc0
BA
26 }
27
28 static get ShowMoves() {
29 return "byrow";
30 }
31
6ec2feb2
BA
32 getPPpath(m) {
33 // Show the piece taken, if any, and not multiple pawns:
34 if (m.vanish.length == 1) return "Apocalypse/empty";
35 return m.vanish[1].c + m.vanish[1].p;
36 }
37
1a3cfdc0
BA
38 static get PIECES() {
39 return [V.PAWN, V.KNIGHT];
40 }
41
42 static IsGoodPosition(position) {
43 if (position.length == 0) return false;
44 const rows = position.split("/");
45 if (rows.length != V.size.x) return false;
46 // At least one pawn per color
47 let pawns = { "p": 0, "P": 0 };
48 for (let row of rows) {
49 let sumElts = 0;
50 for (let i = 0; i < row.length; i++) {
51 if (['P','p'].includes(row[i])) pawns[row[i]]++;
52 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
53 else {
54 const num = parseInt(row[i]);
55 if (isNaN(num)) return false;
56 sumElts += num;
57 }
58 }
59 if (sumElts != V.size.y) return false;
60 }
61 if (Object.values(pawns).some(v => v == 0))
62 return false;
63 return true;
64 }
65
66 static IsGoodFen(fen) {
67 if (!ChessRules.IsGoodFen(fen)) return false;
68 const fenParsed = V.ParseFen(fen);
69 // 4) Check whiteMove
70 if (
71 (
72 fenParsed.turn == "w" &&
73 // NOTE: do not check really JSON stringified move...
74 (!fenParsed.whiteMove || fenParsed.whiteMove == "-")
75 )
76 ||
77 (fenParsed.turn == "b" && fenParsed.whiteMove != "-")
78 ) {
79 return false;
80 }
81 return true;
82 }
83
84 static IsGoodFlags(flags) {
85 return !!flags.match(/^[0-2]{2,2}$/);
86 }
87
88 aggregateFlags() {
89 return this.penaltyFlags;
90 }
91
92 disaggregateFlags(flags) {
93 this.penaltyFlags = flags;
94 }
95
96 static ParseFen(fen) {
97 const fenParts = fen.split(" ");
98 return Object.assign(
99 ChessRules.ParseFen(fen),
100 { whiteMove: fenParts[4] }
101 );
102 }
103
104 static get size() {
105 return { x: 5, y: 5 };
106 }
107
108 static GenRandInitFen() {
109 return "npppn/p3p/5/P3P/NPPPN w 0 00 -";
110 }
111
112 getFen() {
113 return super.getFen() + " " + this.getWhitemoveFen();
114 }
115
116 getFenForRepeat() {
117 return super.getFenForRepeat() + "_" + this.getWhitemoveFen();
118 }
119
120 getFlagsFen() {
6ec2feb2
BA
121 return (
122 this.penaltyFlags['w'].toString() + this.penaltyFlags['b'].toString()
123 );
1a3cfdc0
BA
124 }
125
126 setOtherVariables(fen) {
127 const parsedFen = V.ParseFen(fen);
128 this.setFlags(parsedFen.flags);
129 // Also init whiteMove
130 this.whiteMove =
131 parsedFen.whiteMove != "-"
132 ? JSON.parse(parsedFen.whiteMove)
133 : null;
134 }
135
136 setFlags(fenflags) {
6ec2feb2
BA
137 this.penaltyFlags = {
138 'w': parseInt(fenflags[0]),
139 'b': parseInt(fenflags[1])
140 };
1a3cfdc0
BA
141 }
142
143 getWhitemoveFen() {
144 if (!this.whiteMove) return "-";
145 return JSON.stringify({
146 start: this.whiteMove.start,
147 end: this.whiteMove.end,
148 appear: this.whiteMove.appear,
e45c98ec
BA
149 vanish: this.whiteMove.vanish,
150 illegal: this.whiteMove.illegal
1a3cfdc0
BA
151 });
152 }
153
154 getSpeculations(moves, sq) {
155 let moveSet = {};
156 moves.forEach(m => {
157 const mHash = "m" + m.start.x + m.start.y + m.end.x + m.end.y;
158 moveSet[mHash] = true;
159 });
160 const color = this.turn;
161 this.turn = V.GetOppCol(color);
162 const oppMoves = super.getAllValidMoves();
163 this.turn = color;
6ec2feb2 164 // For each opponent's move, generate valid moves [from sq if same color]
1a3cfdc0
BA
165 let speculations = [];
166 oppMoves.forEach(m => {
167 V.PlayOnBoard(this.board, m);
168 const newValidMoves =
169 !!sq
6ec2feb2
BA
170 ? (
171 this.getColor(sq[0], sq[1]) == color
172 ? super.getPotentialMovesFrom(sq)
173 : []
174 )
1a3cfdc0
BA
175 : super.getAllValidMoves();
176 newValidMoves.forEach(vm => {
177 const mHash = "m" + vm.start.x + vm.start.y + vm.end.x + vm.end.y;
178 if (!moveSet[mHash]) {
179 moveSet[mHash] = true;
180 vm.illegal = true; //potentially illegal!
181 speculations.push(vm);
182 }
183 });
184 V.UndoOnBoard(this.board, m);
185 });
186 return speculations;
187 }
188
189 getPossibleMovesFrom([x, y]) {
190 const possibleMoves = super.getPotentialMovesFrom([x, y])
191 // Augment potential moves with opponent's moves speculation:
192 return possibleMoves.concat(this.getSpeculations(possibleMoves, [x, y]));
193 }
194
195 getAllValidMoves() {
196 // Return possible moves + potentially valid moves
197 const validMoves = super.getAllValidMoves();
198 return validMoves.concat(this.getSpeculations(validMoves));
199 }
200
201 addPawnMoves([x1, y1], [x2, y2], moves) {
202 let finalPieces = [V.PAWN];
203 const color = this.turn;
204 const lastRank = (color == "w" ? 0 : V.size.x - 1);
205 if (x2 == lastRank) {
206 // If 0 or 1 horsemen, promote in knight
207 let knightCounter = 0;
208 let emptySquares = [];
6ec2feb2
BA
209 for (let i = 0; i < V.size.x; i++) {
210 for (let j = 0; j < V.size.y; j++) {
1a3cfdc0
BA
211 if (this.board[i][j] == V.EMPTY) emptySquares.push([i, j]);
212 else if (
213 this.getColor(i, j) == color &&
214 this.getPiece(i, j) == V.KNIGHT
215 ) {
216 knightCounter++;
217 }
218 }
219 }
220 if (knightCounter <= 1) finalPieces = [V.KNIGHT];
221 else {
6ec2feb2
BA
222 // Generate all possible landings, maybe capturing something on the way
223 let capture = undefined;
224 if (this.board[x2][y2] != V.EMPTY) {
225 capture = JSON.parse(JSON.stringify({
226 x: x2,
227 y: y2,
228 c: this.getColor(x2, y2),
229 p: this.getPiece(x2, y2)
230 }));
231 }
1a3cfdc0 232 emptySquares.forEach(sq => {
6ec2feb2
BA
233 if (sq[0] != lastRank) {
234 let newMove = this.getBasicMove([x1, y1], [sq[0], sq[1]]);
235 if (!!capture) newMove.vanish.push(capture);
236 moves.push(newMove);
237 }
1a3cfdc0
BA
238 });
239 return;
240 }
241 }
242 let tr = null;
243 for (let piece of finalPieces) {
244 tr = (piece != V.PAWN ? { c: color, p: piece } : null);
245 moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
246 }
247 }
248
249 filterValid(moves) {
250 // No checks:
251 return moves;
252 }
253
254 atLeastOneMove(color) {
255 const curTurn = this.turn;
256 this.turn = color;
257 const res = super.atLeastOneMove();
258 this.turn = curTurn;
259 return res;
260 }
261
262 // White and black (partial) moves were played: merge
263 resolveSynchroneMove(move) {
6ec2feb2
BA
264 let m1 = this.whiteMove;
265 let m2 = move;
266 const movingLikeCapture = (m) => {
267 const shift = (m.vanish[0].c == 'w' ? -1 : 1);
268 return (
269 m.start.x + shift == m.end.x &&
270 Math.abs(m.end.y - m.start.y) == 1
271 );
272 };
273 const isPossible = (m, other) => {
274 return (
275 (
276 m.vanish[0].p == V.KNIGHT &&
277 (m.vanish.length == 1 || m.vanish[1].c != m.vanish[0].c)
278 )
279 ||
280 (
281 // Promotion attempt
282 m.end.x == (m.vanish[0].c == "w" ? 0 : V.size.x - 1) &&
283 other.vanish.length == 2 &&
284 other.vanish[1].p == V.KNIGHT &&
285 other.vanish[1].c == m.vanish[0].c
286 )
287 ||
288 (
289 // Moving attempt
290 !movingLikeCapture(m) &&
291 other.start.x == m.end.x &&
292 other.start.y == m.end.y
293 )
294 ||
295 (
296 // Capture attempt
297 movingLikeCapture(m) &&
298 other.end.x == m.end.x &&
299 other.end.y == m.end.y
300 )
301 );
302 };
303 if (!!m1.illegal && !isPossible(m1, m2)) {
304 // Either an anticipated capture of something which didn't move
305 // (or not to the right square), or a push through blocus.
306 // ==> Just discard the move, and add a penalty point
307 this.penaltyFlags[m1.vanish[0].c]++;
308 m1.isNull = true;
1a3cfdc0 309 }
6ec2feb2
BA
310 if (!!m2.illegal && !isPossible(m2, m1)) {
311 this.penaltyFlags[m2.vanish[0].c]++;
312 m2.isNull = true;
313 }
314 if (!!m1.isNull) m1 = null;
315 if (!!m2.isNull) m2 = null;
316 // If one move is illegal, just execute the other
317 if (!m1 && !!m2) return m2;
318 if (!m2 && !!m1) return m1;
1a3cfdc0
BA
319 // For PlayOnBoard (no need for start / end, irrelevant)
320 let smove = {
321 appear: [],
322 vanish: []
323 };
1a3cfdc0
BA
324 if (!m1 && !m2) return smove;
325 // Both move are now legal:
326 smove.vanish.push(m1.vanish[0]);
327 smove.vanish.push(m2.vanish[0]);
328 if ((m1.end.x != m2.end.x) || (m1.end.y != m2.end.y)) {
329 // Easy case: two independant moves
330 smove.appear.push(m1.appear[0]);
331 smove.appear.push(m2.appear[0]);
332 // "Captured" pieces may have moved:
333 if (
334 m1.vanish.length == 2 &&
335 (
336 m1.vanish[1].x != m2.start.x ||
337 m1.vanish[1].y != m2.start.y
338 )
339 ) {
340 smove.vanish.push(m1.vanish[1]);
341 }
342 if (
343 m2.vanish.length == 2 &&
344 (
345 m2.vanish[1].x != m1.start.x ||
346 m2.vanish[1].y != m1.start.y
347 )
348 ) {
349 smove.vanish.push(m2.vanish[1]);
350 }
351 } else {
6ec2feb2
BA
352 // Collision: priority to the anticipated capture, if any.
353 // If ex-aequo: knight wins (higher risk), or both disappears.
354 // Then, priority to the knight vs pawn: remains.
355 // Finally: both disappears.
356 let remain = null;
1a3cfdc0
BA
357 const p1 = m1.vanish[0].p;
358 const p2 = m2.vanish[0].p;
6ec2feb2
BA
359 if (!!m1.illegal && !m2.illegal) remain = { c: 'w', p: p1 };
360 else if (!!m2.illegal && !m1.illegal) remain = { c: 'b', p: p2 };
361 if (!remain) {
362 // Either both are illegal or both are legal
363 if (p1 == V.KNIGHT && p2 == V.PAWN) remain = { c: 'w', p: p1 };
364 else if (p2 == V.KNIGHT && p1 == V.PAWN) remain = { c: 'b', p: p2 };
365 // If remain is still null: same type same risk, both disappear
366 }
367 if (!!remain) {
1a3cfdc0
BA
368 smove.appear.push({
369 x: m1.end.x,
370 y: m1.end.y,
6ec2feb2
BA
371 p: remain.p,
372 c: remain.c
1a3cfdc0
BA
373 });
374 }
375 }
376 return smove;
377 }
378
379 play(move) {
6ec2feb2
BA
380 if (!this.states) this.states = [];
381 const stateFen = this.getFen();
382 this.states.push(stateFen);
383
1a3cfdc0
BA
384 // Do not play on board (would reveal the move...)
385 move.flags = JSON.stringify(this.aggregateFlags());
386 this.turn = V.GetOppCol(this.turn);
387 this.movesCount++;
388 this.postPlay(move);
389 }
390
391 postPlay(move) {
392 if (this.turn == 'b') {
393 // NOTE: whiteMove is used read-only, so no need to copy
394 this.whiteMove = move;
395 return;
396 }
1a3cfdc0
BA
397 // A full turn just ended:
398 const smove = this.resolveSynchroneMove(move);
399 V.PlayOnBoard(this.board, smove);
400 move.whiteMove = this.whiteMove; //for undo
401 this.whiteMove = null;
402 move.smove = smove;
403 }
404
405 undo(move) {
406 this.disaggregateFlags(JSON.parse(move.flags));
407 if (this.turn == 'w')
408 // Back to the middle of the move
409 V.UndoOnBoard(this.board, move.smove);
410 this.turn = V.GetOppCol(this.turn);
411 this.movesCount--;
412 this.postUndo(move);
6ec2feb2
BA
413
414 const stateFen = this.getFen();
415 if (stateFen != this.states[this.states.length-1]) debugger;
416 this.states.pop();
1a3cfdc0
BA
417 }
418
419 postUndo(move) {
420 if (this.turn == 'w') this.whiteMove = null;
421 else this.whiteMove = move.whiteMove;
422 }
423
424 getCheckSquares(color) {
425 return [];
426 }
427
428 getCurrentScore() {
429 if (this.turn == 'b')
430 // Turn (white + black) not over yet
431 return "*";
432 // Count footmen: if a side has none, it loses
433 let fmCount = { 'w': 0, 'b': 0 };
434 for (let i=0; i<5; i++) {
435 for (let j=0; j<5; j++) {
436 if (this.board[i][j] != V.EMPTY && this.getPiece(i, j) == V.PAWN)
437 fmCount[this.getColor(i, j)]++;
438 }
439 }
440 if (Object.values(fmCount).some(v => v == 0)) {
441 if (fmCount['w'] == 0 && fmCount['b'] == 0)
442 // Everyone died
443 return "1/2";
444 if (fmCount['w'] == 0) return "0-1";
445 return "1-0"; //fmCount['b'] == 0
446 }
447 // Check penaltyFlags: if a side has 2 or more, it loses
6ec2feb2
BA
448 if (Object.values(this.penaltyFlags).every(v => v == 2)) return "1/2";
449 if (this.penaltyFlags['w'] == 2) return "0-1";
450 if (this.penaltyFlags['b'] == 2) return "1-0";
1a3cfdc0
BA
451 if (!this.atLeastOneMove('w') || !this.atLeastOneMove('b'))
452 // Stalemate (should be very rare)
453 return "1/2";
454 return "*";
455 }
456
457 getComputerMove() {
458 const maxeval = V.INFINITY;
459 const color = this.turn;
460 let moves = this.getAllValidMoves();
461 if (moves.length == 0)
462 // TODO: this situation should not happen
463 return null;
464
465 if (Math.random() < 0.5)
466 // Return a random move
467 return moves[randInt(moves.length)];
468
469 // Rank moves at depth 1:
470 // try to capture something (not re-capturing)
471 moves.forEach(m => {
472 V.PlayOnBoard(this.board, m);
473 m.eval = this.evalPosition();
474 V.UndoOnBoard(this.board, m);
475 });
476 moves.sort((a, b) => {
477 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
478 });
479 let candidates = [0];
480 for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
481 candidates.push(i);
482 return moves[candidates[randInt(candidates.length)]];
483 }
484
485 getNotation(move) {
486 // Basic system: piece + init + dest square
487 return (
488 move.vanish[0].p.toUpperCase() +
489 V.CoordsToSquare(move.start) +
490 V.CoordsToSquare(move.end)
491 );
492 }
493};