Fix Koopa promotions with captures, and Balakhlava: pawns move forward
[vchess.git] / client / src / variants / Alice.js
1 import { ChessRules } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3
4 // NOTE: alternative implementation, probably cleaner = use only 1 board
5 // TODO? atLeastOneMove() would be more efficient if rewritten here
6 // (less sideBoard computations)
7 export class AliceRules extends ChessRules {
8 static get ALICE_PIECES() {
9 return {
10 s: "p",
11 t: "q",
12 u: "r",
13 c: "b",
14 o: "n",
15 l: "k"
16 };
17 }
18 static get ALICE_CODES() {
19 return {
20 p: "s",
21 q: "t",
22 r: "u",
23 b: "c",
24 n: "o",
25 k: "l"
26 };
27 }
28
29 static get PIECES() {
30 return ChessRules.PIECES.concat(Object.keys(V.ALICE_PIECES));
31 }
32
33 getPpath(b) {
34 return (Object.keys(V.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b;
35 }
36
37 getEpSquare(moveOrSquare) {
38 if (!moveOrSquare) return undefined;
39 if (typeof moveOrSquare === "string") {
40 const square = moveOrSquare;
41 if (square == "-") return undefined;
42 return V.SquareToCoords(square);
43 }
44 // Argument is a move:
45 const move = moveOrSquare;
46 const s = move.start,
47 e = move.end;
48 if (
49 s.y == e.y &&
50 Math.abs(s.x - e.x) == 2 &&
51 // Special conditions: a pawn can be on the other side
52 ['p','s'].includes(move.appear[0].p) &&
53 ['p','s'].includes(move.vanish[0].p)
54 ) {
55 return {
56 x: (s.x + e.x) / 2,
57 y: s.y
58 };
59 }
60 return undefined; //default
61 }
62
63 // king can be l or L (on the other mirror side)
64 static IsGoodPosition(position) {
65 if (position.length == 0) return false;
66 const rows = position.split("/");
67 if (rows.length != V.size.x) return false;
68 let kings = { "k": 0, "K": 0, 'l': 0, 'L': 0 };
69 for (let row of rows) {
70 let sumElts = 0;
71 for (let i = 0; i < row.length; i++) {
72 if (['K','k','L','l'].includes(row[i])) kings[row[i]]++;
73 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
74 else {
75 const num = parseInt(row[i], 10);
76 if (isNaN(num)) return false;
77 sumElts += num;
78 }
79 }
80 if (sumElts != V.size.y) return false;
81 }
82 if (kings['k'] + kings['l'] != 1 || kings['K'] + kings['L'] != 1)
83 return false;
84 return true;
85 }
86
87 setOtherVariables(fen) {
88 super.setOtherVariables(fen);
89 const rows = V.ParseFen(fen).position.split("/");
90 if (this.kingPos["w"][0] < 0 || this.kingPos["b"][0] < 0) {
91 // INIT_COL_XXX won't be required if Alice kings are found
92 // (it means 'king moved')
93 for (let i = 0; i < rows.length; i++) {
94 let k = 0; //column index on board
95 for (let j = 0; j < rows[i].length; j++) {
96 switch (rows[i].charAt(j)) {
97 case "l":
98 this.kingPos["b"] = [i, k];
99 break;
100 case "L":
101 this.kingPos["w"] = [i, k];
102 break;
103 default: {
104 const num = parseInt(rows[i].charAt(j), 10);
105 if (!isNaN(num)) k += num - 1;
106 }
107 }
108 k++;
109 }
110 }
111 }
112 }
113
114 // Return the (standard) color+piece notation at a square for a board
115 getSquareOccupation(i, j, mirrorSide) {
116 const piece = this.getPiece(i, j);
117 if (mirrorSide == 1 && Object.keys(V.ALICE_CODES).includes(piece))
118 return this.board[i][j];
119 if (mirrorSide == 2 && Object.keys(V.ALICE_PIECES).includes(piece))
120 return this.getColor(i, j) + V.ALICE_PIECES[piece];
121 return "";
122 }
123
124 // Build board of the given (mirror)side
125 getSideBoard(mirrorSide) {
126 // Build corresponding board from complete board
127 let sideBoard = ArrayFun.init(V.size.x, V.size.y, "");
128 for (let i = 0; i < V.size.x; i++) {
129 for (let j = 0; j < V.size.y; j++)
130 sideBoard[i][j] = this.getSquareOccupation(i, j, mirrorSide);
131 }
132 return sideBoard;
133 }
134
135 // NOTE: castle & enPassant
136 // https://www.chessvariants.com/other.dir/alice.html
137 getPotentialMovesFrom([x, y], sideBoard) {
138 const pieces = Object.keys(V.ALICE_CODES);
139 const codes = Object.keys(V.ALICE_PIECES);
140 const mirrorSide = pieces.includes(this.getPiece(x, y)) ? 1 : 2;
141 if (!sideBoard) sideBoard = [this.getSideBoard(1), this.getSideBoard(2)];
142 const color = this.getColor(x, y);
143
144 // Search valid moves on sideBoard
145 const saveBoard = this.board;
146 this.board = sideBoard[mirrorSide - 1];
147 const moves = super.getPotentialMovesFrom([x, y]).filter(m => {
148 // Filter out king moves which result in under-check position on
149 // current board (before mirror traversing)
150 let aprioriValid = true;
151 if (m.appear[0].p == V.KING) {
152 this.play(m);
153 if (this.underCheck(color, sideBoard)) aprioriValid = false;
154 this.undo(m);
155 }
156 return aprioriValid;
157 });
158 this.board = saveBoard;
159
160 // Finally filter impossible moves
161 const res = moves.filter(m => {
162 if (m.appear.length == 2) {
163 // Castle: appear[i] must be an empty square on the other board
164 for (let psq of m.appear) {
165 if (
166 this.getSquareOccupation(psq.x, psq.y, 3 - mirrorSide) != V.EMPTY
167 ) {
168 return false;
169 }
170 }
171 } else if (this.board[m.end.x][m.end.y] != V.EMPTY) {
172 // Attempt to capture
173 const piece = this.getPiece(m.end.x, m.end.y);
174 if (
175 (mirrorSide == 1 && codes.includes(piece)) ||
176 (mirrorSide == 2 && pieces.includes(piece))
177 ) {
178 return false;
179 }
180 }
181 // If the move is computed on board1, m.appear change for Alice pieces.
182 if (mirrorSide == 1) {
183 m.appear.forEach(psq => {
184 // forEach: castling taken into account
185 psq.p = V.ALICE_CODES[psq.p]; //goto board2
186 });
187 }
188 else {
189 // Move on board2: mark vanishing pieces as Alice
190 m.vanish.forEach(psq => {
191 psq.p = V.ALICE_CODES[psq.p];
192 });
193 }
194 // Fix en-passant captures
195 if (
196 m.vanish[0].p == V.PAWN &&
197 m.vanish.length == 2 &&
198 this.board[m.end.x][m.end.y] == V.EMPTY
199 ) {
200 m.vanish[1].c = V.GetOppCol(this.turn);
201 const [epX, epY] = [m.vanish[1].x, m.vanish[1].y];
202 m.vanish[1].p = this.getPiece(epX, epY);
203 }
204 return true;
205 });
206 return res;
207 }
208
209 filterValid(moves, sideBoard) {
210 if (moves.length == 0) return [];
211 if (!sideBoard) sideBoard = [this.getSideBoard(1), this.getSideBoard(2)];
212 const color = this.turn;
213 return moves.filter(m => {
214 this.playSide(m, sideBoard); //no need to track flags
215 const res = !this.underCheck(color, sideBoard);
216 this.undoSide(m, sideBoard);
217 return res;
218 });
219 }
220
221 getAllValidMoves() {
222 const color = this.turn;
223 let potentialMoves = [];
224 const sideBoard = [this.getSideBoard(1), this.getSideBoard(2)];
225 for (var i = 0; i < V.size.x; i++) {
226 for (var j = 0; j < V.size.y; j++) {
227 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
228 Array.prototype.push.apply(
229 potentialMoves,
230 this.getPotentialMovesFrom([i, j], sideBoard)
231 );
232 }
233 }
234 }
235 return this.filterValid(potentialMoves, sideBoard);
236 }
237
238 // Play on sideboards [TODO: only one sideBoard required]
239 playSide(move, sideBoard) {
240 const pieces = Object.keys(V.ALICE_CODES);
241 move.vanish.forEach(psq => {
242 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
243 sideBoard[mirrorSide - 1][psq.x][psq.y] = V.EMPTY;
244 });
245 move.appear.forEach(psq => {
246 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
247 const piece = mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p];
248 sideBoard[mirrorSide - 1][psq.x][psq.y] = psq.c + piece;
249 if (piece == V.KING) this.kingPos[psq.c] = [psq.x, psq.y];
250 });
251 }
252
253 // Undo on sideboards
254 undoSide(move, sideBoard) {
255 const pieces = Object.keys(V.ALICE_CODES);
256 move.appear.forEach(psq => {
257 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
258 sideBoard[mirrorSide - 1][psq.x][psq.y] = V.EMPTY;
259 });
260 move.vanish.forEach(psq => {
261 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
262 const piece = mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p];
263 sideBoard[mirrorSide - 1][psq.x][psq.y] = psq.c + piece;
264 if (piece == V.KING) this.kingPos[psq.c] = [psq.x, psq.y];
265 });
266 }
267
268 // sideBoard: arg containing both boards (see getAllValidMoves())
269 underCheck(color, sideBoard) {
270 const kp = this.kingPos[color];
271 const mirrorSide = sideBoard[0][kp[0]][kp[1]] != V.EMPTY ? 1 : 2;
272 let saveBoard = this.board;
273 this.board = sideBoard[mirrorSide - 1];
274 let res = this.isAttacked(kp, [V.GetOppCol(color)]);
275 this.board = saveBoard;
276 return res;
277 }
278
279 getCheckSquares() {
280 const color = this.turn;
281 const pieces = Object.keys(V.ALICE_CODES);
282 const kp = this.kingPos[color];
283 const mirrorSide = pieces.includes(this.getPiece(kp[0], kp[1])) ? 1 : 2;
284 let sideBoard = this.getSideBoard(mirrorSide);
285 let saveBoard = this.board;
286 this.board = sideBoard;
287 let res = this.isAttacked(this.kingPos[color], [V.GetOppCol(color)])
288 ? [JSON.parse(JSON.stringify(this.kingPos[color]))]
289 : [];
290 this.board = saveBoard;
291 return res;
292 }
293
294 postPlay(move) {
295 super.postPlay(move); //standard king
296 const piece = move.vanish[0].p;
297 const c = move.vanish[0].c;
298 // "l" = Alice king
299 if (piece == "l") {
300 this.kingPos[c][0] = move.appear[0].x;
301 this.kingPos[c][1] = move.appear[0].y;
302 this.castleFlags[c] = [8, 8];
303 }
304 }
305
306 postUndo(move) {
307 super.postUndo(move);
308 const c = move.vanish[0].c;
309 if (move.vanish[0].p == "l")
310 this.kingPos[c] = [move.start.x, move.start.y];
311 }
312
313 getCurrentScore() {
314 if (this.atLeastOneMove()) return "*";
315 const pieces = Object.keys(V.ALICE_CODES);
316 const color = this.turn;
317 const kp = this.kingPos[color];
318 const mirrorSide = pieces.includes(this.getPiece(kp[0], kp[1])) ? 1 : 2;
319 let sideBoard = this.getSideBoard(mirrorSide);
320 let saveBoard = this.board;
321 this.board = sideBoard;
322 let res = "*";
323 if (!this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]))
324 res = "1/2";
325 else res = color == "w" ? "0-1" : "1-0";
326 this.board = saveBoard;
327 return res;
328 }
329
330 static get VALUES() {
331 return Object.assign(
332 {
333 s: 1,
334 u: 5,
335 o: 3,
336 c: 3,
337 t: 9,
338 l: 1000
339 },
340 ChessRules.VALUES
341 );
342 }
343
344 static get SEARCH_DEPTH() {
345 return 2;
346 }
347
348 getNotation(move) {
349 if (move.appear.length == 2 && move.appear[0].p == V.KING) {
350 if (move.end.y < move.start.y) return "0-0-0";
351 return "0-0";
352 }
353
354 const finalSquare = V.CoordsToSquare(move.end);
355 const piece = this.getPiece(move.start.x, move.start.y);
356
357 const captureMark = move.vanish.length > move.appear.length ? "x" : "";
358 let pawnMark = "";
359 if (["p", "s"].includes(piece) && captureMark.length == 1)
360 pawnMark = V.CoordToColumn(move.start.y); //start column
361
362 // Piece or pawn movement
363 let notation = piece.toUpperCase() + pawnMark + captureMark + finalSquare;
364 if (["s", "p"].includes(piece) && !["s", "p"].includes(move.appear[0].p))
365 // Promotion
366 notation += "=" + move.appear[0].p.toUpperCase();
367 return notation;
368 }
369 };