3903e2b45dde82d828adb12e2bb384b356ea4d0a
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
{
6 static get PawnSpecs() {
10 { promotions: ChessRules
.PawnSpecs
.promotions
.concat([V
.PHOENIX
]) }
14 static get HasFlags() {
18 static get PHOENIX() {
23 // 'b' is already taken:
27 // Special code for "something to fill space" (around goals)
28 // --> If goal is outside the board (current prototype: it's inside)
29 // static get FILL() {
33 static get HAS_BALL_CODE() {
45 static get HAS_BALL_DECODE() {
58 return ChessRules
.PIECES
60 .concat(Object
.keys(V
.HAS_BALL_DECODE
))
65 if (b
== V
.BALL
) return 'a';
66 return ChessRules
.board2fen(b
);
70 if (f
== 'a') return V
.BALL
;
71 return ChessRules
.fen2board(f
);
74 // Check that exactly one ball is on the board
75 // + at least one piece per color.
76 static IsGoodPosition(position
) {
77 if (position
.length
== 0) return false;
78 const rows
= position
.split("/");
79 if (rows
.length
!= V
.size
.x
) return false;
80 let pieces
= { "w": 0, "b": 0 };
81 const withBall
= Object
.keys(V
.HAS_BALL_DECODE
).concat([V
.BALL
]);
83 for (let row
of rows
) {
85 for (let i
= 0; i
< row
.length
; i
++) {
86 const lowerRi
= row
[i
].toLowerCase();
87 if (V
.PIECES
.includes(lowerRi
)) {
88 if (lowerRi
!= V
.BALL
) pieces
[row
[i
] == lowerRi
? "b" : "w"]++;
89 if (withBall
.includes(lowerRi
)) ballCount
++;
92 const num
= parseInt(row
[i
]);
93 if (isNaN(num
)) return false;
97 if (sumElts
!= V
.size
.y
) return false;
99 if (ballCount
!= 1 || Object
.values(pieces
).some(v
=> v
== 0))
107 Object
.keys(V
.HAS_BALL_DECODE
)
110 if (withPrefix
.includes(b
[1])) prefix
= "Ball/";
114 canTake([x1
, y1
], [x2
, y2
]) {
115 // Capture enemy or pass ball to friendly pieces
117 this.getColor(x1
, y1
) !== this.getColor(x2
, y2
) ||
118 Object
.keys(V
.HAS_BALL_DECODE
).includes(this.board
[x1
][y1
].charAt(1))
122 getCheckSquares(color
) {
126 static GenRandInitFen(randomness
) {
128 return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 -";
130 let pieces
= { w: new Array(9), b: new Array(9) };
131 for (let c
of ["w", "b"]) {
132 if (c
== 'b' && randomness
== 1) {
133 pieces
['b'] = pieces
['w'];
137 // Get random squares for every piece, with bishops and phoenixes
138 // on different colors:
139 let positions
= shuffle(ArrayFun
.range(9));
140 const composition
= ['b', 'b', 'h', 'h', 'n', 'n', 'r', 'r', 'q'];
141 let rem2
= positions
[0] % 2;
142 if (rem2
== positions
[1] % 2) {
143 // Fix bishops (on different colors)
144 for (let i
=4; i
<9; i
++) {
145 if (positions
[i
] % 2 != rem2
)
146 [positions
[1], positions
[i
]] = [positions
[i
], positions
[1]];
149 rem2
= positions
[2] % 2;
150 if (rem2
== positions
[3] % 2) {
151 // Fix phoenixes too:
152 for (let i
=4; i
<9; i
++) {
153 if (positions
[i
] % 2 != rem2
)
154 [positions
[3], positions
[i
]] = [positions
[i
], positions
[3]];
157 for (let i
= 0; i
< 9; i
++) pieces
[c
][positions
[i
]] = composition
[i
];
160 pieces
["b"].join("") +
161 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
162 pieces
["w"].join("").toUpperCase() +
163 // En-passant allowed, but no flags
171 return { x: 9, y: 9 };
175 const p
= this.board
[i
][j
].charAt(1);
176 if (Object
.keys(V
.HAS_BALL_DECODE
).includes(p
))
177 return V
.HAS_BALL_DECODE
[p
];
182 return Object
.assign(
201 // Because of the ball, getPiece() could be wrong:
202 // use board[x][y][1] instead (always valid).
203 getBasicMove([sx
, sy
], [ex
, ey
], tr
) {
204 const initColor
= this.getColor(sx
, sy
);
205 const initPiece
= this.board
[sx
][sy
].charAt(1);
211 c: tr
? tr
.c : initColor
,
212 p: tr
? tr
.p : initPiece
225 // Fix "ball holding" indication in case of promotions:
226 if (!!tr
&& Object
.keys(V
.HAS_BALL_DECODE
).includes(initPiece
))
227 mv
.appear
[0].p
= V
.HAS_BALL_CODE
[tr
.p
];
229 // The opponent piece disappears if we take it
230 if (this.board
[ex
][ey
] != V
.EMPTY
) {
235 c: this.getColor(ex
, ey
),
236 p: this.board
[ex
][ey
].charAt(1)
241 // Post-processing: maybe the ball was taken, or a piece + ball
242 if (mv
.vanish
.length
== 2) {
245 mv
.vanish
[1].c
== 'a' ||
246 // Capture a ball-holding piece?
247 Object
.keys(V
.HAS_BALL_DECODE
).includes(mv
.vanish
[1].p
)
249 mv
.appear
[0].p
= V
.HAS_BALL_CODE
[mv
.appear
[0].p
];
250 } else if (mv
.vanish
[1].c
== mv
.vanish
[0].c
) {
251 // Pass the ball: the passing unit does not disappear
252 mv
.appear
.push(JSON
.parse(JSON
.stringify(mv
.vanish
[0])));
253 mv
.appear
[0].p
= V
.HAS_BALL_CODE
[mv
.vanish
[1].p
];
254 mv
.appear
[1].p
= V
.HAS_BALL_DECODE
[mv
.appear
[1].p
];
256 // Else: standard capture
262 // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
263 // So base implementation is fine.
265 getPotentialMovesFrom([x
, y
]) {
266 if (this.getPiece(x
, y
) == V
.PHOENIX
)
267 return this.getPotentialPhoenixMoves([x
, y
]);
268 return super.getPotentialMovesFrom([x
, y
]);
271 // "Sliders": at most 3 steps
272 getSlideNJumpMoves([x
, y
], steps
, oneStep
) {
274 outerLoop: for (let step
of steps
) {
278 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
279 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
280 if (oneStep
|| stepCount
== 3) continue outerLoop
;
285 if (V
.OnBoard(i
, j
) && this.canTake([x
, y
], [i
, j
]))
286 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
291 getPotentialPhoenixMoves(sq
) {
292 return this.getSlideNJumpMoves(sq
, V
.steps
[V
.PHOENIX
], "oneStep");
299 // isAttacked: unused here (no checks)
306 const color
= V
.GetOppCol(this.turn
);
307 const lastRank
= (color
== "w" ? 0 : 8);
311 Object
.keys(V
.HAS_BALL_DECODE
).includes(
312 this.board
[lastRank
][i
].charAt(1)) &&
313 this.getColor(lastRank
, i
) == color
318 return color
== "w" ? "1-0" : "0-1";
320 if (this.atLeastOneMove()) return "*";
321 // Stalemate (quite unlikely?)
325 static get VALUES() {
337 static get SEARCH_DEPTH() {
343 let evaluation
= super.evalPosition();
344 if (this.board
[4][4] == V
.BALL
)
345 // Ball not captured yet
347 // Ponder depending on ball position
348 for (let i
=0; i
<9; i
++) {
349 for (let j
=0; j
<9; j
++) {
350 if (Object
.keys(V
.HAS_BALL_DECODE
).includes(this.board
[i
][j
][1]))
351 return evaluation
/2 + (this.getColor(i
, j
) == "w" ? 8 - i : -i
);
354 return 0; //never reached
358 const finalSquare
= V
.CoordsToSquare(move.end
);
359 if (move.appear
.length
== 2)
360 // A pass: special notation
361 return V
.CoordsToSquare(move.start
) + "P" + finalSquare
;
362 const piece
= this.getPiece(move.start
.x
, move.start
.y
);
363 if (piece
== V
.PAWN
) {
366 if (move.vanish
.length
> move.appear
.length
) {
368 const startColumn
= V
.CoordToColumn(move.start
.y
);
369 notation
= startColumn
+ "x" + finalSquare
;
371 else notation
= finalSquare
;
372 if (![V
.PAWN
, V
.HAS_BALL_CODE
[V
.PAWN
]].includes(move.appear
[0].p
)) {
375 V
.HAS_BALL_DECODE
[move.appear
[0].p
] || move.appear
[0].p
;
376 notation
+= "=" + promotePiece
.toUpperCase();
382 piece
.toUpperCase() +
383 (move.vanish
.length
> move.appear
.length
? "x" : "") +