Almost completed Empire Chess. Next is Synochess (+ rules translations)
[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 {
7e8a7ea1 5
1a3cfdc0
BA
6 static get PawnSpecs() {
7 return Object.assign(
8 {},
9 ChessRules.PawnSpecs,
10 {
11 twoSquares: false,
12 promotions: [V.KNIGHT]
13 }
14 );
15 }
16
00eef1ca
BA
17 static get SomeHiddenMoves() {
18 return true;
19 }
20
1a3cfdc0
BA
21 static get HasCastle() {
22 return false;
23 }
24
25 static get HasEnpassant() {
26 return false;
27 }
28
29 static get CanAnalyze() {
70d47c1d 30 return false;
1a3cfdc0
BA
31 }
32
33 static get ShowMoves() {
34 return "byrow";
35 }
36
6ec2feb2
BA
37 getPPpath(m) {
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;
41 }
42
1a3cfdc0
BA
43 static get PIECES() {
44 return [V.PAWN, V.KNIGHT];
45 }
46
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) {
54 let sumElts = 0;
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++;
58 else {
e50a8025 59 const num = parseInt(row[i], 10);
1a3cfdc0
BA
60 if (isNaN(num)) return false;
61 sumElts += num;
62 }
63 }
64 if (sumElts != V.size.y) return false;
65 }
66 if (Object.values(pawns).some(v => v == 0))
67 return false;
68 return true;
69 }
70
71 static IsGoodFen(fen) {
72 if (!ChessRules.IsGoodFen(fen)) return false;
73 const fenParsed = V.ParseFen(fen);
74 // 4) Check whiteMove
75 if (
76 (
9edfb714 77 fenParsed.turn == "b" &&
1a3cfdc0
BA
78 // NOTE: do not check really JSON stringified move...
79 (!fenParsed.whiteMove || fenParsed.whiteMove == "-")
80 )
81 ||
9edfb714 82 (fenParsed.turn == "w" && fenParsed.whiteMove != "-")
1a3cfdc0
BA
83 ) {
84 return false;
85 }
86 return true;
87 }
88
89 static IsGoodFlags(flags) {
90 return !!flags.match(/^[0-2]{2,2}$/);
91 }
92
93 aggregateFlags() {
94 return this.penaltyFlags;
95 }
96
97 disaggregateFlags(flags) {
98 this.penaltyFlags = flags;
99 }
100
101 static ParseFen(fen) {
102 const fenParts = fen.split(" ");
103 return Object.assign(
104 ChessRules.ParseFen(fen),
105 { whiteMove: fenParts[4] }
106 );
107 }
108
109 static get size() {
110 return { x: 5, y: 5 };
111 }
112
113 static GenRandInitFen() {
114 return "npppn/p3p/5/P3P/NPPPN w 0 00 -";
115 }
116
117 getFen() {
118 return super.getFen() + " " + this.getWhitemoveFen();
119 }
120
121 getFenForRepeat() {
122 return super.getFenForRepeat() + "_" + this.getWhitemoveFen();
123 }
124
125 getFlagsFen() {
6ec2feb2
BA
126 return (
127 this.penaltyFlags['w'].toString() + this.penaltyFlags['b'].toString()
128 );
1a3cfdc0
BA
129 }
130
131 setOtherVariables(fen) {
132 const parsedFen = V.ParseFen(fen);
133 this.setFlags(parsedFen.flags);
134 // Also init whiteMove
135 this.whiteMove =
136 parsedFen.whiteMove != "-"
137 ? JSON.parse(parsedFen.whiteMove)
138 : null;
139 }
140
141 setFlags(fenflags) {
6ec2feb2 142 this.penaltyFlags = {
e50a8025
BA
143 'w': parseInt(fenflags[0], 10),
144 'b': parseInt(fenflags[1], 10)
6ec2feb2 145 };
1a3cfdc0
BA
146 }
147
148 getWhitemoveFen() {
149 if (!this.whiteMove) return "-";
150 return JSON.stringify({
151 start: this.whiteMove.start,
152 end: this.whiteMove.end,
153 appear: this.whiteMove.appear,
6e0f2842 154 vanish: this.whiteMove.vanish
1a3cfdc0
BA
155 });
156 }
157
158 getSpeculations(moves, sq) {
159 let moveSet = {};
160 moves.forEach(m => {
161 const mHash = "m" + m.start.x + m.start.y + m.end.x + m.end.y;
162 moveSet[mHash] = true;
163 });
164 const color = this.turn;
165 this.turn = V.GetOppCol(color);
166 const oppMoves = super.getAllValidMoves();
167 this.turn = color;
6ec2feb2 168 // For each opponent's move, generate valid moves [from sq if same color]
1a3cfdc0
BA
169 let speculations = [];
170 oppMoves.forEach(m => {
171 V.PlayOnBoard(this.board, m);
172 const newValidMoves =
173 !!sq
6ec2feb2
BA
174 ? (
175 this.getColor(sq[0], sq[1]) == color
176 ? super.getPotentialMovesFrom(sq)
177 : []
178 )
1a3cfdc0
BA
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;
6e0f2842 184 vm.end.illegal = true; //potentially illegal!
1a3cfdc0
BA
185 speculations.push(vm);
186 }
187 });
188 V.UndoOnBoard(this.board, m);
189 });
190 return speculations;
191 }
192
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]));
197 }
198
199 getAllValidMoves() {
200 // Return possible moves + potentially valid moves
201 const validMoves = super.getAllValidMoves();
202 return validMoves.concat(this.getSpeculations(validMoves));
203 }
204
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 = [];
6ec2feb2
BA
213 for (let i = 0; i < V.size.x; i++) {
214 for (let j = 0; j < V.size.y; j++) {
1a3cfdc0
BA
215 if (this.board[i][j] == V.EMPTY) emptySquares.push([i, j]);
216 else if (
217 this.getColor(i, j) == color &&
218 this.getPiece(i, j) == V.KNIGHT
219 ) {
220 knightCounter++;
221 }
222 }
223 }
224 if (knightCounter <= 1) finalPieces = [V.KNIGHT];
225 else {
6ec2feb2
BA
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({
230 x: x2,
231 y: y2,
232 c: this.getColor(x2, y2),
233 p: this.getPiece(x2, y2)
234 }));
235 }
1a3cfdc0 236 emptySquares.forEach(sq => {
6ec2feb2
BA
237 if (sq[0] != lastRank) {
238 let newMove = this.getBasicMove([x1, y1], [sq[0], sq[1]]);
239 if (!!capture) newMove.vanish.push(capture);
240 moves.push(newMove);
241 }
1a3cfdc0
BA
242 });
243 return;
244 }
245 }
246 let tr = null;
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));
250 }
251 }
252
253 filterValid(moves) {
254 // No checks:
255 return moves;
256 }
257
258 atLeastOneMove(color) {
259 const curTurn = this.turn;
260 this.turn = color;
261 const res = super.atLeastOneMove();
262 this.turn = curTurn;
263 return res;
264 }
265
266 // White and black (partial) moves were played: merge
267 resolveSynchroneMove(move) {
6ec2feb2
BA
268 let m1 = this.whiteMove;
269 let m2 = move;
270 const movingLikeCapture = (m) => {
271 const shift = (m.vanish[0].c == 'w' ? -1 : 1);
272 return (
273 m.start.x + shift == m.end.x &&
274 Math.abs(m.end.y - m.start.y) == 1
275 );
276 };
277 const isPossible = (m, other) => {
278 return (
279 (
280 m.vanish[0].p == V.KNIGHT &&
b866a62a
BA
281 (
282 m.vanish.length == 1 ||
283 m.vanish[1].c != m.vanish[0].c ||
284 // Self-capture attempt
285 (
6e0f2842 286 !other.end.illegal &&
b866a62a
BA
287 other.end.x == m.end.x &&
288 other.end.y == m.end.y
289 )
290 )
6ec2feb2
BA
291 )
292 ||
293 (
b866a62a 294 m.vanish[0].p == V.PAWN &&
6e0f2842 295 !other.end.illegal &&
b866a62a
BA
296 (
297 (
298 // Promotion attempt
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
303 )
304 ||
305 (
306 // Moving attempt
307 !movingLikeCapture(m) &&
308 other.start.x == m.end.x &&
309 other.start.y == m.end.y
310 )
311 ||
312 (
313 // Capture attempt
314 movingLikeCapture(m) &&
315 other.end.x == m.end.x &&
316 other.end.y == m.end.y
317 )
318 )
6ec2feb2
BA
319 )
320 );
321 };
6e0f2842 322 if (!!m1.end.illegal && !isPossible(m1, m2)) {
6ec2feb2
BA
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]++;
327 m1.isNull = true;
1a3cfdc0 328 }
6e0f2842 329 if (!!m2.end.illegal && !isPossible(m2, m1)) {
6ec2feb2
BA
330 this.penaltyFlags[m2.vanish[0].c]++;
331 m2.isNull = true;
332 }
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;
1a3cfdc0
BA
338 // For PlayOnBoard (no need for start / end, irrelevant)
339 let smove = {
340 appear: [],
341 vanish: []
342 };
1a3cfdc0 343 if (!m1 && !m2) return smove;
54f51146 344 // Both moves are now legal or at least possible:
1a3cfdc0
BA
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:
352 if (
353 m1.vanish.length == 2 &&
354 (
355 m1.vanish[1].x != m2.start.x ||
356 m1.vanish[1].y != m2.start.y
357 )
358 ) {
359 smove.vanish.push(m1.vanish[1]);
360 }
361 if (
362 m2.vanish.length == 2 &&
363 (
364 m2.vanish[1].x != m1.start.x ||
365 m2.vanish[1].y != m1.start.y
366 )
367 ) {
368 smove.vanish.push(m2.vanish[1]);
369 }
370 } else {
6ec2feb2
BA
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.
375 let remain = null;
1a3cfdc0
BA
376 const p1 = m1.vanish[0].p;
377 const p2 = m2.vanish[0].p;
6e0f2842
BA
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 };
6ec2feb2
BA
380 if (!remain) {
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
385 }
386 if (!!remain) {
1a3cfdc0
BA
387 smove.appear.push({
388 x: m1.end.x,
389 y: m1.end.y,
6ec2feb2
BA
390 p: remain.p,
391 c: remain.c
1a3cfdc0
BA
392 });
393 }
394 }
395 return smove;
396 }
397
398 play(move) {
399 // Do not play on board (would reveal the move...)
400 move.flags = JSON.stringify(this.aggregateFlags());
401 this.turn = V.GetOppCol(this.turn);
402 this.movesCount++;
403 this.postPlay(move);
404 }
405
406 postPlay(move) {
407 if (this.turn == 'b') {
408 // NOTE: whiteMove is used read-only, so no need to copy
409 this.whiteMove = move;
410 return;
411 }
1a3cfdc0
BA
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;
417 move.smove = smove;
418 }
419
420 undo(move) {
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);
426 this.movesCount--;
427 this.postUndo(move);
428 }
429
430 postUndo(move) {
431 if (this.turn == 'w') this.whiteMove = null;
432 else this.whiteMove = move.whiteMove;
433 }
434
af34341d 435 getCheckSquares() {
1a3cfdc0
BA
436 return [];
437 }
438
439 getCurrentScore() {
440 if (this.turn == 'b')
441 // Turn (white + black) not over yet
442 return "*";
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)]++;
449 }
450 }
451 if (Object.values(fmCount).some(v => v == 0)) {
452 if (fmCount['w'] == 0 && fmCount['b'] == 0)
453 // Everyone died
454 return "1/2";
455 if (fmCount['w'] == 0) return "0-1";
456 return "1-0"; //fmCount['b'] == 0
457 }
458 // Check penaltyFlags: if a side has 2 or more, it loses
6ec2feb2
BA
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";
1a3cfdc0
BA
462 if (!this.atLeastOneMove('w') || !this.atLeastOneMove('b'))
463 // Stalemate (should be very rare)
464 return "1/2";
465 return "*";
466 }
467
468 getComputerMove() {
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
474 return null;
475
1a3cfdc0 476 // Rank moves at depth 1:
9edfb714
BA
477 let validMoves = [];
478 let illegalMoves = [];
1a3cfdc0 479 moves.forEach(m => {
9edfb714 480 // Warning: m might be illegal!
6e0f2842 481 if (!m.end.illegal) {
9edfb714
BA
482 V.PlayOnBoard(this.board, m);
483 m.eval = this.evalPosition();
484 V.UndoOnBoard(this.board, m);
485 validMoves.push(m);
486 } else illegalMoves.push(m);
1a3cfdc0 487 });
9edfb714
BA
488
489 const illegalRatio = illegalMoves.length / moves.length;
490 if (Math.random() < illegalRatio)
491 // Return a random illegal move
492 return illegalMoves[randInt(illegalMoves.length)];
493
494 validMoves.sort((a, b) => {
1a3cfdc0
BA
495 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
496 });
497 let candidates = [0];
9edfb714
BA
498 for (
499 let i = 1;
500 i < validMoves.length && validMoves[i].eval == moves[0].eval;
501 i++
502 ) {
1a3cfdc0 503 candidates.push(i);
9edfb714
BA
504 }
505 return validMoves[candidates[randInt(candidates.length)]];
1a3cfdc0
BA
506 }
507
508 getNotation(move) {
509 // Basic system: piece + init + dest square
510 return (
54f51146 511 (move.vanish[0].p == V.KNIGHT ? "N" : "") +
1a3cfdc0
BA
512 V.CoordsToSquare(move.start) +
513 V.CoordsToSquare(move.end)
514 );
515 }
7e8a7ea1 516
1a3cfdc0 517};