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