f24e5b2f995f74855bb2471074dcd7a1fdbfade9
[vchess.git] / client / src / variants / Koopa.js
1 import { ChessRules, PiPo } from "@/base_rules";
2
3 export class KoopaRules extends ChessRules {
4 static get HasEnpassant() {
5 return false;
6 }
7
8 static get STUNNED() {
9 return ['s', 'u', 'o', 'c', 't', 'l'];
10 }
11
12 static get PIECES() {
13 return ChessRules.PIECES.concat(V.STUNNED);
14 }
15
16 static ParseFen(fen) {
17 let res = ChessRules.ParseFen(fen);
18 const fenParts = fen.split(" ");
19 res.stunned = fenParts[4];
20 return res;
21 }
22
23 static IsGoodFen(fen) {
24 if (!ChessRules.IsGoodFen(fen)) return false;
25 const fenParsed = V.ParseFen(fen);
26 // 5) Check "stunned"
27 if (
28 !fenParsed.stunned ||
29 (
30 fenParsed.stunned != "-" &&
31 !fenParsed.stunned.match(/^([a-h][1-8][1-4],?)*$/)
32 )
33 ) {
34 return false;
35 }
36 return true;
37 }
38
39 getPpath(b) {
40 return (V.STUNNED.includes(b[1]) ? "Koopa/" : "") + b;
41 }
42
43 getFen() {
44 return super.getFen() + " " + this.getStunnedFen();
45 }
46
47 getFenForRepeat() {
48 return super.getFenForRepeat() + "_" + this.getStunnedFen();
49 }
50
51 getStunnedFen() {
52 return (
53 Object.keys(this.stunned)
54 .map(square => square + this.stunned[square])
55 .join(",")
56 );
57 }
58
59 // Base GenRandInitFen() is fine because en-passant indicator will
60 // stand for stunned indicator.
61
62 scanKings(fen) {
63 this.INIT_COL_KING = { w: -1, b: -1 };
64 // Squares of white and black king:
65 this.kingPos = { w: [-1, -1], b: [-1, -1] };
66 const fenRows = V.ParseFen(fen).position.split("/");
67 const startRow = { 'w': V.size.x - 1, 'b': 0 };
68 for (let i = 0; i < fenRows.length; i++) {
69 let k = 0; //column index on board
70 for (let j = 0; j < fenRows[i].length; j++) {
71 switch (fenRows[i].charAt(j)) {
72 case "k":
73 case "l":
74 this.kingPos["b"] = [i, k];
75 this.INIT_COL_KING["b"] = k;
76 break;
77 case "K":
78 case "L":
79 this.kingPos["w"] = [i, k];
80 this.INIT_COL_KING["w"] = k;
81 break;
82 default: {
83 const num = parseInt(fenRows[i].charAt(j));
84 if (!isNaN(num)) k += num - 1;
85 }
86 }
87 k++;
88 }
89 }
90 }
91
92 setOtherVariables(fen) {
93 super.setOtherVariables(fen);
94 let stunnedArray = [];
95 const stunnedFen = V.ParseFen(fen).stunned;
96 if (stunnedFen != "-") {
97 stunnedArray =
98 stunnedFen
99 .split(",")
100 .map(s => {
101 return {
102 square: s.substr(0, 2),
103 state: parseInt(s[2])
104 };
105 });
106 }
107 this.stunned = {};
108 stunnedArray.forEach(s => {
109 this.stunned[s.square] = s.state;
110 });
111 }
112
113 getNormalizedStep(step) {
114 const [deltaX, deltaY] = [Math.abs(step[0]), Math.abs(step[1])];
115 if (deltaX == 0 || deltaY == 0 || deltaX == deltaY)
116 return [step[0] / deltaX || 0, step[1] / deltaY || 0];
117 // Knight:
118 const divisor = Math.min(deltaX, deltaY)
119 return [step[0] / divisor, step[1] / divisor];
120 }
121
122 getPotentialMovesFrom([x, y]) {
123 let moves = super.getPotentialMovesFrom([x, y]);
124 // Complete moves: stuns & kicks
125 let promoteAfterStun = [];
126 const color = this.turn;
127 moves.forEach(m => {
128 if (m.vanish.length == 2 && m.appear.length == 1) {
129 const step =
130 this.getNormalizedStep([m.end.x - m.start.x, m.end.y - m.start.y]);
131 // "Capture" something: is target stunned?
132 if (V.STUNNED.includes(m.vanish[1].p)) {
133 // Kick it: continue movement in the same direction,
134 // destroying all on its path.
135 let [i, j] = [m.end.x + step[0], m.end.y + step[1]];
136 while (V.OnBoard(i, j)) {
137 if (this.board[i][j] != V.EMPTY) {
138 m.vanish.push(
139 new PiPo({
140 x: i,
141 y: j,
142 c: this.getColor(i, j),
143 p: this.getPiece(i, j)
144 })
145 );
146 }
147 i += step[0];
148 j += step[1];
149 }
150 }
151 else {
152 // The piece is now stunned
153 m.appear.push(JSON.parse(JSON.stringify(m.vanish[1])));
154 const pIdx = ChessRules.PIECES.findIndex(p => p == m.appear[1].p);
155 m.appear[1].p = V.STUNNED[pIdx];
156 // And the capturer continue in the same direction until an empty
157 // square or the edge of the board, maybe stunning other pieces.
158 let [i, j] = [m.end.x + step[0], m.end.y + step[1]];
159 while (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) {
160 const colIJ = this.getColor(i, j);
161 const pieceIJ = this.getPiece(i, j);
162 let pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ);
163 if (pIdx >= 0) {
164 // The piece isn't already stunned
165 m.vanish.push(
166 new PiPo({
167 x: i,
168 y: j,
169 c: colIJ,
170 p: pieceIJ
171 })
172 );
173 m.appear.push(
174 new PiPo({
175 x: i,
176 y: j,
177 c: colIJ,
178 p: V.STUNNED[pIdx]
179 })
180 );
181 }
182 i += step[0];
183 j += step[1];
184 }
185 if (V.OnBoard(i, j)) {
186 m.appear[0].x = i;
187 m.appear[0].y = j;
188 // Is it a pawn on last rank?
189 if ((color == 'w' && i == 0) || (color == 'b' && i == 7)) {
190 m.appear[0].p = V.ROOK;
191 for (let ppiece of [V.KNIGHT, V.BISHOP, V.QUEEN]) {
192 let mp = JSON.parse(JSON.stringify(m));
193 mp.appear[0].p = ppiece;
194 promoteAfterStun.push(mp);
195 }
196 }
197 }
198 else
199 // The piece is out
200 m.appear.shift();
201 }
202 }
203 });
204 return moves.concat(promoteAfterStun);
205 }
206
207 filterValid(moves) {
208 // Forbid kicking own king out
209 const color = this.turn;
210 return moves.filter(m => {
211 const kingAppear = m.appear.some(a => a.c == color && a.p == V.KING);
212 return m.vanish.every(v => {
213 return (
214 v.c != color ||
215 !["k", "l"].includes(v.p) ||
216 (v.p == "k" && kingAppear)
217 );
218 });
219 });
220 }
221
222 getCheckSquares() {
223 return [];
224 }
225
226 getCurrentScore() {
227 if (this.kingPos['w'][0] < 0) return "0-1";
228 if (this.kingPos['b'][0] < 0) return "1-0";
229 if (!this.atLeastOneMove()) return "1/2";
230 return "*";
231 }
232
233 postPlay(move) {
234 // Base method is fine because a stunned king (which won't be detected)
235 // can still castle after going back to normal.
236 super.postPlay(move);
237 const kIdx = move.vanish.findIndex(v => v.p == "l");
238 if (kIdx >= 0)
239 // A stunned king vanish (game over)
240 this.kingPos[move.vanish[kIdx].c] = [-1, -1];
241 move.stunned = JSON.stringify(this.stunned);
242 // Array of stunned stage 1 pieces (just back to normal then)
243 Object.keys(this.stunned).forEach(square => {
244 // All (formerly) stunned pieces progress by 1 level, if still on board
245 const coords = V.SquareToCoords(square);
246 const [x, y] = [coords.x, coords.y];
247 if (V.STUNNED.includes(this.board[x][y][1])) {
248 // Stunned piece still on board
249 this.stunned[square]--;
250 if (this.stunned[square] == 0) {
251 delete this.stunned[square];
252 const color = this.getColor(x, y);
253 const piece = this.getPiece(x, y);
254 const pIdx = V.STUNNED.findIndex(p => p == piece);
255 this.board[x][y] = color + ChessRules.PIECES[pIdx];
256 }
257 }
258 else delete this.stunned[square];
259 });
260 // Any new stunned pieces?
261 move.appear.forEach(a => {
262 if (V.STUNNED.includes(a.p))
263 // Set to maximum stun level:
264 this.stunned[V.CoordsToSquare({ x: a.x, y: a.y })] = 4;
265 });
266 }
267
268 postUndo(move) {
269 super.postUndo(move);
270 const kIdx = move.vanish.findIndex(v => v.p == "l");
271 if (kIdx >= 0) {
272 // A stunned king vanished
273 this.kingPos[move.vanish[kIdx].c] =
274 [move.vanish[kIdx].x, move.vanish[kIdx].y];
275 }
276 this.stunned = JSON.parse(move.stunned);
277 for (let i=0; i<8; i++) {
278 for (let j=0; j<8; j++) {
279 const square = V.CoordsToSquare({ x: i, y: j });
280 const pieceIJ = this.getPiece(i, j);
281 if (!this.stunned[square]) {
282 const pIdx = V.STUNNED.findIndex(p => p == pieceIJ);
283 if (pIdx >= 0)
284 this.board[i][j] = this.getColor(i, j) + ChessRules.PIECES[pIdx];
285 }
286 else {
287 const pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ);
288 if (pIdx >= 0)
289 this.board[i][j] = this.getColor(i, j) + V.STUNNED[pIdx];
290 }
291 }
292 }
293 }
294
295 static get VALUES() {
296 return Object.assign(
297 {
298 s: 1,
299 u: 5,
300 o: 3,
301 c: 3,
302 t: 9,
303 l: 1000
304 },
305 ChessRules.VALUES
306 );
307 }
308
309 static get SEARCH_DEPTH() {
310 return 2;
311 }
312
313 getNotation(move) {
314 if (
315 move.appear.length == 2 &&
316 move.vanish.length == 2 &&
317 move.appear.concat(move.vanish).every(
318 av => ChessRules.PIECES.includes(av.p)) &&
319 move.appear[0].p == V.KING
320 ) {
321 if (move.end.y < move.start.y) return "0-0-0";
322 return "0-0";
323 }
324 const finalSquare = V.CoordsToSquare(move.end);
325 const piece = this.getPiece(move.start.x, move.start.y);
326 const captureMark = move.vanish.length >= 2 ? "x" : "";
327 let pawnMark = "";
328 if (piece == 'p' && captureMark.length == 1)
329 pawnMark = V.CoordToColumn(move.start.y); //start column
330 // Piece or pawn movement
331 let notation =
332 (piece == V.PAWN ? pawnMark : piece.toUpperCase()) +
333 captureMark + finalSquare;
334 if (
335 piece == 'p' &&
336 move.appear[0].c == move.vanish[0].c &&
337 move.appear[0].p != 'p'
338 ) {
339 // Promotion
340 notation += "=" + move.appear[0].p.toUpperCase();
341 }
342 return notation;
343 }
344 };