57de5683b4b22d01402c77832207f6fe48612832
[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 setOtherVariables(fen) {
64 super.setOtherVariables(fen);
65 const rows = V.ParseFen(fen).position.split("/");
66 if (this.kingPos["w"][0] < 0 || this.kingPos["b"][0] < 0) {
67 // INIT_COL_XXX won't be required if Alice kings are found
68 // (it means 'king moved')
69 for (let i = 0; i < rows.length; i++) {
70 let k = 0; //column index on board
71 for (let j = 0; j < rows[i].length; j++) {
72 switch (rows[i].charAt(j)) {
73 case "l":
74 this.kingPos["b"] = [i, k];
75 break;
76 case "L":
77 this.kingPos["w"] = [i, k];
78 break;
79 default: {
80 const num = parseInt(rows[i].charAt(j));
81 if (!isNaN(num)) k += num - 1;
82 }
83 }
84 k++;
85 }
86 }
87 }
88 }
89
90 // Return the (standard) color+piece notation at a square for a board
91 getSquareOccupation(i, j, mirrorSide) {
92 const piece = this.getPiece(i, j);
93 if (mirrorSide == 1 && Object.keys(V.ALICE_CODES).includes(piece))
94 return this.board[i][j];
95 if (mirrorSide == 2 && Object.keys(V.ALICE_PIECES).includes(piece))
96 return this.getColor(i, j) + V.ALICE_PIECES[piece];
97 return "";
98 }
99
100 // Build board of the given (mirror)side
101 getSideBoard(mirrorSide) {
102 // Build corresponding board from complete board
103 let sideBoard = ArrayFun.init(V.size.x, V.size.y, "");
104 for (let i = 0; i < V.size.x; i++) {
105 for (let j = 0; j < V.size.y; j++)
106 sideBoard[i][j] = this.getSquareOccupation(i, j, mirrorSide);
107 }
108 return sideBoard;
109 }
110
111 // NOTE: castle & enPassant
112 // https://www.chessvariants.com/other.dir/alice.html
113 getPotentialMovesFrom([x, y], sideBoard) {
114 const pieces = Object.keys(V.ALICE_CODES);
115 const codes = Object.keys(V.ALICE_PIECES);
116 const mirrorSide = pieces.includes(this.getPiece(x, y)) ? 1 : 2;
117 if (!sideBoard) sideBoard = [this.getSideBoard(1), this.getSideBoard(2)];
118 const color = this.getColor(x, y);
119
120 // Search valid moves on sideBoard
121 const saveBoard = this.board;
122 this.board = sideBoard[mirrorSide - 1];
123 const moves = super.getPotentialMovesFrom([x, y]).filter(m => {
124 // Filter out king moves which result in under-check position on
125 // current board (before mirror traversing)
126 let aprioriValid = true;
127 if (m.appear[0].p == V.KING) {
128 this.play(m);
129 if (this.underCheck(color, sideBoard)) aprioriValid = false;
130 this.undo(m);
131 }
132 return aprioriValid;
133 });
134 this.board = saveBoard;
135
136 // Finally filter impossible moves
137 const res = moves.filter(m => {
138 if (m.appear.length == 2) {
139 // Castle: appear[i] must be an empty square on the other board
140 for (let psq of m.appear) {
141 if (
142 this.getSquareOccupation(psq.x, psq.y, 3 - mirrorSide) != V.EMPTY
143 ) {
144 return false;
145 }
146 }
147 } else if (this.board[m.end.x][m.end.y] != V.EMPTY) {
148 // Attempt to capture
149 const piece = this.getPiece(m.end.x, m.end.y);
150 if (
151 (mirrorSide == 1 && codes.includes(piece)) ||
152 (mirrorSide == 2 && pieces.includes(piece))
153 ) {
154 return false;
155 }
156 }
157 // If the move is computed on board1, m.appear change for Alice pieces.
158 if (mirrorSide == 1) {
159 m.appear.forEach(psq => {
160 // forEach: castling taken into account
161 psq.p = V.ALICE_CODES[psq.p]; //goto board2
162 });
163 }
164 else {
165 // Move on board2: mark vanishing pieces as Alice
166 m.vanish.forEach(psq => {
167 psq.p = V.ALICE_CODES[psq.p];
168 });
169 }
170 // Fix en-passant captures
171 if (
172 m.vanish[0].p == V.PAWN &&
173 m.vanish.length == 2 &&
174 this.board[m.end.x][m.end.y] == V.EMPTY
175 ) {
176 m.vanish[1].c = V.GetOppCol(this.getColor(x, y));
177 // In the special case of en-passant, if
178 // - board1 takes board2 : vanish[1] --> Alice
179 // - board2 takes board1 : vanish[1] --> normal
180 let van = m.vanish[1];
181 if (mirrorSide == 1 && codes.includes(this.getPiece(van.x, van.y)))
182 van.p = V.ALICE_CODES[van.p];
183 else if (
184 mirrorSide == 2 &&
185 pieces.includes(this.getPiece(van.x, van.y))
186 )
187 van.p = V.ALICE_PIECES[van.p];
188 }
189 return true;
190 });
191 return res;
192 }
193
194 getEnpassantCaptures([x, y], shiftX) {
195 const Lep = this.epSquares.length;
196 const epSquare = this.epSquares[Lep - 1]; //always at least one element
197 let enpassantMove = null;
198 if (
199 !!epSquare &&
200 epSquare.x == x + shiftX &&
201 Math.abs(epSquare.y - y) == 1
202 ) {
203 enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
204 // May capture in same world or different:
205 const capturedPiece =
206 this.board[x][epSquare.y] != V.EMPTY
207 ? this.getPiece(x, epSquare.y)
208 : ['p','s'][1 - "ps".indexOf(this.getPiece(x, y))];
209 enpassantMove.vanish.push({
210 x: x,
211 y: epSquare.y,
212 p: capturedPiece,
213 c: V.GetOppCol(this.turn)
214 });
215 }
216 return !!enpassantMove ? [enpassantMove] : [];
217 }
218
219 filterValid(moves, sideBoard) {
220 if (moves.length == 0) return [];
221 if (!sideBoard) sideBoard = [this.getSideBoard(1), this.getSideBoard(2)];
222 const color = this.turn;
223 return moves.filter(m => {
224 this.playSide(m, sideBoard); //no need to track flags
225 const res = !this.underCheck(color, sideBoard);
226 this.undoSide(m, sideBoard);
227 return res;
228 });
229 }
230
231 getAllValidMoves() {
232 const color = this.turn;
233 let potentialMoves = [];
234 const sideBoard = [this.getSideBoard(1), this.getSideBoard(2)];
235 for (var i = 0; i < V.size.x; i++) {
236 for (var j = 0; j < V.size.y; j++) {
237 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
238 Array.prototype.push.apply(
239 potentialMoves,
240 this.getPotentialMovesFrom([i, j], sideBoard)
241 );
242 }
243 }
244 }
245 return this.filterValid(potentialMoves, sideBoard);
246 }
247
248 // Play on sideboards [TODO: only one sideBoard required]
249 playSide(move, sideBoard) {
250 const pieces = Object.keys(V.ALICE_CODES);
251 move.vanish.forEach(psq => {
252 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
253 sideBoard[mirrorSide - 1][psq.x][psq.y] = V.EMPTY;
254 });
255 move.appear.forEach(psq => {
256 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
257 const piece = mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p];
258 sideBoard[mirrorSide - 1][psq.x][psq.y] = psq.c + piece;
259 if (piece == V.KING) this.kingPos[psq.c] = [psq.x, psq.y];
260 });
261 }
262
263 // Undo on sideboards
264 undoSide(move, sideBoard) {
265 const pieces = Object.keys(V.ALICE_CODES);
266 move.appear.forEach(psq => {
267 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
268 sideBoard[mirrorSide - 1][psq.x][psq.y] = V.EMPTY;
269 });
270 move.vanish.forEach(psq => {
271 const mirrorSide = pieces.includes(psq.p) ? 1 : 2;
272 const piece = mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p];
273 sideBoard[mirrorSide - 1][psq.x][psq.y] = psq.c + piece;
274 if (piece == V.KING) this.kingPos[psq.c] = [psq.x, psq.y];
275 });
276 }
277
278 // sideBoard: arg containing both boards (see getAllValidMoves())
279 underCheck(color, sideBoard) {
280 const kp = this.kingPos[color];
281 const mirrorSide = sideBoard[0][kp[0]][kp[1]] != V.EMPTY ? 1 : 2;
282 let saveBoard = this.board;
283 this.board = sideBoard[mirrorSide - 1];
284 let res = this.isAttacked(kp, [V.GetOppCol(color)]);
285 this.board = saveBoard;
286 return res;
287 }
288
289 getCheckSquares() {
290 const color = this.turn;
291 const pieces = Object.keys(V.ALICE_CODES);
292 const kp = this.kingPos[color];
293 const mirrorSide = pieces.includes(this.getPiece(kp[0], kp[1])) ? 1 : 2;
294 let sideBoard = this.getSideBoard(mirrorSide);
295 let saveBoard = this.board;
296 this.board = sideBoard;
297 let res = this.isAttacked(this.kingPos[color], [V.GetOppCol(color)])
298 ? [JSON.parse(JSON.stringify(this.kingPos[color]))]
299 : [];
300 this.board = saveBoard;
301 return res;
302 }
303
304 postPlay(move) {
305 super.postPlay(move); //standard king
306 const piece = move.vanish[0].p;
307 const c = move.vanish[0].c;
308 // "l" = Alice king
309 if (piece == "l") {
310 this.kingPos[c][0] = move.appear[0].x;
311 this.kingPos[c][1] = move.appear[0].y;
312 this.castleFlags[c] = [8, 8];
313 }
314 }
315
316 postUndo(move) {
317 super.postUndo(move);
318 const c = move.vanish[0].c;
319 if (move.vanish[0].p == "l")
320 this.kingPos[c] = [move.start.x, move.start.y];
321 }
322
323 getCurrentScore() {
324 if (this.atLeastOneMove()) return "*";
325 const pieces = Object.keys(V.ALICE_CODES);
326 const color = this.turn;
327 const kp = this.kingPos[color];
328 const mirrorSide = pieces.includes(this.getPiece(kp[0], kp[1])) ? 1 : 2;
329 let sideBoard = this.getSideBoard(mirrorSide);
330 let saveBoard = this.board;
331 this.board = sideBoard;
332 let res = "*";
333 if (!this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]))
334 res = "1/2";
335 else res = color == "w" ? "0-1" : "1-0";
336 this.board = saveBoard;
337 return res;
338 }
339
340 static get VALUES() {
341 return Object.assign(
342 {
343 s: 1,
344 u: 5,
345 o: 3,
346 c: 3,
347 t: 9,
348 l: 1000
349 },
350 ChessRules.VALUES
351 );
352 }
353
354 static get SEARCH_DEPTH() {
355 return 2;
356 }
357
358 getNotation(move) {
359 if (move.appear.length == 2 && move.appear[0].p == V.KING) {
360 if (move.end.y < move.start.y) return "0-0-0";
361 return "0-0";
362 }
363
364 const finalSquare = V.CoordsToSquare(move.end);
365 const piece = this.getPiece(move.start.x, move.start.y);
366
367 const captureMark = move.vanish.length > move.appear.length ? "x" : "";
368 let pawnMark = "";
369 if (["p", "s"].includes(piece) && captureMark.length == 1)
370 pawnMark = V.CoordToColumn(move.start.y); //start column
371
372 // Piece or pawn movement
373 let notation = piece.toUpperCase() + pawnMark + captureMark + finalSquare;
374 if (["s", "p"].includes(piece) && !["s", "p"].includes(move.appear[0].p)) {
375 // Promotion
376 notation += "=" + move.appear[0].p.toUpperCase();
377 }
378 return notation;
379 }
380 };