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