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