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