3903e2b45dde82d828adb12e2bb384b356ea4d0a
[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 PawnSpecs() {
7 return Object.assign(
8 {},
9 ChessRules.PawnSpecs,
10 { promotions: ChessRules.PawnSpecs.promotions.concat([V.PHOENIX]) }
11 );
12 }
13
14 static get HasFlags() {
15 return false;
16 }
17
18 static get PHOENIX() {
19 return 'h';
20 }
21
22 static get BALL() {
23 // 'b' is already taken:
24 return "aa";
25 }
26
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() {
30 // return "ff";
31 // }
32
33 static get HAS_BALL_CODE() {
34 return {
35 'p': 's',
36 'r': 'u',
37 'n': 'o',
38 'b': 'd',
39 'q': 't',
40 'k': 'l',
41 'h': 'i'
42 };
43 }
44
45 static get HAS_BALL_DECODE() {
46 return {
47 's': 'p',
48 'u': 'r',
49 'o': 'n',
50 'd': 'b',
51 't': 'q',
52 'l': 'k',
53 'i': 'h'
54 };
55 }
56
57 static get PIECES() {
58 return ChessRules.PIECES
59 .concat([V.PHOENIX])
60 .concat(Object.keys(V.HAS_BALL_DECODE))
61 .concat(['a']);
62 }
63
64 static board2fen(b) {
65 if (b == V.BALL) return 'a';
66 return ChessRules.board2fen(b);
67 }
68
69 static fen2board(f) {
70 if (f == 'a') return V.BALL;
71 return ChessRules.fen2board(f);
72 }
73
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]);
82 let ballCount = 0;
83 for (let row of rows) {
84 let sumElts = 0;
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++;
90 sumElts++;
91 } else {
92 const num = parseInt(row[i]);
93 if (isNaN(num)) return false;
94 sumElts += num;
95 }
96 }
97 if (sumElts != V.size.y) return false;
98 }
99 if (ballCount != 1 || Object.values(pieces).some(v => v == 0))
100 return false;
101 return true;
102 }
103
104 getPpath(b) {
105 let prefix = "";
106 const withPrefix =
107 Object.keys(V.HAS_BALL_DECODE)
108 .concat([V.PHOENIX])
109 .concat(['a']);
110 if (withPrefix.includes(b[1])) prefix = "Ball/";
111 return prefix + b;
112 }
113
114 canTake([x1, y1], [x2, y2]) {
115 // Capture enemy or pass ball to friendly pieces
116 return (
117 this.getColor(x1, y1) !== this.getColor(x2, y2) ||
118 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1))
119 );
120 }
121
122 getCheckSquares(color) {
123 return [];
124 }
125
126 static GenRandInitFen(randomness) {
127 if (randomness == 0)
128 return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 -";
129
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'];
134 break;
135 }
136
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]];
147 }
148 }
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]];
155 }
156 }
157 for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i];
158 }
159 return (
160 pieces["b"].join("") +
161 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
162 pieces["w"].join("").toUpperCase() +
163 // En-passant allowed, but no flags
164 " w 0 -"
165 );
166 }
167
168 scanKings() {}
169
170 static get size() {
171 return { x: 9, y: 9 };
172 }
173
174 getPiece(i, j) {
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];
178 return p;
179 }
180
181 static get steps() {
182 return Object.assign(
183 {},
184 ChessRules.steps,
185 // Add phoenix moves
186 {
187 h: [
188 [-2, -2],
189 [-2, 2],
190 [2, -2],
191 [2, 2],
192 [-1, 0],
193 [1, 0],
194 [0, -1],
195 [0, 1]
196 ]
197 }
198 );
199 }
200
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);
206 let mv = new Move({
207 appear: [
208 new PiPo({
209 x: ex,
210 y: ey,
211 c: tr ? tr.c : initColor,
212 p: tr ? tr.p : initPiece
213 })
214 ],
215 vanish: [
216 new PiPo({
217 x: sx,
218 y: sy,
219 c: initColor,
220 p: initPiece
221 })
222 ]
223 });
224
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];
228
229 // The opponent piece disappears if we take it
230 if (this.board[ex][ey] != V.EMPTY) {
231 mv.vanish.push(
232 new PiPo({
233 x: ex,
234 y: ey,
235 c: this.getColor(ex, ey),
236 p: this.board[ex][ey].charAt(1)
237 })
238 );
239 }
240
241 // Post-processing: maybe the ball was taken, or a piece + ball
242 if (mv.vanish.length == 2) {
243 if (
244 // Take the ball?
245 mv.vanish[1].c == 'a' ||
246 // Capture a ball-holding piece?
247 Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
248 ) {
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];
255 }
256 // Else: standard capture
257 }
258
259 return mv;
260 }
261
262 // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
263 // So base implementation is fine.
264
265 getPotentialMovesFrom([x, y]) {
266 if (this.getPiece(x, y) == V.PHOENIX)
267 return this.getPotentialPhoenixMoves([x, y]);
268 return super.getPotentialMovesFrom([x, y]);
269 }
270
271 // "Sliders": at most 3 steps
272 getSlideNJumpMoves([x, y], steps, oneStep) {
273 let moves = [];
274 outerLoop: for (let step of steps) {
275 let i = x + step[0];
276 let j = y + step[1];
277 let stepCount = 1;
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;
281 i += step[0];
282 j += step[1];
283 stepCount++;
284 }
285 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
286 moves.push(this.getBasicMove([x, y], [i, j]));
287 }
288 return moves;
289 }
290
291 getPotentialPhoenixMoves(sq) {
292 return this.getSlideNJumpMoves(sq, V.steps[V.PHOENIX], "oneStep");
293 }
294
295 filterValid(moves) {
296 return moves;
297 }
298
299 // isAttacked: unused here (no checks)
300
301 postPlay() {}
302 postUndo() {}
303
304 getCurrentScore() {
305 // Turn has changed:
306 const color = V.GetOppCol(this.turn);
307 const lastRank = (color == "w" ? 0 : 8);
308 if ([3,4,5].some(
309 i => {
310 return (
311 Object.keys(V.HAS_BALL_DECODE).includes(
312 this.board[lastRank][i].charAt(1)) &&
313 this.getColor(lastRank, i) == color
314 );
315 }
316 )) {
317 // Goal scored!
318 return color == "w" ? "1-0" : "0-1";
319 }
320 if (this.atLeastOneMove()) return "*";
321 // Stalemate (quite unlikely?)
322 return "1/2";
323 }
324
325 static get VALUES() {
326 return {
327 p: 1,
328 r: 3,
329 n: 3,
330 b: 2,
331 q: 5,
332 h: 3,
333 a: 0 //ball: neutral
334 };
335 }
336
337 static get SEARCH_DEPTH() {
338 return 2;
339 }
340
341 evalPosition() {
342 // Count material:
343 let evaluation = super.evalPosition();
344 if (this.board[4][4] == V.BALL)
345 // Ball not captured yet
346 return evaluation;
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);
352 }
353 }
354 return 0; //never reached
355 }
356
357 getNotation(move) {
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) {
364 // Pawn move
365 let notation = "";
366 if (move.vanish.length > move.appear.length) {
367 // Capture
368 const startColumn = V.CoordToColumn(move.start.y);
369 notation = startColumn + "x" + finalSquare;
370 }
371 else notation = finalSquare;
372 if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
373 // Promotion
374 const promotePiece =
375 V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
376 notation += "=" + promotePiece.toUpperCase();
377 }
378 return notation;
379 }
380 // Piece movement
381 return (
382 piece.toUpperCase() +
383 (move.vanish.length > move.appear.length ? "x" : "") +
384 finalSquare
385 );
386 }
387 };