1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
2 import { ArrayFun
} from "@/utils/array";
3 import { shuffle
} from "@/utils/alea";
5 export class BallRules
extends ChessRules
{
22 static get PawnSpecs() {
26 { promotions: ChessRules
.PawnSpecs
.promotions
.concat([V
.PHOENIX
]) }
30 static get HasFlags() {
34 static get PHOENIX() {
39 // Ball is already taken:
43 static get HAS_BALL_CODE() {
55 static get HAS_BALL_DECODE() {
68 return ChessRules
.PIECES
70 .concat(Object
.keys(V
.HAS_BALL_DECODE
))
75 if (b
== V
.BALL
) return 'a';
76 return ChessRules
.board2fen(b
);
80 if (f
== 'a') return V
.BALL
;
81 return ChessRules
.fen2board(f
);
84 static ParseFen(fen
) {
86 ChessRules
.ParseFen(fen
),
87 { pmove: fen
.split(" ")[4] }
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
]);
100 for (let row
of rows
) {
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
++;
109 const num
= parseInt(row
[i
], 10);
110 if (isNaN(num
)) return false;
114 if (sumElts
!= V
.size
.y
) return false;
116 if (ballCount
!= 1 || Object
.values(pieces
).some(v
=> v
== 0))
121 static IsGoodFen(fen
) {
122 if (!ChessRules
.IsGoodFen(fen
)) return false;
123 const fenParts
= fen
.split(" ");
124 if (fenParts
.length
!= 5) return false;
126 fenParts
[4] != "-" &&
127 !fenParts
[4].match(/^([a-i][1-9]){2,2}$/)
137 Object
.keys(V
.HAS_BALL_DECODE
)
140 if (withPrefix
.includes(b
[1])) prefix
= "Ball/";
146 m
.vanish
.length
== 2 &&
147 m
.appear
.length
== 2 &&
148 m
.appear
[0].c
!= m
.appear
[1].c
150 // Take ball in place (from opponent)
151 return "Ball/inplace";
153 return super.getPPpath(m
);
156 canTake([x1
, y1
], [x2
, y2
]) {
157 if (this.getColor(x1
, y1
) !== this.getColor(x2
, y2
)) {
158 // The piece holding the ball cannot capture:
160 !(Object
.keys(V
.HAS_BALL_DECODE
)
161 .includes(this.board
[x1
][y1
].charAt(1)))
164 // Pass: possible only if one of the friendly pieces has the ball
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))
172 return super.getFen() + " " + this.getPmoveFen();
176 return super.getFenForRepeat() + "_" + this.getPmoveFen();
180 const L
= this.pmoves
.length
;
181 if (!this.pmoves
[L
-1]) return "-";
183 V
.CoordsToSquare(this.pmoves
[L
-1].start
) +
184 V
.CoordsToSquare(this.pmoves
[L
-1].end
)
188 static GenRandInitFen(randomness
) {
190 return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 - -";
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'];
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]];
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]];
219 for (let i
= 0; i
< 9; i
++) pieces
[c
][positions
[i
]] = composition
[i
];
222 pieces
["b"].join("") +
223 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
224 pieces
["w"].join("").toUpperCase() +
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)
239 start: V
.SquareToCoords(pmove
.substr(0, 2)),
240 end: V
.SquareToCoords(pmove
.substr(2))
247 return { x: 9, y: 9 };
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
];
258 return Object
.assign(
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);
287 c: tr
? tr
.c : initColor
,
288 p: tr
? tr
.p : initPiece
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
];
305 // The opponent piece disappears if we take it
306 if (this.board
[ex
][ey
] != V
.EMPTY
) {
311 c: this.getColor(ex
, ey
),
312 p: this.board
[ex
][ey
].charAt(1)
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) {
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
)
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
;
335 p: V
.HAS_BALL_DECODE
[mv
.vanish
[1].p
],
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
];
346 // Else: standard capture
352 // NOTE: if a pawn captures en-passant, he doesn't hold the ball
353 // So base implementation is fine.
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
) {
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
)
369 const color
= this.turn
;
370 const oppCol
= V
.GetOppCol(color
);
384 p: V
.HAS_BALL_DECODE
[m
.vanish
[1].p
]
401 end: { x: m
.end
.x
, y: m
.end
.y
}
410 // "Sliders": at most 3 steps
411 getSlideNJumpMoves([x
, y
], steps
, oneStep
) {
413 outerLoop: for (let step
of steps
) {
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
;
424 if (V
.OnBoard(i
, j
) && this.canTake([x
, y
], [i
, j
]))
425 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
430 getPotentialPhoenixMoves(sq
) {
431 return this.getSlideNJumpMoves(sq
, V
.steps
[V
.PHOENIX
], "oneStep");
436 move.vanish
.length
== 2 &&
437 move.appear
.length
== 2 &&
438 move.appear
[0].c
!= move.appear
[1].c
449 oppositePasses(m1
, m2
) {
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
459 const L
= this.pmoves
.length
;
460 const lp
= this.pmoves
[L
-1];
461 if (!lp
) return moves
;
462 return moves
.filter(m
=> {
464 m
.vanish
.length
== 1 ||
465 m
.appear
.length
== 1 ||
466 m
.appear
[0].c
== m
.appear
[1].c
||
467 !this.oppositePasses(lp
, m
)
472 // isAttacked: unused here (no checks)
475 this.pmoves
.push(this.getPmove(move));
488 const color
= V
.GetOppCol(this.turn
);
489 const lastRank
= (color
== "w" ? 0 : 8);
493 Object
.keys(V
.HAS_BALL_DECODE
).includes(
494 this.board
[lastRank
][i
].charAt(1)) &&
495 this.getColor(lastRank
, i
) == color
500 return color
== "w" ? "1-0" : "0-1";
502 if (this.atLeastOneMove()) return "*";
503 // Stalemate (quite unlikely?)
507 static get VALUES() {
519 static get SEARCH_DEPTH() {
525 let evaluation
= super.evalPosition();
526 if (this.board
[4][4] == V
.BALL
)
527 // Ball not captured yet
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
);
536 return 0; //never reached
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
) {
548 if (move.vanish
.length
> move.appear
.length
) {
550 const startColumn
= V
.CoordToColumn(move.start
.y
);
551 notation
= startColumn
+ "x" + finalSquare
;
553 else notation
= finalSquare
;
554 if (![V
.PAWN
, V
.HAS_BALL_CODE
[V
.PAWN
]].includes(move.appear
[0].p
)) {
557 V
.HAS_BALL_DECODE
[move.appear
[0].p
] || move.appear
[0].p
;
558 notation
+= "=" + promotePiece
.toUpperCase();
564 piece
.toUpperCase() +
565 (move.vanish
.length
> move.appear
.length
? "x" : "") +