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