Some fixes + draft Avalanche
[vchess.git] / client / src / variants / Avalanche.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class AvalancheRules extends ChessRules {
5
6 static get PawnSpecs() {
7 return (
8 Object.assign(
9 { promotions: [V.PAWN] },
10 ChessRules.PawnSpecs
11 )
12 );
13 }
14
15 static get HasEnpassant() {
16 return false;
17 }
18
19 static IsGoodFen(fen) {
20 if (!ChessRules.IsGoodFen(fen)) return false;
21 const fenParts = fen.split(" ");
22 if (fenParts.length != 5) return false;
23 if (!fenParts[4].match(/^[0-8]$/)) return false;
24 return true;
25 }
26
27 canIplay(side, [x, y]) {
28 if (this.subTurn == 0) return (x >= V.size.x);
29 const c = this.getColor(x, y);
30 return (
31 (this.subTurn == 1 && c == side) ||
32 (this.subTurn == 2 && c != side && this.getPiece(x, y) == V.PAWN)
33 );
34 }
35
36 static ParseFen(fen) {
37 const fenParts = fen.split(" ");
38 return Object.assign(
39 ChessRules.ParseFen(fen),
40 { promoteFile: fenParts[4] }
41 );
42 }
43
44 getPromoteFen() {
45 const L = this.promoteFile.length;
46 return (this.promoteFile[L-1] + 1);
47 }
48
49 getFen() {
50 return super.getFen() + " " + this.getPromoteFen();
51 }
52
53 getFenForRepeat() {
54 return super.getFenForRepeat() + "_" + this.getPromoteFen();
55 }
56
57 static GenRandInitFen(randomness) {
58 return ChessRules.GenRandInitFen(randomness).slice(0, -1) + "0";
59 }
60
61 getPiece(i, j) {
62 if (i >= V.size.x) return V.RESERVE_PIECES[j];
63 return this.board[i][j].charAt(1);
64 }
65
66 static get RESERVE_PIECES() {
67 // Promotion pieces
68 return [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
69 }
70
71 setOtherVariables(fen) {
72 super.setOtherVariables(fen);
73 const fenPromoteFile = V.ParseFen(fen).promoteFile;
74 this.promoteFile = [parseInt(fenPromoteFile, 10) - 1];
75 this.reserve = { 'w': null, 'b': null };
76 if (this.promoteFile[0] >= 0) {
77 this.reserve = {
78 [this.turn]: {
79 [V.ROOK]: 1,
80 [V.KNIGHT]: 1,
81 [V.BISHOP]: 1,
82 [V.QUEEN]: 1
83 }
84 };
85 this.subTurn = 0;
86 }
87 else this.subTurn = 1;
88 }
89
90 getReservePpath(index, color) {
91 return color + V.RESERVE_PIECES[index];
92 }
93
94 getReserveMove(y) {
95 // Send a new piece piece to our first rank
96 const color = this.turn;
97 const L = this.promoteFile.length;
98 const [rank, file] = [color == 'w' ? 0 : 7, this.promoteFile[L-1]];
99 return new Move({
100 appear: [
101 new PiPo({ x: rank, y: file, c: color, p: V.RESERVE_PIECES[y] })
102 ],
103 vanish: [
104 new PiPo({ x: rank, y: file, c: color, p: V.PAWN })
105 ],
106 start: { x: 8, y: y },
107 end: { x: rank, y: file }
108 });
109 }
110
111 getPotentialMovesFrom([x, y]) {
112 if (this.subTurn == 0)
113 // Reserves, outside of board: x == sizeX(+1)
114 return (x >= 8 ? [this.getReserveMove(y)] : []);
115 if (this.subTurn == 1)
116 // Usual case:
117 return super.getPotentialMovesFrom([x, y]);
118 // subTurn == 2: only allowed to push an opponent's pawn (if possible)
119 const oppPawnShift = (this.turn == 'w' ? 1 : -1);
120 if (
121 V.OnBoard(x + oppPawnShift, y) &&
122 this.board[x + oppPawnShift][y] == V.EMPTY
123 ) {
124 return [this.getBasicMove([x, y], [x + oppPawnShift, y])];
125 }
126 return [];
127 }
128
129 getAllValidMoves() {
130 if (this.subTurn == 0) {
131 let moves = [];
132 for (let y = 0; y < V.RESERVE_PIECES.length; y++)
133 moves.push(this.getReserveMove(y));
134 return moves;
135 }
136 if (this.subTurn == 1)
137 return this.filterValid(super.getAllPotentialMoves());
138 // subTurn == 2: move opponent's pawn only
139 let moves = [];
140 const oppCol = V.GetOppCol(this.turn);
141 for (let i = 0; i < 8; i++) {
142 for (let j = 0; j < 8; j++) {
143 if (
144 this.board[i][j] != V.EMPTY &&
145 this.getColor(i, j) == oppCol &&
146 this.getPiece(i, j) == V.PAWN
147 ) {
148 Array.prototype.push.apply(
149 moves, this.getPotentialMovesFrom([i, j]));
150 }
151 }
152 }
153 return moves;
154 }
155
156 filterValid(moves) {
157 if (this.subTurn != 1) return moves; //self-checks by pawns are allowed
158 return super.filterValid(moves);
159 }
160
161 atLeastOneMove() {
162 if (this.subTurn == 0) return true; //TODO: never called in this situation
163 if (this.subTurn == 1) {
164 // Cannot use super method: infinite recursive calls
165 const color = this.turn;
166 for (let i = 0; i < 8; i++) {
167 for (let j = 0; j < 8; j++) {
168 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
169 const moves = this.getPotentialMovesFrom([i, j]);
170 if (moves.length > 0) {
171 for (let k = 0; k < moves.length; k++) {
172 const piece = moves[k].vanish[0].p;
173 if (piece == V.KING) {
174 this.kingPos[color] =
175 [moves[k].appear[0].x, moves[k].appear[0].y];
176 }
177 V.PlayOnBoard(this.board, moves[k]);
178 const res = !this.underCheck(color);
179 V.UndoOnBoard(this.board, moves[k]);
180 if (piece == V.KING) {
181 this.kingPos[color] =
182 [moves[k].vanish[0].x, moves[k].vanish[0].y];
183 }
184 if (res) return true;
185 }
186 }
187 }
188 }
189 }
190 return false;
191 }
192 // subTurn == 2: need to find an enemy pawn which can advance
193 const oppCol = V.GetOppCol(this.turn);
194 const oppPawnShift = (oppCol == 'w' ? -1 : 1);
195 for (let i = 0; i < 8; i++) {
196 for (let j = 0; j < 8; j++) {
197 if (
198 this.board[i][j] != V.EMPTY &&
199 this.getColor(i, j) == oppCol &&
200 this.getPiece(i, j) == V.PAWN &&
201 V.OnBoard(i + oppPawnShift, j) &&
202 this.board[i + oppPawnShift][j] == V.EMPTY
203 ) {
204 return true;
205 }
206 }
207 }
208 return false;
209 }
210
211 getCheckSquares() {
212 if (this.kingPos[this.turn][0] < 0) return [];
213 return super.getCheckSquares();
214 }
215
216 getCurrentScore() {
217 // If my king disappeared: I lost!
218 const c = this.turn;
219 if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
220 return super.getCurrentScore();
221 }
222
223 prePlay(move) {
224 if (this.subTurn != 1) return;
225 const c = move.vanish[0].c;
226 const piece = move.vanish[0].p;
227 const firstRank = c == "w" ? V.size.x - 1 : 0;
228 if (piece == V.KING) {
229 this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
230 this.castleFlags[c] = [V.size.y, V.size.y];
231 return;
232 }
233 const oppCol = V.GetOppCol(c);
234 if (move.vanish.length == 2 && move.vanish[1].p == V.KING) {
235 // Opponent's king is captured, game over
236 this.kingPos[oppCol] = [-1, -1];
237 move.captureKing = true;
238 }
239 const oppFirstRank = V.size.x - 1 - firstRank;
240 if (
241 move.start.x == firstRank && //our rook moves?
242 this.castleFlags[c].includes(move.start.y)
243 ) {
244 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
245 this.castleFlags[c][flagIdx] = V.size.y;
246 }
247 if (
248 move.end.x == oppFirstRank && //we took opponent rook?
249 this.castleFlags[oppCol].includes(move.end.y)
250 ) {
251 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
252 this.castleFlags[oppCol][flagIdx] = V.size.y;
253 }
254 }
255
256 play(move) {
257 move.flags = JSON.stringify(this.aggregateFlags());
258 this.prePlay(move);
259 V.PlayOnBoard(this.board, move);
260 const c = this.turn;
261 move.turn = [c, this.subTurn];
262 const oppCol = V.GetOppCol(c);
263 const oppLastRank = (c == 'w' ? 7 : 0);
264 if (this.subTurn <= 1) this.reserve[oppCol] = null;
265 if (this.subTurn == 0) {
266 this.subTurn++;
267 this.reserve[c] = null;
268 }
269 else if (this.subTurn == 1) {
270 this.subTurn++;
271 if (
272 this.movesCount == 0 ||
273 !!move.captureKing ||
274 !this.atLeastOneMove()
275 ) {
276 this.turn = oppCol;
277 this.movesCount++;
278 this.subTurn = 1;
279 this.promoteFile.push(-1);
280 move.pushPromote = true;
281 }
282 }
283 else {
284 // subTurn == 2
285 this.turn = oppCol;
286 if (move.end.x == oppLastRank) {
287 this.promoteFile.push(move.end.y);
288 this.reserve[oppCol] = {
289 [V.ROOK]: 1,
290 [V.KNIGHT]: 1,
291 [V.BISHOP]: 1,
292 [V.QUEEN]: 1
293 };
294 this.subTurn = 0;
295 }
296 else {
297 this.subTurn = 1;
298 this.promoteFile.push(-1);
299 }
300 move.pushPromote = true;
301 this.movesCount++;
302 }
303 }
304
305 undo(move) {
306 this.disaggregateFlags(JSON.parse(move.flags));
307 V.UndoOnBoard(this.board, move);
308 const changeTurn = (this.turn != move.turn[0]);
309 this.turn = move.turn[0];
310 this.subTurn = move.turn[1];
311 if (!!move.pushPromote) {
312 const promoteFile = this.promoteFile.pop();
313 if (promoteFile >= 0) this.reserve[V.GetOppCol(this.turn)] = null;
314 }
315 else if (this.subTurn == 0) {
316 this.reserve[this.turn] = {
317 [V.ROOK]: 1,
318 [V.KNIGHT]: 1,
319 [V.BISHOP]: 1,
320 [V.QUEEN]: 1
321 };
322 }
323 if (changeTurn) this.movesCount--;
324 this.postUndo(move);
325 }
326
327 postUndo(move) {
328 if (this.subTurn != 1) return;
329 if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
330 // Opponent's king was captured
331 this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
332 super.postUndo(move);
333 }
334
335 getComputerMove() {
336 // Just try to capture as much material as possible (1-half move)
337 const moves = this.getAllValidMoves();
338 if (this.subTurn == 0) {
339 this.play(moves[3]); //HACK... 3 = queen index
340 const res = this.getComputerMove();
341 this.undo(moves[3]);
342 return [moves[3], res];
343 }
344 // subTurn == 1 (necessarily)
345 let candidates = [];
346 let maxValue = -V.INFINITY;
347 for (let m of moves) {
348 let value = 0;
349 if (m.vanish.length == 2) {
350 // Compute delta value, to not give all material on pawns... (TODO)
351 // 0.5 to favor captures (if same type of piece).
352 value = 0.5 +
353 ChessRules.VALUES[m.vanish[1].p] - ChessRules.VALUES[m.vanish[0].p];
354 }
355 if (value > maxValue) {
356 candidates = [m];
357 maxValue = value;
358 }
359 else if (value == maxValue) candidates.push(m);
360 }
361 const m1 = candidates[randInt(candidates.length)];
362 this.play(m1);
363 let m2 = null;
364 if (this.subTurn == 2) {
365 // Just pick a pawn at random
366 const moves2 = this.getAllValidMoves();
367 m2 = moves2[randInt(moves2.length)];
368 }
369 this.undo(m1);
370 if (!m2) return m1;
371 return [m1, m2];
372 }
373
374 getNotation(move) {
375 if (this.subTurn == 0)
376 return move.appear[0].p.toUpperCase() + "@" + V.CoordsToSquare(move.end);
377 if (this.subTurn == 1) return super.getNotation(move);
378 // subTurn == 2: just indicate final square
379 return V.CoordsToSquare(move.end);
380 }
381
382 };