Add Twokings variant
[vchess.git] / client / src / variants / Coregal.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { randInt, sample } from "@/utils/alea";
4
5 export class CoregalRules extends ChessRules {
6 static IsGoodPosition(position) {
7 if (!super.IsGoodPosition(position)) return false;
8 const rows = position.split("/");
9 // Check that at least one queen of each color is there:
10 let queens = {};
11 for (let row of rows) {
12 for (let i = 0; i < row.length; i++)
13 if (['Q','q'].includes(row[i])) queens[row[i]] = true;
14 }
15 if (Object.keys(queens).length != 2) return false;
16 return true;
17 }
18
19 static IsGoodFlags(flags) {
20 return !!flags.match(/^[a-z]{8,8}$/);
21 }
22
23 // Scanning king position for faster updates is still interesting,
24 // but no need for INIT_COL_KING because it's given in castle flags.
25 scanKings(fen) {
26 this.kingPos = { w: [-1, -1], b: [-1, -1] };
27 const fenRows = V.ParseFen(fen).position.split("/");
28 const startRow = { 'w': V.size.x - 1, 'b': 0 };
29 for (let i = 0; i < fenRows.length; i++) {
30 let k = 0;
31 for (let j = 0; j < fenRows[i].length; j++) {
32 switch (fenRows[i].charAt(j)) {
33 case "k":
34 this.kingPos["b"] = [i, k];
35 break;
36 case "K":
37 this.kingPos["w"] = [i, k];
38 break;
39 default: {
40 const num = parseInt(fenRows[i].charAt(j));
41 if (!isNaN(num)) k += num - 1;
42 }
43 }
44 k++;
45 }
46 }
47 }
48
49 getCheckSquares(color) {
50 let squares = [];
51 const oppCol = V.GetOppCol(color);
52 if (this.isAttacked(this.kingPos[color], oppCol))
53 squares.push(JSON.parse(JSON.stringify(this.kingPos[color])));
54 for (let i=0; i<V.size.x; i++) {
55 for (let j=0; j<V.size.y; j++) {
56 if (
57 this.getColor(i, j) == color &&
58 this.getPiece(i, j) == V.QUEEN &&
59 this.isAttacked([i, j], oppCol)
60 ) {
61 squares.push([i, j]);
62 }
63 }
64 }
65 return squares;
66 }
67
68 static GenRandInitFen(randomness) {
69 if (randomness == 0)
70 // Castle flags here indicate pieces positions (if can castle)
71 return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 adehadeh -";
72
73 let pieces = { w: new Array(8), b: new Array(8) };
74 let flags = "";
75 for (let c of ["w", "b"]) {
76 if (c == 'b' && randomness == 1) {
77 pieces['b'] = pieces['w'];
78 flags += flags;
79 break;
80 }
81
82 // Get random squares for king and queen between b and g files
83 let randIndex = randInt(6);
84 let kingPos = randIndex + 1;
85 randIndex = randInt(5);
86 if (randIndex >= kingPos) randIndex++;
87 let queenPos = randIndex + 1;
88
89 // Get random squares for rooks to the left and right of the queen
90 // and king: not all squares of the same colors (for bishops).
91 const minQR = Math.min(kingPos, queenPos);
92 const maxQR = Math.max(kingPos, queenPos);
93 let rook1Pos = randInt(minQR);
94 let rook2Pos = 7 - randInt(7 - maxQR);
95
96 // Now, if we are unlucky all these 4 pieces may be on the same color.
97 const rem2 = [kingPos, queenPos, rook1Pos, rook2Pos].map(pos => pos % 2);
98 if (rem2.every(r => r == 0) || rem2.every(r => r == 1)) {
99 // Shift a random of these pieces to the left or right
100 switch (randInt(4)) {
101 case 0:
102 if (rook1Pos == 0) rook1Pos++;
103 else rook1Pos--;
104 break;
105 case 1:
106 if (Math.random() < 0.5) kingPos++;
107 else kingPos--;
108 break;
109 case 2:
110 if (Math.random() < 0.5) queenPos++;
111 else queenPos--;
112 break;
113 case 3:
114 if (rook2Pos == 7) rook2Pos--;
115 else rook2Pos++;
116 break;
117 }
118 }
119 let bishop1Options = { 0: true, 2: true, 4: true, 6: true };
120 let bishop2Options = { 1: true, 3: true, 5: true, 7: true };
121 [kingPos, queenPos, rook1Pos, rook2Pos].forEach(pos => {
122 if (!!bishop1Options[pos]) delete bishop1Options[pos];
123 else if (!!bishop2Options[pos]) delete bishop2Options[pos];
124 });
125 const bishop1Pos = parseInt(sample(Object.keys(bishop1Options), 1)[0]);
126 const bishop2Pos = parseInt(sample(Object.keys(bishop2Options), 1)[0]);
127
128 // Knights' positions are now determined
129 const forbidden = [
130 kingPos, queenPos, rook1Pos, rook2Pos, bishop1Pos, bishop2Pos
131 ];
132 const [knight1Pos, knight2Pos] =
133 ArrayFun.range(8).filter(pos => !forbidden.includes(pos));
134
135 pieces[c][rook1Pos] = "r";
136 pieces[c][knight1Pos] = "n";
137 pieces[c][bishop1Pos] = "b";
138 pieces[c][queenPos] = "q";
139 pieces[c][kingPos] = "k";
140 pieces[c][bishop2Pos] = "b";
141 pieces[c][knight2Pos] = "n";
142 pieces[c][rook2Pos] = "r";
143 flags +=
144 [rook1Pos, queenPos, kingPos, rook2Pos].sort().map(V.CoordToColumn).join("");
145 }
146 // Add turn + flags + enpassant
147 return (
148 pieces["b"].join("") +
149 "/pppppppp/8/8/8/8/PPPPPPPP/" +
150 pieces["w"].join("").toUpperCase() +
151 " w 0 " + flags + " -"
152 );
153 }
154
155 setFlags(fenflags) {
156 // white pieces positions, then black pieces positions
157 this.castleFlags = { w: [...Array(4)], b: [...Array(4)] };
158 for (let i = 0; i < 8; i++) {
159 this.castleFlags[i < 4 ? "w" : "b"][i % 4] =
160 V.ColumnToCoord(fenflags.charAt(i))
161 }
162 }
163
164 getPotentialQueenMoves(sq) {
165 return super.getPotentialQueenMoves(sq).concat(this.getCastleMoves(sq));
166 }
167
168 getCastleMoves([x, y]) {
169 const c = this.getColor(x, y);
170 if (
171 x != (c == "w" ? V.size.x - 1 : 0) ||
172 !this.castleFlags[c].slice(1, 3).includes(y)
173 ) {
174 // x isn't first rank, or piece moved
175 return [];
176 }
177 const castlingPiece = this.getPiece(x, y);
178
179 // Relative position of the selected piece: left or right ?
180 // If left: small castle left, large castle right.
181 // If right: usual situation.
182 const relPos = (this.castleFlags[c][1] == y ? "left" : "right");
183
184 // Castling ?
185 const oppCol = V.GetOppCol(c);
186 let moves = [];
187 let i = 0;
188 // Castling piece, then rook:
189 const finalSquares = {
190 0: (relPos == "left" ? [1, 2] : [2, 3]),
191 3: (relPos == "right" ? [6, 5] : [5, 4])
192 };
193
194 // Left, then right castle:
195 castlingCheck: for (let castleSide of [0, 3]) {
196 if (this.castleFlags[c][castleSide] >= 8) continue;
197
198 // Rook and castling piece are on initial position
199 const rookPos = this.castleFlags[c][castleSide];
200
201 // Nothing on the path of the king ? (and no checks)
202 const finDist = finalSquares[castleSide][0] - y;
203 let step = finDist / Math.max(1, Math.abs(finDist));
204 i = y;
205 do {
206 if (
207 this.isAttacked([x, i], oppCol) ||
208 (this.board[x][i] != V.EMPTY &&
209 // NOTE: next check is enough, because of chessboard constraints
210 (this.getColor(x, i) != c ||
211 ![castlingPiece, V.ROOK].includes(this.getPiece(x, i))))
212 ) {
213 continue castlingCheck;
214 }
215 i += step;
216 } while (i != finalSquares[castleSide][0]);
217
218 // Nothing on the path to the rook?
219 step = castleSide == 0 ? -1 : 1;
220 for (i = y + step; i != rookPos; i += step) {
221 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
222 }
223
224 // Nothing on final squares, except maybe castling piece and rook?
225 for (i = 0; i < 2; i++) {
226 if (
227 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
228 ![y, rookPos].includes(finalSquares[castleSide][i])
229 ) {
230 continue castlingCheck;
231 }
232 }
233
234 // If this code is reached, castle is valid
235 moves.push(
236 new Move({
237 appear: [
238 new PiPo({ x: x, y: finalSquares[castleSide][0], p: castlingPiece, c: c }),
239 new PiPo({ x: x, y: finalSquares[castleSide][1], p: V.ROOK, c: c })
240 ],
241 vanish: [
242 new PiPo({ x: x, y: y, p: castlingPiece, c: c }),
243 new PiPo({ x: x, y: rookPos, p: V.ROOK, c: c })
244 ],
245 // In this variant, always castle by playing onto the rook
246 end: { x: x, y: rookPos }
247 })
248 );
249 }
250
251 return moves;
252 }
253
254 underCheck(color) {
255 const oppCol = V.GetOppCol(color);
256 if (this.isAttacked(this.kingPos[color], oppCol)) return true;
257 for (let i=0; i<V.size.x; i++) {
258 for (let j=0; j<V.size.y; j++) {
259 if (
260 this.getColor(i, j) == color &&
261 this.getPiece(i, j) == V.QUEEN &&
262 this.isAttacked([i, j], oppCol)
263 ) {
264 return true;
265 }
266 }
267 }
268 return false;
269 }
270
271 // "twoKings" arg for the similar Twokings variant.
272 updateCastleFlags(move, piece, twoKings) {
273 const c = V.GetOppCol(this.turn);
274 const firstRank = (c == "w" ? V.size.x - 1 : 0);
275 // Update castling flags if castling pieces moved or were captured
276 const oppCol = V.GetOppCol(c);
277 const oppFirstRank = V.size.x - 1 - firstRank;
278 if (move.start.x == firstRank) {
279 if (piece == V.KING || (!twoKings && piece == V.QUEEN)) {
280 if (this.castleFlags[c][1] == move.start.y)
281 this.castleFlags[c][1] = 8;
282 else if (this.castleFlags[c][2] == move.start.y)
283 this.castleFlags[c][2] = 8;
284 // Else: the flag is already turned off
285 }
286 }
287 else if (
288 move.start.x == firstRank && //our rook moves?
289 [this.castleFlags[c][0], this.castleFlags[c][3]].includes(move.start.y)
290 ) {
291 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 3);
292 this.castleFlags[c][flagIdx] = 8;
293 } else if (
294 move.end.x == oppFirstRank && //we took opponent rook?
295 [this.castleFlags[oppCol][0], this.castleFlags[oppCol][3]].includes(move.end.y)
296 ) {
297 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 3);
298 this.castleFlags[oppCol][flagIdx] = 8;
299 }
300 }
301
302 // NOTE: do not set queen value to 1000 or so, because there may be several.
303
304 static get SEARCH_DEPTH() {
305 return 2;
306 }
307
308 getNotation(move) {
309 if (move.appear.length == 2) {
310 // Castle: determine the right notation
311 const color = move.appear[0].c;
312 let symbol = (move.appear[0].p == V.QUEEN ? "Q" : "") + "0-0";
313 if (
314 (
315 this.castleFlags[color][1] == move.vanish[0].y &&
316 move.end.y > move.start.y
317 )
318 ||
319 (
320 this.castleFlags[color][2] == move.vanish[0].y &&
321 move.end.y < move.start.y
322 )
323 ) {
324 symbol += "-0";
325 }
326 return symbol;
327 }
328 return super.getNotation(move);
329 }
330 };