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