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