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