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