Games open in a new tab from main Hall (to not lose challenges)
[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 PawnSpecs() {
14 return Object.assign(
15 {},
16 ChessRules.PawnSpecs,
17 {
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 >= 2) return super.getPotentialMovesFrom([x, y]);
80 // Only reserve moves are allowed for now:
81 if (V.OnBoard(x, y)) return [];
82 const color = this.turn;
83 const p = V.RESERVE_PIECES[y];
84 if (this.reserve[color][p] == 0) return [];
85 const iBound =
86 p != V.ROOK
87 ? (color == 'w' ? [4, 7] : [0, 3])
88 : (color == 'w' ? [7, 7] : [0, 0]);
89 const jBound = (i) => {
90 if (color == 'w' && i == 4) return [4, 7];
91 if (color == 'b' && i == 3) return [0, 3];
92 return [0, 7];
93 };
94 let moves = [];
95 for (let i = iBound[0]; i <= iBound[1]; i++) {
96 const jb = jBound(i);
97 for (let j = jb[0]; j <= jb[1]; j++) {
98 if (this.board[i][j] == V.EMPTY) {
99 let mv = new Move({
100 appear: [
101 new PiPo({
102 x: i,
103 y: j,
104 c: color,
105 p: p
106 })
107 ],
108 vanish: [],
109 start: { x: x, y: y },
110 end: { x: i, y: j }
111 });
112 moves.push(mv);
113 }
114 }
115 }
116 return moves;
117 }
118
119 getPotentialPawnMoves([x, y]) {
120 const color = this.turn;
121 const shiftX = V.PawnSpecs.directions[color];
122 let moves = [];
123 if (x + shiftX >= 0 && x + shiftX < 8) {
124 if (this.board[x + shiftX][y] == V.EMPTY)
125 // One square forward
126 moves.push(this.getBasicMove([x, y], [x + shiftX, y]));
127 // Captures
128 for (let shiftY of [-1, 1]) {
129 if (
130 y + shiftY >= 0 && y + shiftY < 8 &&
131 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
132 this.canTake([x, y], [x + shiftX, y + shiftY])
133 ) {
134 moves.push(this.getBasicMove([x, y], [x + shiftX, y + shiftY]));
135 }
136 }
137 }
138 let queenOnBoard = false;
139 let pawnsCount = 0;
140 outerLoop: for (let i=0; i<8; i++) {
141 for (let j=0; j<8; j++) {
142 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
143 const p = this.getPiece(i, j);
144 if (p == V.QUEEN) {
145 queenOnBoard = true;
146 break outerLoop;
147 }
148 else if (p == V.PAWN && pawnsCount <= 1) pawnsCount++;
149 }
150 }
151 }
152 if (
153 !queenOnBoard &&
154 (
155 pawnsCount == 1 ||
156 (color == 'w' && ((y <= 3 && x == y) || (y >= 4 && x == 7 - y))) ||
157 (color == 'b' && ((y >= 4 && x == y) || (y <= 3 && x == 7 - y)))
158 )
159 ) {
160 const addPromotion = ([xx, yy], moveTo) => {
161 // The promoted pawn shouldn't attack anything,
162 // and the promotion shouldn't discover a rook attack on anything.
163 const finalSquare = (!moveTo ? [x, y] : [xx, yy]);
164 let validP = true;
165 for (let step of V.steps[V.BISHOP]) {
166 const [i, j] = [finalSquare[0] + step[0], finalSquare[1] + step[1]];
167 if (
168 V.OnBoard(i, j) &&
169 this.board[i][j] != V.EMPTY &&
170 this.getColor(i, j) != color
171 ) {
172 validP = false;
173 break;
174 }
175 }
176 if (validP && !!moveTo) {
177 // Also check rook discovered attacks on the enemy king
178 let found = {
179 "0,-1": 0,
180 "0,1": 0,
181 "1,0": 0,
182 "-1,0": 0
183 };
184 // TODO: check opposite steps one after another, which could
185 // save some time (no need to explore the other line).
186 for (let step of V.steps[V.ROOK]) {
187 let [i, j] = [x + step[0], y + step[1]];
188 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
189 i += step[0];
190 j += step[1];
191 }
192 if (V.OnBoard(i, j)) {
193 const colIJ = this.getColor(i, j);
194 const pieceIJ = this.getPiece(i, j);
195 if (colIJ != color && pieceIJ == V.KING)
196 found[step[0] + "," + step[1]] = -1;
197 else if (colIJ == color && pieceIJ == V.ROOK)
198 found[step[0] + "," + step[1]] = 1;
199 }
200 }
201 if (
202 (found["0,-1"] * found["0,1"] < 0) ||
203 (found["-1,0"] * found["1,0"] < 0)
204 ) {
205 validP = false;
206 }
207 }
208 if (validP) {
209 moves.push(
210 new Move({
211 appear: [
212 new PiPo({
213 x: !!moveTo ? xx : x,
214 y: yy, //yy == y if !!moveTo
215 c: color,
216 p: V.QUEEN
217 })
218 ],
219 vanish: [
220 new PiPo({
221 x: x,
222 y: y,
223 c: color,
224 p: V.PAWN
225 })
226 ],
227 start: { x: x, y: y },
228 end: { x: xx, y: yy }
229 })
230 );
231 }
232 };
233 // In-place promotion always possible:
234 addPromotion([x - shiftX, y]);
235 for (let step of V.steps[V.BISHOP]) {
236 const [i, j] = [x + step[0], y + step[1]];
237 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
238 addPromotion([i, j], "moveTo");
239 }
240 }
241 return moves;
242 }
243
244 getPotentialBishopMoves(sq) {
245 const forward = (this.turn == 'w' ? -1 : 1);
246 return this.getSlideNJumpMoves(
247 sq,
248 V.steps[V.BISHOP].concat([ [forward, 0] ]),
249 "oneStep"
250 );
251 }
252
253 getPotentialQueenMoves(sq) {
254 return this.getSlideNJumpMoves(
255 sq,
256 V.steps[V.BISHOP],
257 "oneStep"
258 );
259 }
260
261 getAllValidMoves() {
262 if (this.movesCount >= 2) return super.getAllValidMoves();
263 const color = this.turn;
264 let moves = [];
265 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
266 moves = moves.concat(
267 this.getPotentialMovesFrom([V.size.x + (color == "w" ? 0 : 1), i])
268 );
269 }
270 return this.filterValid(moves);
271 }
272
273 isAttackedByBishop(sq, color) {
274 const forward = (this.turn == 'w' ? 1 : -1);
275 return this.isAttackedBySlideNJump(
276 sq,
277 color,
278 V.BISHOP,
279 V.steps[V.BISHOP].concat([ [forward, 0] ]),
280 "oneStep"
281 );
282 }
283
284 isAttackedByQueen(sq, color) {
285 return this.isAttackedBySlideNJump(
286 sq,
287 color,
288 V.QUEEN,
289 V.steps[V.BISHOP],
290 "oneStep"
291 );
292 }
293
294 underCheck(color) {
295 if (this.movesCount <= 1) return false;
296 return super.underCheck(color);
297 }
298
299 play(move) {
300 const color = move.appear[0].c;
301 if (this.movesCount <= 1) {
302 V.PlayOnBoard(this.board, move);
303 const piece = move.appear[0].p;
304 this.reserve[color][piece]--;
305 if (piece == V.KING) this.kingPos[color] = [move.end.x, move.end.y];
306 if (this.subTurn == 8) {
307 // All placement moves are done
308 this.movesCount++;
309 this.turn = V.GetOppCol(color);
310 if (this.movesCount == 1) this.subTurn = 1;
311 else {
312 // Initial placement is over
313 delete this["reserve"];
314 delete this["subTurn"];
315 }
316 }
317 else this.subTurn++;
318 }
319 else super.play(move);
320 }
321
322 undo(move) {
323 const color = move.appear[0].c;
324 if (this.movesCount <= 2) {
325 V.UndoOnBoard(this.board, move);
326 const piece = move.appear[0].p;
327 if (piece == V.KING) this.kingPos[color] = [-1, -1];
328 if (!this.subTurn || this.subTurn == 1) {
329 // All placement moves are undone (if any)
330 if (!this.subTurn) this.re_setReserve(8);
331 else this.subTurn = 8;
332 this.movesCount--;
333 this.turn = color;
334 }
335 else this.subTurn--;
336 this.reserve[color][piece]++;
337 }
338 else super.undo(move);
339 }
340
341 getCheckSquares() {
342 if (this.movesCount <= 1) return [];
343 return super.getCheckSquares();
344 }
345
346 getCurrentScore() {
347 if (this.movesCount <= 1) return "*";
348 return super.getCurrentScore();
349 }
350
351 static get VALUES() {
352 return {
353 p: 1,
354 r: 5,
355 n: 3,
356 b: 3,
357 q: 2,
358 k: 1000
359 };
360 }
361
362 getComputerMove() {
363 if (this.movesCount >= 2) return super.getComputerMove();
364 // Play a random "initialization move"
365 let res = [];
366 for (let i=0; i<8; i++) {
367 const moves = this.getAllValidMoves();
368 const moveIdx = randInt(moves.length);
369 this.play(moves[moveIdx]);
370 res.push(moves[moveIdx]);
371 }
372 for (let i=7; i>=0; i--) this.undo(res[i]);
373 return res;
374 }
375
376 getNotation(move) {
377 // Do not note placement moves (complete move would be too long)
378 if (move.vanish.length == 0) return "";
379 if (move.appear[0].p != move.vanish[0].p) {
380 // Pawn promotion: indicate correct final square
381 const initSquare =
382 V.CoordsToSquare({ x: move.vanish[0].x, y: move.vanish[0].y })
383 const destSquare =
384 V.CoordsToSquare({ x: move.appear[0].x, y: move.appear[0].y })
385 const prefix = (initSquare != destSquare ? initSquare : "");
386 return prefix + destSquare + "=Q";
387 }
388 return super.getNotation(move);
389 }
390 };