Fix typo for en passant captures
[vchess.git] / client / src / variants / Omega.js
CommitLineData
472c0c4f
BA
1import { ChessRules, Move, PiPo } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
3import { randInt } from "@/utils/alea";
4
5export class OmegaRules extends ChessRules {
6 static get PawnSpecs() {
7 return Object.assign(
8 {},
9 ChessRules.PawnSpecs,
10 {
11 initShift: { w: 2, b: 2 },
12 threeSquares: true,
13 promotions:
14 ChessRules.PawnSpecs.promotions.concat([V.CHAMPION, V.WIZARD])
15 }
16 );
17 }
18
19 // For space between corners:
20 static get NOTHING() {
21 return "xx";
22 }
23
24 static board2fen(b) {
25 if (b[0] == 'x') return 'x';
26 return ChessRules.board2fen(b);
27 }
28
29 static fen2board(f) {
30 if (f == 'x') return V.NOTHING;
31 return ChessRules.fen2board(f);
32 }
33
34 getPpath(b) {
35 if (b[0] == 'x') return "Omega/nothing";
36 return ([V.CHAMPION, V.WIZARD].includes(b[1]) ? "Omega/" : "") + b;
37 }
38
c0b036aa
BA
39 static IsGoodPosition(position) {
40 if (position.length == 0) return false;
41 const rows = position.split("/");
42 if (rows.length != V.size.x) return false;
43 let kings = { "k": 0, "K": 0 };
44 for (let row of rows) {
45 let sumElts = 0;
46 for (let i = 0; i < row.length; i++) {
47 if (['K','k'].includes(row[i])) kings[row[i]]++;
48 if (['x'].concat(V.PIECES).includes(row[i].toLowerCase())) sumElts++;
49 else {
50 const num = parseInt(row[i]);
51 if (isNaN(num)) return false;
52 sumElts += num;
53 }
54 }
55 if (sumElts != V.size.y) return false;
56 }
57 if (Object.values(kings).some(v => v != 1)) return false;
58 return true;
59 }
60
472c0c4f
BA
61 // NOTE: keep this extensive check because the board has holes
62 static IsGoodEnpassant(enpassant) {
63 if (enpassant != "-") {
64 const squares = enpassant.split(",");
65 if (squares.length > 2) return false;
66 for (let sq of squares) {
67 const ep = V.SquareToCoords(sq);
68 if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
69 }
70 }
71 return true;
72 }
73
74 static get size() {
75 return { x: 12, y: 12 };
76 }
77
78 static OnBoard(x, y) {
79 return (
80 (x >= 1 && x <= 10 && y >= 1 && y <= 10) ||
81 (x == 11 && [0, 11].includes(y)) ||
82 (x == 0 && [0, 11].includes(y))
83 );
84 }
85
86 // Dabbabah + alfil + wazir
87 static get CHAMPION() {
88 return "c";
89 }
90
91 // Camel + ferz
92 static get WIZARD() {
93 return "w";
94 }
95
96 static get PIECES() {
97 return ChessRules.PIECES.concat([V.CHAMPION, V.WIZARD]);
98 }
99
100 static get steps() {
101 return Object.assign(
102 {},
103 ChessRules.steps,
104 {
105 w: [
106 [-3, -1],
107 [-3, 1],
108 [-1, -3],
109 [-1, 3],
110 [1, -3],
111 [1, 3],
112 [3, -1],
113 [3, 1],
114 [-1, -1],
115 [-1, 1],
116 [1, -1],
117 [1, 1]
118 ],
119 c: [
120 [1, 0],
121 [-1, 0],
122 [0, 1],
123 [0, -1],
124 [2, 2],
125 [2, -2],
126 [-2, 2],
127 [-2, -2],
128 [-2, 0],
129 [0, -2],
130 [2, 0],
131 [0, 2]
132 ]
133 }
134 );
135 }
136
137 static GenRandInitFen(randomness) {
138 if (randomness == 0) {
139 return (
140 "wxxxxxxxxxxw/xcrnbqkbnrcx/xppppppppppx/x91x/x91x/x91x/" +
141 "x91x/x91x/x91x/xPPPPPPPPPPx/xCRNBQKBNRCx/WxxxxxxxxxxW " +
142 "w 0 cjcj -"
143 );
144 }
145
146 let pieces = { w: new Array(10), b: new Array(10) };
147 let flags = "";
148 // Shuffle pieces on first (and last rank if randomness == 2)
149 for (let c of ["w", "b"]) {
150 if (c == 'b' && randomness == 1) {
151 pieces['b'] = pieces['w'];
152 flags += flags;
153 break;
154 }
155
156 let positions = ArrayFun.range(10);
157
158 // Get random squares for bishops
159 let randIndex = 2 * randInt(5);
160 const bishop1Pos = positions[randIndex];
161 // The second bishop must be on a square of different color
162 let randIndex_tmp = 2 * randInt(5) + 1;
163 const bishop2Pos = positions[randIndex_tmp];
164 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
165 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
166
167 // Get random squares for champions
168 randIndex = 2 * randInt(4);
169 let bishopSameColorPos = (bishop1Pos % 2 == 0 ? bishop1Pos : bishop2Pos);
170 if (randIndex >= bishopSameColorPos) randIndex += 2;
171 const champion1Pos = positions[randIndex];
172 // The second champion must be on a square of different color
173 randIndex_tmp = 2 * randInt(4) + 1;
174 bishopSameColorPos = (bishop1Pos % 2 == 0 ? bishop1Pos : bishop2Pos);
175 if (randIndex_tmp >= bishopSameColorPos) randIndex_tmp += 2;
176 const champion2Pos = positions[randIndex_tmp];
177 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
178 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
179
180 // Get random squares for other pieces
181 randIndex = randInt(6);
182 const knight1Pos = positions[randIndex];
183 positions.splice(randIndex, 1);
184 randIndex = randInt(5);
185 const knight2Pos = positions[randIndex];
186 positions.splice(randIndex, 1);
187
188 randIndex = randInt(4);
189 const queenPos = positions[randIndex];
190 positions.splice(randIndex, 1);
191
192 // Rooks and king positions are now fixed
193 const rook1Pos = positions[0];
194 const kingPos = positions[1];
195 const rook2Pos = positions[2];
196
197 pieces[c][champion1Pos] = "c";
198 pieces[c][rook1Pos] = "r";
199 pieces[c][knight1Pos] = "n";
200 pieces[c][bishop1Pos] = "b";
201 pieces[c][queenPos] = "q";
202 pieces[c][kingPos] = "k";
203 pieces[c][bishop2Pos] = "b";
204 pieces[c][knight2Pos] = "n";
205 pieces[c][rook2Pos] = "r";
206 pieces[c][champion2Pos] = "c";
207 flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(rook2Pos);
208 }
209 // Add turn + flags + enpassant
210 return (
211 "wxxxxxxxxxxw/" +
212 "x" + pieces["b"].join("") +
213 "x/xppppppppppx/x91x/x91x/x91x/x91x/x91x/x91x/xPPPPPPPPPPx/x" +
214 pieces["w"].join("").toUpperCase() + "x" +
215 "/WxxxxxxxxxxW " +
216 "w 0 " + flags + " -"
217 );
218 }
219
220 // There may be 2 enPassant squares (if pawn jump 3 squares)
221 getEnpassantFen() {
222 const L = this.epSquares.length;
223 if (!this.epSquares[L - 1]) return "-"; //no en-passant
224 let res = "";
225 this.epSquares[L - 1].forEach(sq => {
226 res += V.CoordsToSquare(sq) + ",";
227 });
228 return res.slice(0, -1); //remove last comma
229 }
230
231 // En-passant after 2-sq or 3-sq jumps
232 getEpSquare(moveOrSquare) {
233 if (!moveOrSquare) return undefined;
234 if (typeof moveOrSquare === "string") {
235 const square = moveOrSquare;
236 if (square == "-") return undefined;
237 let res = [];
238 square.split(",").forEach(sq => {
239 res.push(V.SquareToCoords(sq));
240 });
241 return res;
242 }
243 // Argument is a move:
244 const move = moveOrSquare;
245 const [sx, sy, ex] = [move.start.x, move.start.y, move.end.x];
246 if (this.getPiece(sx, sy) == V.PAWN && Math.abs(sx - ex) >= 2) {
247 const step = (ex - sx) / Math.abs(ex - sx);
248 let res = [
249 {
250 x: sx + step,
251 y: sy
252 }
253 ];
254 if (sx + 2 * step != ex) {
255 // 3-squares jump
256 res.push({
257 x: sx + 2 * step,
258 y: sy
259 });
260 }
261 return res;
262 }
263 return undefined; //default
264 }
265
266 getPotentialMovesFrom([x, y]) {
267 switch (this.getPiece(x, y)) {
268 case V.CHAMPION:
269 return this.getPotentialChampionMoves([x, y]);
270 case V.WIZARD:
271 return this.getPotentialWizardMoves([x, y]);
272 default:
273 return super.getPotentialMovesFrom([x, y]);
274 }
275 }
276
f6c6d0c3 277 getEnpassantCaptures([x, y], shiftX) {
472c0c4f
BA
278 const Lep = this.epSquares.length;
279 const epSquare = this.epSquares[Lep - 1];
280 let moves = [];
281 if (!!epSquare) {
282 for (let epsq of epSquare) {
283 // TODO: some redundant checks
284 if (epsq.x == x + shiftX && Math.abs(epsq.y - y) == 1) {
285 let enpassantMove = this.getBasicMove([x, y], [epsq.x, epsq.y]);
286 // WARNING: the captured pawn may be diagonally behind us,
287 // if it's a 3-squares jump and we take on 1st passing square
288 const px = this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX;
289 enpassantMove.vanish.push({
290 x: px,
291 y: epsq.y,
292 p: "p",
293 c: this.getColor(px, epsq.y)
294 });
295 moves.push(enpassantMove);
296 }
297 }
298 }
299 return moves;
300 }
301
302 getPotentialChampionMoves(sq) {
303 return this.getSlideNJumpMoves(sq, V.steps[V.CHAMPION], "oneStep");
304 }
305
306 getPotentialWizardMoves(sq) {
307 return this.getSlideNJumpMoves(sq, V.steps[V.WIZARD], "oneStep");
308 }
309
310 getCastleMoves([x, y], castleInCheck) {
311 const c = this.getColor(x, y);
312 if (x != (c == "w" ? V.size.x - 2 : 1) || y != this.INIT_COL_KING[c])
313 return []; //x isn't first rank, or king has moved (shortcut)
314
315 // Castling ?
316 const oppCol = V.GetOppCol(c);
317 let moves = [];
318 let i = 0;
319 // King, then rook:
320 const finalSquares = [
321 [4, 5],
322 [8, 7]
323 ];
324 castlingCheck: for (
325 let castleSide = 0;
326 castleSide < 2;
327 castleSide++ //large, then small
328 ) {
329 if (this.castleFlags[c][castleSide] >= V.size.y) continue;
330 // If this code is reached, rook and king are on initial position
331
332 // NOTE: in some variants this is not a rook
333 const rookPos = this.castleFlags[c][castleSide];
334 if (this.board[x][rookPos] == V.EMPTY || this.getColor(x, rookPos) != c)
335 // Rook is not here, or changed color (see Benedict)
336 continue;
337
338 // Nothing on the path of the king ? (and no checks)
339 const castlingPiece = this.getPiece(x, rookPos);
340 const finDist = finalSquares[castleSide][0] - y;
341 let step = finDist / Math.max(1, Math.abs(finDist));
342 i = y;
343 do {
344 if (
345 (!castleInCheck && this.isAttacked([x, i], oppCol)) ||
346 (this.board[x][i] != V.EMPTY &&
347 // NOTE: next check is enough, because of chessboard constraints
348 (this.getColor(x, i) != c ||
349 ![V.KING, castlingPiece].includes(this.getPiece(x, i))))
350 ) {
351 continue castlingCheck;
352 }
353 i += step;
354 } while (i != finalSquares[castleSide][0]);
355
356 // Nothing on the path to the rook?
357 step = castleSide == 0 ? -1 : 1;
358 for (i = y + step; i != rookPos; i += step) {
359 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
360 }
361
362 // Nothing on final squares, except maybe king and castling rook?
363 for (i = 0; i < 2; i++) {
364 if (
365 finalSquares[castleSide][i] != rookPos &&
366 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
367 (
368 this.getPiece(x, finalSquares[castleSide][i]) != V.KING ||
369 this.getColor(x, finalSquares[castleSide][i]) != c
370 )
371 ) {
372 continue castlingCheck;
373 }
374 }
375
376 // If this code is reached, castle is valid
377 moves.push(
378 new Move({
379 appear: [
380 new PiPo({
381 x: x,
382 y: finalSquares[castleSide][0],
383 p: V.KING,
384 c: c
385 }),
386 new PiPo({
387 x: x,
388 y: finalSquares[castleSide][1],
389 p: castlingPiece,
390 c: c
391 })
392 ],
393 vanish: [
394 new PiPo({ x: x, y: y, p: V.KING, c: c }),
395 new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
396 ],
397 end:
398 Math.abs(y - rookPos) <= 2
399 ? { x: x, y: rookPos }
400 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
401 })
402 );
403 }
404
405 return moves;
406 }
407
408 isAttacked(sq, color) {
409 return (
410 super.isAttacked(sq, color) ||
411 this.isAttackedByChampion(sq, color) ||
412 this.isAttackedByWizard(sq, color)
413 );
414 }
415
416 isAttackedByWizard(sq, color) {
417 return (
418 this.isAttackedBySlideNJump(
419 sq, color, V.WIZARD, V.steps[V.WIZARD], "oneStep")
420 );
421 }
422
423 isAttackedByChampion(sq, color) {
424 return (
425 this.isAttackedBySlideNJump(
426 sq, color, V.CHAMPION, V.steps[V.CHAMPION], "oneStep")
427 );
428 }
429
430 updateCastleFlags(move, piece) {
431 const c = V.GetOppCol(this.turn);
432 const firstRank = (c == "w" ? V.size.x - 2 : 1);
433 // Update castling flags if rooks are moved
434 const oppCol = this.turn;
435 const oppFirstRank = V.size.x - 1 - firstRank;
436 if (piece == V.KING)
437 this.castleFlags[c] = [V.size.y, V.size.y];
438 else if (
439 move.start.x == firstRank && //our rook moves?
440 this.castleFlags[c].includes(move.start.y)
441 ) {
442 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
443 this.castleFlags[c][flagIdx] = V.size.y;
444 }
445 // NOTE: not "else if" because a rook could take an opposing rook
446 if (
447 move.end.x == oppFirstRank && //we took opponent rook?
448 this.castleFlags[oppCol].includes(move.end.y)
449 ) {
450 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
451 this.castleFlags[oppCol][flagIdx] = V.size.y;
452 }
453 }
454
455 static get SEARCH_DEPTH() {
456 return 2;
457 }
458
459 // Values taken from https://omegachess.com/strategy.htm
460 static get VALUES() {
461 return {
462 p: 1,
463 n: 2,
464 b: 4,
465 r: 6,
466 q: 12,
467 w: 4,
468 c: 4,
469 k: 1000
470 };
471 }
f92d56d6
BA
472
473 evalPosition() {
474 let evaluation = 0;
475 for (let i = 0; i < V.size.x; i++) {
476 for (let j = 0; j < V.size.y; j++) {
477 if (![V.EMPTY,V.NOTHING].includes(this.board[i][j])) {
478 const sign = this.getColor(i, j) == "w" ? 1 : -1;
479 evaluation += sign * V.VALUES[this.getPiece(i, j)];
480 }
481 }
482 }
483 return evaluation;
484 }
472c0c4f 485};