Fix Koopa promotions with captures, and Balakhlava: pawns move forward
[vchess.git] / client / src / variants / Sittuyin.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class SittuyinRules extends ChessRules {
5 static get HasFlags() {
6 return false;
7 }
8
9 static get HasEnpassant() {
10 return false;
11 }
12
13 static get Monochrome() {
14 return true;
15 }
16
17 static get Notoodark() {
18 return true;
19 }
20
21 static get Lines() {
22 return ChessRules.Lines.concat([
23 [[0, 0], [8, 8]],
24 [[0, 8], [8, 0]]
25 ]);
26 }
27
28 static get PawnSpecs() {
29 return Object.assign(
30 {},
31 ChessRules.PawnSpecs,
32 {
33 // Promotions are handled differently here
34 promotions: [V.QUEEN]
35 }
36 );
37 }
38
39 static GenRandInitFen() {
40 return "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8 w 0";
41 }
42
43 re_setReserve(subTurn) {
44 const mc = this.movesCount;
45 const wc = (mc == 0 ? 1 : 0);
46 const bc = (mc <= 1 ? 1 : 0);
47 this.reserve = {
48 w: {
49 [V.ROOK]: wc * 2,
50 [V.KNIGHT]: wc * 2,
51 [V.BISHOP]: wc * 2,
52 [V.QUEEN]: wc,
53 [V.KING]: wc
54 },
55 b: {
56 [V.ROOK]: bc * 2,
57 [V.KNIGHT]: bc * 2,
58 [V.BISHOP]: bc * 2,
59 [V.QUEEN]: bc,
60 [V.KING]: bc
61 }
62 }
63 this.subTurn = subTurn || 1;
64 }
65
66 setOtherVariables(fen) {
67 super.setOtherVariables(fen);
68 if (this.movesCount <= 1) this.re_setReserve();
69 }
70
71 getPpath(b) {
72 return "Sittuyin/" + b;
73 }
74
75 getColor(i, j) {
76 if (i >= V.size.x) return i == V.size.x ? "w" : "b";
77 return this.board[i][j].charAt(0);
78 }
79
80 getPiece(i, j) {
81 if (i >= V.size.x) return V.RESERVE_PIECES[j];
82 return this.board[i][j].charAt(1);
83 }
84
85 getReservePpath(index, color) {
86 return "Sittuyin/" + color + V.RESERVE_PIECES[index];
87 }
88
89 static get RESERVE_PIECES() {
90 return [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.KING];
91 }
92
93 getPotentialMovesFrom([x, y]) {
94 if (this.movesCount >= 2) return super.getPotentialMovesFrom([x, y]);
95 // Only reserve moves are allowed for now:
96 if (V.OnBoard(x, y)) return [];
97 const color = this.turn;
98 const p = V.RESERVE_PIECES[y];
99 if (this.reserve[color][p] == 0) return [];
100 const iBound =
101 p != V.ROOK
102 ? (color == 'w' ? [4, 7] : [0, 3])
103 : (color == 'w' ? [7, 7] : [0, 0]);
104 const jBound = (i) => {
105 if (color == 'w' && i == 4) return [4, 7];
106 if (color == 'b' && i == 3) return [0, 3];
107 return [0, 7];
108 };
109 let moves = [];
110 for (let i = iBound[0]; i <= iBound[1]; i++) {
111 const jb = jBound(i);
112 for (let j = jb[0]; j <= jb[1]; j++) {
113 if (this.board[i][j] == V.EMPTY) {
114 let mv = new Move({
115 appear: [
116 new PiPo({
117 x: i,
118 y: j,
119 c: color,
120 p: p
121 })
122 ],
123 vanish: [],
124 start: { x: x, y: y },
125 end: { x: i, y: j }
126 });
127 moves.push(mv);
128 }
129 }
130 }
131 return moves;
132 }
133
134 getPotentialPawnMoves([x, y]) {
135 const color = this.turn;
136 const shiftX = V.PawnSpecs.directions[color];
137 let moves = [];
138 if (x + shiftX >= 0 && x + shiftX < 8) {
139 if (this.board[x + shiftX][y] == V.EMPTY)
140 // One square forward
141 moves.push(this.getBasicMove([x, y], [x + shiftX, y]));
142 // Captures
143 for (let shiftY of [-1, 1]) {
144 if (
145 y + shiftY >= 0 && y + shiftY < 8 &&
146 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
147 this.canTake([x, y], [x + shiftX, y + shiftY])
148 ) {
149 moves.push(this.getBasicMove([x, y], [x + shiftX, y + shiftY]));
150 }
151 }
152 }
153 let queenOnBoard = false;
154 let pawnsCount = 0;
155 outerLoop: for (let i=0; i<8; i++) {
156 for (let j=0; j<8; j++) {
157 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
158 const p = this.getPiece(i, j);
159 if (p == V.QUEEN) {
160 queenOnBoard = true;
161 break outerLoop;
162 }
163 else if (p == V.PAWN && pawnsCount <= 1) pawnsCount++;
164 }
165 }
166 }
167 if (
168 !queenOnBoard &&
169 (
170 pawnsCount == 1 ||
171 (color == 'w' && ((y <= 3 && x == y) || (y >= 4 && x == 7 - y))) ||
172 (color == 'b' && ((y >= 4 && x == y) || (y <= 3 && x == 7 - y)))
173 )
174 ) {
175 const addPromotion = ([xx, yy], moveTo) => {
176 // The promoted pawn shouldn't attack anything,
177 // and the promotion shouldn't discover a rook attack on anything.
178 const finalSquare = (!moveTo ? [x, y] : [xx, yy]);
179 let validP = true;
180 for (let step of V.steps[V.BISHOP]) {
181 const [i, j] = [finalSquare[0] + step[0], finalSquare[1] + step[1]];
182 if (
183 V.OnBoard(i, j) &&
184 this.board[i][j] != V.EMPTY &&
185 this.getColor(i, j) != color
186 ) {
187 validP = false;
188 break;
189 }
190 }
191 if (validP && !!moveTo) {
192 // Also check rook discovered attacks on the enemy king
193 let found = {
194 "0,-1": 0,
195 "0,1": 0,
196 "1,0": 0,
197 "-1,0": 0
198 };
199 // TODO: check opposite steps one after another, which could
200 // save some time (no need to explore the other line).
201 for (let step of V.steps[V.ROOK]) {
202 let [i, j] = [x + step[0], y + step[1]];
203 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
204 i += step[0];
205 j += step[1];
206 }
207 if (V.OnBoard(i, j)) {
208 const colIJ = this.getColor(i, j);
209 const pieceIJ = this.getPiece(i, j);
210 if (colIJ != color && pieceIJ == V.KING)
211 found[step[0] + "," + step[1]] = -1;
212 else if (colIJ == color && pieceIJ == V.ROOK)
213 found[step[0] + "," + step[1]] = 1;
214 }
215 }
216 if (
217 (found["0,-1"] * found["0,1"] < 0) ||
218 (found["-1,0"] * found["1,0"] < 0)
219 ) {
220 validP = false;
221 }
222 }
223 if (validP) {
224 moves.push(
225 new Move({
226 appear: [
227 new PiPo({
228 x: !!moveTo ? xx : x,
229 y: yy, //yy == y if !!moveTo
230 c: color,
231 p: V.QUEEN
232 })
233 ],
234 vanish: [
235 new PiPo({
236 x: x,
237 y: y,
238 c: color,
239 p: V.PAWN
240 })
241 ],
242 start: { x: x, y: y },
243 end: { x: xx, y: yy }
244 })
245 );
246 }
247 };
248 // In-place promotion always possible:
249 addPromotion([x - shiftX, y]);
250 for (let step of V.steps[V.BISHOP]) {
251 const [i, j] = [x + step[0], y + step[1]];
252 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
253 addPromotion([i, j], "moveTo");
254 }
255 }
256 return moves;
257 }
258
259 getPotentialBishopMoves(sq) {
260 const forward = (this.turn == 'w' ? -1 : 1);
261 return this.getSlideNJumpMoves(
262 sq,
263 V.steps[V.BISHOP].concat([ [forward, 0] ]),
264 "oneStep"
265 );
266 }
267
268 getPotentialQueenMoves(sq) {
269 return this.getSlideNJumpMoves(
270 sq,
271 V.steps[V.BISHOP],
272 "oneStep"
273 );
274 }
275
276 getAllValidMoves() {
277 if (this.movesCount >= 2) return super.getAllValidMoves();
278 const color = this.turn;
279 let moves = [];
280 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
281 moves = moves.concat(
282 this.getPotentialMovesFrom([V.size.x + (color == "w" ? 0 : 1), i])
283 );
284 }
285 return this.filterValid(moves);
286 }
287
288 isAttackedByBishop(sq, color) {
289 const forward = (this.turn == 'w' ? 1 : -1);
290 return this.isAttackedBySlideNJump(
291 sq,
292 color,
293 V.BISHOP,
294 V.steps[V.BISHOP].concat([ [forward, 0] ]),
295 "oneStep"
296 );
297 }
298
299 isAttackedByQueen(sq, color) {
300 return this.isAttackedBySlideNJump(
301 sq,
302 color,
303 V.QUEEN,
304 V.steps[V.BISHOP],
305 "oneStep"
306 );
307 }
308
309 underCheck(color) {
310 if (this.movesCount <= 1) return false;
311 return super.underCheck(color);
312 }
313
314 play(move) {
315 const color = move.appear[0].c;
316 if (this.movesCount <= 1) {
317 V.PlayOnBoard(this.board, move);
318 const piece = move.appear[0].p;
319 this.reserve[color][piece]--;
320 if (piece == V.KING) this.kingPos[color] = [move.end.x, move.end.y];
321 if (this.subTurn == 8) {
322 // All placement moves are done
323 this.movesCount++;
324 this.turn = V.GetOppCol(color);
325 if (this.movesCount == 1) this.subTurn = 1;
326 else {
327 // Initial placement is over
328 delete this["reserve"];
329 delete this["subTurn"];
330 }
331 }
332 else this.subTurn++;
333 }
334 else super.play(move);
335 }
336
337 undo(move) {
338 const color = move.appear[0].c;
339 if (this.movesCount <= 2) {
340 V.UndoOnBoard(this.board, move);
341 const piece = move.appear[0].p;
342 if (piece == V.KING) this.kingPos[color] = [-1, -1];
343 if (!this.subTurn || this.subTurn == 1) {
344 // All placement moves are undone (if any)
345 if (!this.subTurn) this.re_setReserve(8);
346 else this.subTurn = 8;
347 this.movesCount--;
348 this.turn = color;
349 }
350 else this.subTurn--;
351 this.reserve[color][piece]++;
352 }
353 else super.undo(move);
354 }
355
356 getCheckSquares() {
357 if (this.movesCount <= 1) return [];
358 return super.getCheckSquares();
359 }
360
361 getCurrentScore() {
362 if (this.movesCount <= 1) return "*";
363 return super.getCurrentScore();
364 }
365
366 static get VALUES() {
367 return {
368 p: 1,
369 r: 5,
370 n: 3,
371 b: 3,
372 q: 2,
373 k: 1000
374 };
375 }
376
377 getComputerMove() {
378 if (this.movesCount >= 2) return super.getComputerMove();
379 // Play a random "initialization move"
380 let res = [];
381 for (let i=0; i<8; i++) {
382 const moves = this.getAllValidMoves();
383 const moveIdx = randInt(moves.length);
384 this.play(moves[moveIdx]);
385 res.push(moves[moveIdx]);
386 }
387 for (let i=7; i>=0; i--) this.undo(res[i]);
388 return res;
389 }
390
391 getNotation(move) {
392 // Do not note placement moves (complete move would be too long)
393 if (move.vanish.length == 0) return "";
394 if (move.appear[0].p != move.vanish[0].p) {
395 // Pawn promotion: indicate correct final square
396 const initSquare =
397 V.CoordsToSquare({ x: move.vanish[0].x, y: move.vanish[0].y })
398 const destSquare =
399 V.CoordsToSquare({ x: move.appear[0].x, y: move.appear[0].y })
400 const prefix = (initSquare != destSquare ? initSquare : "");
401 return prefix + destSquare + "=Q";
402 }
403 return super.getNotation(move);
404 }
405 };