A few fixes (for updateCastleFlags()) + add Madhouse and Pocketknight variants
[vchess.git] / client / src / variants / Ball.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { shuffle } from "@/utils/alea";
4
5 export class BallRules extends ChessRules {
6 static get Lines() {
7 return [
8 // White goal:
9 [[0, 3], [0, 6]],
10 [[0, 6], [1, 6]],
11 [[1, 6], [1, 3]],
12 [[1, 3], [0, 3]],
13 // Black goal:
14 [[9, 3], [9, 6]],
15 [[9, 6], [8, 6]],
16 [[8, 6], [8, 3]],
17 [[8, 3], [9, 3]]
18 ];
19 }
20
21 static get PawnSpecs() {
22 return Object.assign(
23 {},
24 ChessRules.PawnSpecs,
25 { promotions: ChessRules.PawnSpecs.promotions.concat([V.PHOENIX]) }
26 );
27 }
28
29 static get HasFlags() {
30 return false;
31 }
32
33 static get PHOENIX() {
34 return 'h';
35 }
36
37 static get BALL() {
38 // Ball is already taken:
39 return "aa";
40 }
41
42 static get HAS_BALL_CODE() {
43 return {
44 'p': 's',
45 'r': 'u',
46 'n': 'o',
47 'b': 'c',
48 'q': 't',
49 'k': 'l',
50 'h': 'i'
51 };
52 }
53
54 static get HAS_BALL_DECODE() {
55 return {
56 's': 'p',
57 'u': 'r',
58 'o': 'n',
59 'c': 'b',
60 't': 'q',
61 'l': 'k',
62 'i': 'h'
63 };
64 }
65
66 static get PIECES() {
67 return ChessRules.PIECES
68 .concat([V.PHOENIX])
69 .concat(Object.keys(V.HAS_BALL_DECODE))
70 .concat(['a']);
71 }
72
73 static board2fen(b) {
74 if (b == V.BALL) return 'a';
75 return ChessRules.board2fen(b);
76 }
77
78 static fen2board(f) {
79 if (f == 'a') return V.BALL;
80 return ChessRules.fen2board(f);
81 }
82
83 static ParseFen(fen) {
84 return Object.assign(
85 ChessRules.ParseFen(fen),
86 { pmove: fen.split(" ")[4] }
87 );
88 }
89
90 // Check that exactly one ball is on the board
91 // + at least one piece per color.
92 static IsGoodPosition(position) {
93 if (position.length == 0) return false;
94 const rows = position.split("/");
95 if (rows.length != V.size.x) return false;
96 let pieces = { "w": 0, "b": 0 };
97 const withBall = Object.keys(V.HAS_BALL_DECODE).concat([V.BALL]);
98 let ballCount = 0;
99 for (let row of rows) {
100 let sumElts = 0;
101 for (let i = 0; i < row.length; i++) {
102 const lowerRi = row[i].toLowerCase();
103 if (V.PIECES.includes(lowerRi)) {
104 if (lowerRi != V.BALL) pieces[row[i] == lowerRi ? "b" : "w"]++;
105 if (withBall.includes(lowerRi)) ballCount++;
106 sumElts++;
107 } else {
108 const num = parseInt(row[i], 10);
109 if (isNaN(num)) return false;
110 sumElts += num;
111 }
112 }
113 if (sumElts != V.size.y) return false;
114 }
115 if (ballCount != 1 || Object.values(pieces).some(v => v == 0))
116 return false;
117 return true;
118 }
119
120 static IsGoodFen(fen) {
121 if (!ChessRules.IsGoodFen(fen)) return false;
122 const fenParts = fen.split(" ");
123 if (fenParts.length != 5) return false;
124 if (
125 fenParts[4] != "-" &&
126 !fenParts[4].match(/^([a-i][1-9]){2,2}$/)
127 ) {
128 return false;
129 }
130 return true;
131 }
132
133 getPpath(b) {
134 let prefix = "";
135 const withPrefix =
136 Object.keys(V.HAS_BALL_DECODE)
137 .concat([V.PHOENIX])
138 .concat(['a']);
139 if (withPrefix.includes(b[1])) prefix = "Ball/";
140 return prefix + b;
141 }
142
143 getPPpath(m) {
144 if (
145 m.vanish.length == 2 &&
146 m.appear.length == 2 &&
147 m.appear[0].c != m.appear[1].c
148 ) {
149 // Take ball in place (from opponent)
150 return "Ball/inplace";
151 }
152 return super.getPPpath(m);
153 }
154
155 canTake([x1, y1], [x2, y2]) {
156 if (this.getColor(x1, y1) !== this.getColor(x2, y2)) {
157 // The piece holding the ball cannot capture:
158 return (
159 !(Object.keys(V.HAS_BALL_DECODE)
160 .includes(this.board[x1][y1].charAt(1)))
161 );
162 }
163 // Pass: possible only if one of the friendly pieces has the ball
164 return (
165 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1)) ||
166 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x2][y2].charAt(1))
167 );
168 }
169
170 getFen() {
171 return super.getFen() + " " + this.getPmoveFen();
172 }
173
174 getFenForRepeat() {
175 return super.getFenForRepeat() + "_" + this.getPmoveFen();
176 }
177
178 getPmoveFen() {
179 const L = this.pmoves.length;
180 if (!this.pmoves[L-1]) return "-";
181 return (
182 V.CoordsToSquare(this.pmoves[L-1].start) +
183 V.CoordsToSquare(this.pmoves[L-1].end)
184 );
185 }
186
187 static GenRandInitFen(randomness) {
188 if (randomness == 0)
189 return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 - -";
190
191 let pieces = { w: new Array(9), b: new Array(9) };
192 for (let c of ["w", "b"]) {
193 if (c == 'b' && randomness == 1) {
194 pieces['b'] = pieces['w'];
195 break;
196 }
197
198 // Get random squares for every piece, with bishops and phoenixes
199 // on different colors:
200 let positions = shuffle(ArrayFun.range(9));
201 const composition = ['b', 'b', 'h', 'h', 'n', 'n', 'r', 'r', 'q'];
202 let rem2 = positions[0] % 2;
203 if (rem2 == positions[1] % 2) {
204 // Fix bishops (on different colors)
205 for (let i=4; i<9; i++) {
206 if (positions[i] % 2 != rem2)
207 [positions[1], positions[i]] = [positions[i], positions[1]];
208 }
209 }
210 rem2 = positions[2] % 2;
211 if (rem2 == positions[3] % 2) {
212 // Fix phoenixes too:
213 for (let i=4; i<9; i++) {
214 if (positions[i] % 2 != rem2)
215 [positions[3], positions[i]] = [positions[i], positions[3]];
216 }
217 }
218 for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i];
219 }
220 return (
221 pieces["b"].join("") +
222 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
223 pieces["w"].join("").toUpperCase() +
224 " w 0 - -"
225 );
226 }
227
228 scanKings() {}
229
230 setOtherVariables(fen) {
231 super.setOtherVariables(fen);
232 const pmove = V.ParseFen(fen).pmove;
233 // Local stack of "pass moves" (no need for appear & vanish)
234 this.pmoves = [
235 pmove != "-"
236 ?
237 {
238 start: V.SquareToCoords(pmove.substr(0, 2)),
239 end: V.SquareToCoords(pmove.substr(2))
240 }
241 : null
242 ];
243 }
244
245 static get size() {
246 return { x: 9, y: 9 };
247 }
248
249 getPiece(i, j) {
250 const p = this.board[i][j].charAt(1);
251 if (Object.keys(V.HAS_BALL_DECODE).includes(p))
252 return V.HAS_BALL_DECODE[p];
253 return p;
254 }
255
256 static get steps() {
257 return Object.assign(
258 {},
259 ChessRules.steps,
260 // Add phoenix moves
261 {
262 h: [
263 [-2, -2],
264 [-2, 2],
265 [2, -2],
266 [2, 2],
267 [-1, 0],
268 [1, 0],
269 [0, -1],
270 [0, 1]
271 ]
272 }
273 );
274 }
275
276 // Because of the ball, getPiece() could be wrong:
277 // use board[x][y][1] instead (always valid).
278 getBasicMove([sx, sy], [ex, ey], tr) {
279 const initColor = this.getColor(sx, sy);
280 const initPiece = this.board[sx][sy].charAt(1);
281 let mv = new Move({
282 appear: [
283 new PiPo({
284 x: ex,
285 y: ey,
286 c: tr ? tr.c : initColor,
287 p: tr ? tr.p : initPiece
288 })
289 ],
290 vanish: [
291 new PiPo({
292 x: sx,
293 y: sy,
294 c: initColor,
295 p: initPiece
296 })
297 ]
298 });
299
300 // Fix "ball holding" indication in case of promotions:
301 if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece))
302 mv.appear[0].p = V.HAS_BALL_CODE[tr.p];
303
304 // The opponent piece disappears if we take it
305 if (this.board[ex][ey] != V.EMPTY) {
306 mv.vanish.push(
307 new PiPo({
308 x: ex,
309 y: ey,
310 c: this.getColor(ex, ey),
311 p: this.board[ex][ey].charAt(1)
312 })
313 );
314 }
315
316 // Post-processing: maybe the ball was taken, or a piece + ball,
317 // or maybe a pass (ball <--> piece)
318 if (mv.vanish.length == 2) {
319 if (
320 // Take the ball?
321 mv.vanish[1].c == 'a' ||
322 // Capture a ball-holding piece? If friendly one, then adjust
323 Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
324 ) {
325 mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p];
326 if (mv.vanish[1].c == mv.vanish[0].c) {
327 // "Capturing" self => pass
328 mv.appear[0].x = mv.start.x;
329 mv.appear[0].y = mv.start.y;
330 mv.appear.push(
331 new PiPo({
332 x: mv.end.x,
333 y: mv.end.y,
334 p: V.HAS_BALL_DECODE[mv.vanish[1].p],
335 c: mv.vanish[0].c
336 })
337 );
338 }
339 } else if (mv.vanish[1].c == mv.vanish[0].c) {
340 // Pass the ball: the passing unit does not disappear
341 mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
342 mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
343 mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p];
344 }
345 // Else: standard capture
346 }
347
348 return mv;
349 }
350
351 // NOTE: if a pawn captures en-passant, he doesn't hold the ball
352 // So base implementation is fine.
353
354 getPotentialMovesFrom([x, y]) {
355 let moves = undefined;
356 const piece = this.getPiece(x, y);
357 if (piece == V.PHOENIX)
358 moves = this.getPotentialPhoenixMoves([x, y]);
359 else moves = super.getPotentialMovesFrom([x, y]);
360 // Add "taking ball in place" move (at most one in list)
361 for (let m of moves) {
362 if (
363 m.vanish.length == 2 &&
364 m.vanish[1].p != 'a' &&
365 m.vanish[0].c != m.vanish[1].c &&
366 Object.keys(V.HAS_BALL_DECODE).includes(m.appear[0].p)
367 ) {
368 const color = this.turn;
369 const oppCol = V.GetOppCol(color);
370 moves.push(
371 new Move({
372 appear: [
373 new PiPo({
374 x: x,
375 y: y,
376 c: color,
377 p: m.appear[0].p
378 }),
379 new PiPo({
380 x: m.vanish[1].x,
381 y: m.vanish[1].y,
382 c: oppCol,
383 p: V.HAS_BALL_DECODE[m.vanish[1].p]
384 })
385 ],
386 vanish: [
387 new PiPo({
388 x: x,
389 y: y,
390 c: color,
391 p: piece
392 }),
393 new PiPo({
394 x: m.vanish[1].x,
395 y: m.vanish[1].y,
396 c: oppCol,
397 p: m.vanish[1].p
398 })
399 ],
400 end: { x: m.end.x, y: m.end.y }
401 })
402 );
403 break;
404 }
405 }
406 return moves;
407 }
408
409 // "Sliders": at most 3 steps
410 getSlideNJumpMoves([x, y], steps, oneStep) {
411 let moves = [];
412 outerLoop: for (let step of steps) {
413 let i = x + step[0];
414 let j = y + step[1];
415 let stepCount = 1;
416 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
417 moves.push(this.getBasicMove([x, y], [i, j]));
418 if (oneStep || stepCount == 3) continue outerLoop;
419 i += step[0];
420 j += step[1];
421 stepCount++;
422 }
423 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
424 moves.push(this.getBasicMove([x, y], [i, j]));
425 }
426 return moves;
427 }
428
429 getPotentialPhoenixMoves(sq) {
430 return this.getSlideNJumpMoves(sq, V.steps[V.PHOENIX], "oneStep");
431 }
432
433 getPmove(move) {
434 if (
435 move.vanish.length == 2 &&
436 move.appear.length == 2 &&
437 move.appear[0].c != move.appear[1].c
438 ) {
439 // In-place pass:
440 return {
441 start: move.start,
442 end: move.end
443 };
444 }
445 return null;
446 }
447
448 oppositePasses(m1, m2) {
449 return (
450 m1.start.x == m2.end.x &&
451 m1.start.y == m2.end.y &&
452 m1.end.x == m2.start.x &&
453 m1.end.y == m2.start.y
454 );
455 }
456
457 filterValid(moves) {
458 const L = this.pmoves.length;
459 const lp = this.pmoves[L-1];
460 if (!lp) return moves;
461 return moves.filter(m => {
462 return (
463 m.vanish.length == 1 ||
464 m.appear.length == 1 ||
465 m.appear[0].c == m.appear[1].c ||
466 !this.oppositePasses(lp, m)
467 );
468 });
469 }
470
471 // isAttacked: unused here (no checks)
472
473 postPlay(move) {
474 this.pmoves.push(this.getPmove(move));
475 }
476
477 postUndo() {
478 this.pmoves.pop();
479 }
480
481 getCheckSquares() {
482 return [];
483 }
484
485 getCurrentScore() {
486 // Turn has changed:
487 const color = V.GetOppCol(this.turn);
488 const lastRank = (color == "w" ? 0 : 8);
489 if ([3,4,5].some(
490 i => {
491 return (
492 Object.keys(V.HAS_BALL_DECODE).includes(
493 this.board[lastRank][i].charAt(1)) &&
494 this.getColor(lastRank, i) == color
495 );
496 }
497 )) {
498 // Goal scored!
499 return color == "w" ? "1-0" : "0-1";
500 }
501 if (this.atLeastOneMove()) return "*";
502 // Stalemate (quite unlikely?)
503 return "1/2";
504 }
505
506 static get VALUES() {
507 return {
508 p: 1,
509 r: 3,
510 n: 3,
511 b: 2,
512 q: 5,
513 h: 3,
514 a: 0 //ball: neutral
515 };
516 }
517
518 static get SEARCH_DEPTH() {
519 return 2;
520 }
521
522 evalPosition() {
523 // Count material:
524 let evaluation = super.evalPosition();
525 if (this.board[4][4] == V.BALL)
526 // Ball not captured yet
527 return evaluation;
528 // Ponder depending on ball position
529 for (let i=0; i<9; i++) {
530 for (let j=0; j<9; j++) {
531 if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1]))
532 return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i);
533 }
534 }
535 return 0; //never reached
536 }
537
538 getNotation(move) {
539 const finalSquare = V.CoordsToSquare(move.end);
540 if (move.appear.length == 2)
541 // A pass: special notation
542 return V.CoordsToSquare(move.start) + "P" + finalSquare;
543 const piece = this.getPiece(move.start.x, move.start.y);
544 if (piece == V.PAWN) {
545 // Pawn move
546 let notation = "";
547 if (move.vanish.length > move.appear.length) {
548 // Capture
549 const startColumn = V.CoordToColumn(move.start.y);
550 notation = startColumn + "x" + finalSquare;
551 }
552 else notation = finalSquare;
553 if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
554 // Promotion
555 const promotePiece =
556 V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
557 notation += "=" + promotePiece.toUpperCase();
558 }
559 return notation;
560 }
561 // Piece movement
562 return (
563 piece.toUpperCase() +
564 (move.vanish.length > move.appear.length ? "x" : "") +
565 finalSquare
566 );
567 }
568 };