Commit | Line | Data |
---|---|---|
4258b58c BA |
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]) { | |
cfeaa4a1 | 28 | if (side != this.turn) return false; |
4258b58c BA |
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 | return [moves[3], res]; | |
344 | } | |
345 | // subTurn == 1 (necessarily) | |
346 | let candidates = []; | |
347 | let maxValue = -V.INFINITY; | |
348 | for (let m of moves) { | |
349 | let value = 0; | |
350 | if (m.vanish.length == 2) { | |
351 | // Compute delta value, to not give all material on pawns... (TODO) | |
352 | // 0.5 to favor captures (if same type of piece). | |
353 | value = 0.5 + | |
354 | ChessRules.VALUES[m.vanish[1].p] - ChessRules.VALUES[m.vanish[0].p]; | |
355 | } | |
356 | if (value > maxValue) { | |
357 | candidates = [m]; | |
358 | maxValue = value; | |
359 | } | |
360 | else if (value == maxValue) candidates.push(m); | |
361 | } | |
362 | const m1 = candidates[randInt(candidates.length)]; | |
363 | this.play(m1); | |
364 | let m2 = null; | |
365 | if (this.subTurn == 2) { | |
366 | // Just pick a pawn at random | |
367 | const moves2 = this.getAllValidMoves(); | |
368 | m2 = moves2[randInt(moves2.length)]; | |
369 | } | |
370 | this.undo(m1); | |
371 | if (!m2) return m1; | |
372 | return [m1, m2]; | |
373 | } | |
374 | ||
375 | getNotation(move) { | |
376 | if (this.subTurn == 0) | |
377 | return move.appear[0].p.toUpperCase() + "@" + V.CoordsToSquare(move.end); | |
378 | if (this.subTurn == 1) return super.getNotation(move); | |
379 | // subTurn == 2: just indicate final square | |
380 | return V.CoordsToSquare(move.end); | |
381 | } | |
382 | ||
383 | }; |