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