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