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