Fix Koopa promotions with captures, and Balakhlava: pawns move forward
[vchess.git] / client / src / variants / Wormhole.js
1 import { ChessRules } from "@/base_rules";
2
3 export class WormholeRules extends ChessRules {
4 static get HasFlags() {
5 return false;
6 }
7
8 static get HasEnpassant() {
9 return false;
10 }
11
12 static get HOLE() {
13 return "xx";
14 }
15
16 static board2fen(b) {
17 if (b[0] == 'x') return 'x';
18 return ChessRules.board2fen(b);
19 }
20
21 static fen2board(f) {
22 if (f == 'x') return V.HOLE;
23 return ChessRules.fen2board(f);
24 }
25
26 getPpath(b) {
27 if (b[0] == 'x') return "Wormhole/hole";
28 return b;
29 }
30
31 static IsGoodPosition(position) {
32 if (position.length == 0) return false;
33 const rows = position.split("/");
34 if (rows.length != V.size.x) return false;
35 let kings = { "k": 0, "K": 0 };
36 for (let row of rows) {
37 let sumElts = 0;
38 for (let i = 0; i < row.length; i++) {
39 if (['K','k'].includes(row[i])) kings[row[i]]++;
40 if (['x'].concat(V.PIECES).includes(row[i].toLowerCase())) sumElts++;
41 else {
42 const num = parseInt(row[i], 10);
43 if (isNaN(num)) return false;
44 sumElts += num;
45 }
46 }
47 if (sumElts != V.size.y) return false;
48 }
49 if (Object.values(kings).some(v => v != 1)) return false;
50 return true;
51 }
52
53 getSquareAfter(square, movement) {
54 let shift1, shift2;
55 if (Array.isArray(movement[0])) {
56 // A knight
57 shift1 = movement[0];
58 shift2 = movement[1];
59 } else {
60 shift1 = movement;
61 shift2 = null;
62 }
63 const tryMove = (init, shift) => {
64 let step = [
65 shift[0] / Math.abs(shift[0]) || 0,
66 shift[1] / Math.abs(shift[1]) || 0,
67 ];
68 const nbSteps = Math.max(Math.abs(shift[0]), Math.abs(shift[1]));
69 let stepsAchieved = 0;
70 let sq = [init[0] + step[0], init[1] + step[1]];
71 while (V.OnBoard(sq[0],sq[1])) {
72 if (this.board[sq[0]][sq[1]] != V.HOLE)
73 stepsAchieved++;
74 if (stepsAchieved < nbSteps) {
75 sq[0] += step[0];
76 sq[1] += step[1];
77 }
78 else break;
79 }
80 if (stepsAchieved < nbSteps)
81 // The move is impossible
82 return null;
83 return sq;
84 };
85 // First, apply shift1
86 let dest = tryMove(square, shift1);
87 if (dest && shift2)
88 // A knight: apply second shift
89 dest = tryMove(dest, shift2);
90 return dest;
91 }
92
93 // NOTE (TODO?): some extra work done in some function because informations
94 // on one step should ease the computation for a step in the same direction.
95 static get steps() {
96 return {
97 r: [
98 [-1, 0],
99 [1, 0],
100 [0, -1],
101 [0, 1],
102 [-2, 0],
103 [2, 0],
104 [0, -2],
105 [0, 2]
106 ],
107 // Decompose knight movements into one step orthogonal + one diagonal
108 n: [
109 [[0, -1], [-1, -1]],
110 [[0, -1], [1, -1]],
111 [[-1, 0], [-1,-1]],
112 [[-1, 0], [-1, 1]],
113 [[0, 1], [-1, 1]],
114 [[0, 1], [1, 1]],
115 [[1, 0], [1, -1]],
116 [[1, 0], [1, 1]]
117 ],
118 b: [
119 [-1, -1],
120 [-1, 1],
121 [1, -1],
122 [1, 1],
123 [-2, -2],
124 [-2, 2],
125 [2, -2],
126 [2, 2]
127 ],
128 k: [
129 [-1, 0],
130 [1, 0],
131 [0, -1],
132 [0, 1],
133 [-1, -1],
134 [-1, 1],
135 [1, -1],
136 [1, 1]
137 ]
138 };
139 }
140
141 getJumpMoves([x, y], steps) {
142 let moves = [];
143 for (let step of steps) {
144 const sq = this.getSquareAfter([x,y], step);
145 if (sq &&
146 (
147 this.board[sq[0]][sq[1]] == V.EMPTY ||
148 this.canTake([x, y], sq)
149 )
150 ) {
151 moves.push(this.getBasicMove([x, y], sq));
152 }
153 }
154 return moves;
155 }
156
157 // What are the pawn moves from square x,y ?
158 getPotentialPawnMoves([x, y]) {
159 const color = this.turn;
160 let moves = [];
161 const [sizeX, sizeY] = [V.size.x, V.size.y];
162 const shiftX = color == "w" ? -1 : 1;
163 const startRank = color == "w" ? sizeX - 2 : 1;
164 const lastRank = color == "w" ? 0 : sizeX - 1;
165
166 const sq1 = this.getSquareAfter([x,y], [shiftX,0]);
167 if (sq1 && this.board[sq1[0]][y] == V.EMPTY) {
168 // One square forward (cannot be a promotion)
169 moves.push(this.getBasicMove([x, y], [sq1[0], y]));
170 if (x == startRank) {
171 // If two squares after is available, then move is possible
172 const sq2 = this.getSquareAfter([x,y], [2*shiftX,0]);
173 if (sq2 && this.board[sq2[0]][y] == V.EMPTY)
174 // Two squares jump
175 moves.push(this.getBasicMove([x, y], [sq2[0], y]));
176 }
177 }
178 // Captures
179 for (let shiftY of [-1, 1]) {
180 const sq = this.getSquareAfter([x,y], [shiftX,shiftY]);
181 if (
182 !!sq &&
183 this.board[sq[0]][sq[1]] != V.EMPTY &&
184 this.canTake([x, y], [sq[0], sq[1]])
185 ) {
186 const finalPieces = sq[0] == lastRank
187 ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
188 : [V.PAWN];
189 for (let piece of finalPieces) {
190 moves.push(
191 this.getBasicMove([x, y], [sq[0], sq[1]], {
192 c: color,
193 p: piece
194 })
195 );
196 }
197 }
198 }
199
200 return moves;
201 }
202
203 getPotentialRookMoves(sq) {
204 return this.getJumpMoves(sq, V.steps[V.ROOK]);
205 }
206
207 getPotentialKnightMoves(sq) {
208 return this.getJumpMoves(sq, V.steps[V.KNIGHT]);
209 }
210
211 getPotentialBishopMoves(sq) {
212 return this.getJumpMoves(sq, V.steps[V.BISHOP]);
213 }
214
215 getPotentialQueenMoves(sq) {
216 return this.getJumpMoves(
217 sq,
218 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
219 );
220 }
221
222 getPotentialKingMoves(sq) {
223 return this.getJumpMoves(sq, V.steps[V.KING]);
224 }
225
226 isAttackedByJump([x, y], color, piece, steps) {
227 for (let step of steps) {
228 const sq = this.getSquareAfter([x,y], step);
229 if (
230 sq &&
231 this.getPiece(sq[0], sq[1]) == piece &&
232 this.getColor(sq[0], sq[1]) == color
233 ) {
234 return true;
235 }
236 }
237 return false;
238 }
239
240 isAttackedByPawn([x, y], color) {
241 const pawnShift = (color == "w" ? 1 : -1);
242 for (let i of [-1, 1]) {
243 const sq = this.getSquareAfter([x,y], [pawnShift,i]);
244 if (
245 sq &&
246 this.getPiece(sq[0], sq[1]) == V.PAWN &&
247 this.getColor(sq[0], sq[1]) == color
248 ) {
249 return true;
250 }
251 }
252 return false;
253 }
254
255 isAttackedByRook(sq, color) {
256 return this.isAttackedByJump(sq, color, V.ROOK, V.steps[V.ROOK]);
257 }
258
259 isAttackedByKnight(sq, color) {
260 // NOTE: knight attack is not symmetric in this variant:
261 // steps order need to be reversed.
262 return this.isAttackedByJump(
263 sq,
264 color,
265 V.KNIGHT,
266 V.steps[V.KNIGHT].map(s => s.reverse())
267 );
268 }
269
270 isAttackedByBishop(sq, color) {
271 return this.isAttackedByJump(sq, color, V.BISHOP, V.steps[V.BISHOP]);
272 }
273
274 isAttackedByQueen(sq, color) {
275 return this.isAttackedByJump(
276 sq,
277 color,
278 V.QUEEN,
279 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
280 );
281 }
282
283 isAttackedByKing(sq, color) {
284 return this.isAttackedByJump(sq, color, V.KING, V.steps[V.KING]);
285 }
286
287 // NOTE: altering move in getBasicMove doesn't work and wouldn't be logical.
288 // This is a side-effect on board generated by the move.
289 static PlayOnBoard(board, move) {
290 board[move.vanish[0].x][move.vanish[0].y] = V.HOLE;
291 for (let psq of move.appear) board[psq.x][psq.y] = psq.c + psq.p;
292 }
293
294 getCurrentScore() {
295 if (this.atLeastOneMove()) return "*";
296 // No valid move: I lose
297 return this.turn == "w" ? "0-1" : "1-0";
298 }
299
300 static get SEARCH_DEPTH() {
301 return 2;
302 }
303
304 evalPosition() {
305 let evaluation = 0;
306 for (let i = 0; i < V.size.x; i++) {
307 for (let j = 0; j < V.size.y; j++) {
308 if (![V.EMPTY,V.HOLE].includes(this.board[i][j])) {
309 const sign = this.getColor(i, j) == "w" ? 1 : -1;
310 evaluation += sign * V.VALUES[this.getPiece(i, j)];
311 }
312 }
313 }
314 return evaluation;
315 }
316
317 getNotation(move) {
318 const piece = this.getPiece(move.start.x, move.start.y);
319 // Indicate start square + dest square, because holes distort the board
320 let notation =
321 (piece != V.PAWN ? piece.toUpperCase() : "") +
322 V.CoordsToSquare(move.start) +
323 (move.vanish.length > move.appear.length ? "x" : "") +
324 V.CoordsToSquare(move.end);
325 if (piece == V.PAWN && move.appear[0].p != V.PAWN)
326 // Promotion
327 notation += "=" + move.appear[0].p.toUpperCase();
328 return notation;
329 }
330 };