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