Better Ball rules (I think...)
[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 if (this.getColor(x1, y1) !== this.getColor(x2, y2)) {
116 // The piece holding the ball cannot capture:
117 return (
118 !(Object.keys(V.HAS_BALL_DECODE)
119 .includes(this.board[x1][y1].charAt(1)))
120 );
121 }
122 // Pass: possible only if one of the friendly pieces has the ball
123 return (
124 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1)) ||
125 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x2][y2].charAt(1))
126 );
127 }
128
129 getCheckSquares(color) {
130 return [];
131 }
132
133 static GenRandInitFen(randomness) {
134 if (randomness == 0)
135 return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 -";
136
137 let pieces = { w: new Array(9), b: new Array(9) };
138 for (let c of ["w", "b"]) {
139 if (c == 'b' && randomness == 1) {
140 pieces['b'] = pieces['w'];
141 break;
142 }
143
144 // Get random squares for every piece, with bishops and phoenixes
145 // on different colors:
146 let positions = shuffle(ArrayFun.range(9));
147 const composition = ['b', 'b', 'h', 'h', 'n', 'n', 'r', 'r', 'q'];
148 let rem2 = positions[0] % 2;
149 if (rem2 == positions[1] % 2) {
150 // Fix bishops (on different colors)
151 for (let i=4; i<9; i++) {
152 if (positions[i] % 2 != rem2)
153 [positions[1], positions[i]] = [positions[i], positions[1]];
154 }
155 }
156 rem2 = positions[2] % 2;
157 if (rem2 == positions[3] % 2) {
158 // Fix phoenixes too:
159 for (let i=4; i<9; i++) {
160 if (positions[i] % 2 != rem2)
161 [positions[3], positions[i]] = [positions[i], positions[3]];
162 }
163 }
164 for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i];
165 }
166 return (
167 pieces["b"].join("") +
168 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
169 pieces["w"].join("").toUpperCase() +
170 // En-passant allowed, but no flags
171 " w 0 -"
172 );
173 }
174
175 scanKings() {}
176
177 static get size() {
178 return { x: 9, y: 9 };
179 }
180
181 getPiece(i, j) {
182 const p = this.board[i][j].charAt(1);
183 if (Object.keys(V.HAS_BALL_DECODE).includes(p))
184 return V.HAS_BALL_DECODE[p];
185 return p;
186 }
187
188 static get steps() {
189 return Object.assign(
190 {},
191 ChessRules.steps,
192 // Add phoenix moves
193 {
194 h: [
195 [-2, -2],
196 [-2, 2],
197 [2, -2],
198 [2, 2],
199 [-1, 0],
200 [1, 0],
201 [0, -1],
202 [0, 1]
203 ]
204 }
205 );
206 }
207
208 // Because of the ball, getPiece() could be wrong:
209 // use board[x][y][1] instead (always valid).
210 getBasicMove([sx, sy], [ex, ey], tr) {
211 const initColor = this.getColor(sx, sy);
212 const initPiece = this.board[sx][sy].charAt(1);
213 let mv = new Move({
214 appear: [
215 new PiPo({
216 x: ex,
217 y: ey,
218 c: tr ? tr.c : initColor,
219 p: tr ? tr.p : initPiece
220 })
221 ],
222 vanish: [
223 new PiPo({
224 x: sx,
225 y: sy,
226 c: initColor,
227 p: initPiece
228 })
229 ]
230 });
231
232 // Fix "ball holding" indication in case of promotions:
233 if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece))
234 mv.appear[0].p = V.HAS_BALL_CODE[tr.p];
235
236 // The opponent piece disappears if we take it
237 if (this.board[ex][ey] != V.EMPTY) {
238 mv.vanish.push(
239 new PiPo({
240 x: ex,
241 y: ey,
242 c: this.getColor(ex, ey),
243 p: this.board[ex][ey].charAt(1)
244 })
245 );
246 }
247
248 // Post-processing: maybe the ball was taken, or a piece + ball,
249 // or maybe a pass (ball <--> piece)
250 if (mv.vanish.length == 2) {
251 if (
252 // Take the ball?
253 mv.vanish[1].c == 'a' ||
254 // Capture a ball-holding piece? If friendly one, then adjust
255 Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
256 ) {
257 mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p];
258 if (mv.vanish[1].c == mv.vanish[0].c) {
259 // "Capturing" self => pass
260 mv.appear[0].x = mv.start.x;
261 mv.appear[0].y = mv.start.y;
262 mv.appear.push(
263 new PiPo({
264 x: mv.end.x,
265 y: mv.end.y,
266 p: V.HAS_BALL_DECODE[mv.vanish[1].p],
267 c: mv.vanish[0].c
268 })
269 );
270 }
271 } else if (mv.vanish[1].c == mv.vanish[0].c) {
272 // Pass the ball: the passing unit does not disappear
273 mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
274 mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
275 mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p];
276 }
277 // Else: standard capture
278 }
279
280 return mv;
281 }
282
283 // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
284 // So base implementation is fine.
285
286 getPotentialMovesFrom([x, y]) {
287 if (this.getPiece(x, y) == V.PHOENIX)
288 return this.getPotentialPhoenixMoves([x, y]);
289 return super.getPotentialMovesFrom([x, y]);
290 }
291
292 // "Sliders": at most 3 steps
293 getSlideNJumpMoves([x, y], steps, oneStep) {
294 let moves = [];
295 outerLoop: for (let step of steps) {
296 let i = x + step[0];
297 let j = y + step[1];
298 let stepCount = 1;
299 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
300 moves.push(this.getBasicMove([x, y], [i, j]));
301 if (oneStep || stepCount == 3) continue outerLoop;
302 i += step[0];
303 j += step[1];
304 stepCount++;
305 }
306 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
307 moves.push(this.getBasicMove([x, y], [i, j]));
308 }
309 return moves;
310 }
311
312 getPotentialPhoenixMoves(sq) {
313 return this.getSlideNJumpMoves(sq, V.steps[V.PHOENIX], "oneStep");
314 }
315
316 filterValid(moves) {
317 return moves;
318 }
319
320 // isAttacked: unused here (no checks)
321
322 postPlay() {}
323 postUndo() {}
324
325 getCurrentScore() {
326 // Turn has changed:
327 const color = V.GetOppCol(this.turn);
328 const lastRank = (color == "w" ? 0 : 8);
329 if ([3,4,5].some(
330 i => {
331 return (
332 Object.keys(V.HAS_BALL_DECODE).includes(
333 this.board[lastRank][i].charAt(1)) &&
334 this.getColor(lastRank, i) == color
335 );
336 }
337 )) {
338 // Goal scored!
339 return color == "w" ? "1-0" : "0-1";
340 }
341 if (this.atLeastOneMove()) return "*";
342 // Stalemate (quite unlikely?)
343 return "1/2";
344 }
345
346 static get VALUES() {
347 return {
348 p: 1,
349 r: 3,
350 n: 3,
351 b: 2,
352 q: 5,
353 h: 3,
354 a: 0 //ball: neutral
355 };
356 }
357
358 static get SEARCH_DEPTH() {
359 return 2;
360 }
361
362 evalPosition() {
363 // Count material:
364 let evaluation = super.evalPosition();
365 if (this.board[4][4] == V.BALL)
366 // Ball not captured yet
367 return evaluation;
368 // Ponder depending on ball position
369 for (let i=0; i<9; i++) {
370 for (let j=0; j<9; j++) {
371 if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1]))
372 return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i);
373 }
374 }
375 return 0; //never reached
376 }
377
378 getNotation(move) {
379 const finalSquare = V.CoordsToSquare(move.end);
380 if (move.appear.length == 2)
381 // A pass: special notation
382 return V.CoordsToSquare(move.start) + "P" + finalSquare;
383 const piece = this.getPiece(move.start.x, move.start.y);
384 if (piece == V.PAWN) {
385 // Pawn move
386 let notation = "";
387 if (move.vanish.length > move.appear.length) {
388 // Capture
389 const startColumn = V.CoordToColumn(move.start.y);
390 notation = startColumn + "x" + finalSquare;
391 }
392 else notation = finalSquare;
393 if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
394 // Promotion
395 const promotePiece =
396 V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
397 notation += "=" + promotePiece.toUpperCase();
398 }
399 return notation;
400 }
401 // Piece movement
402 return (
403 piece.toUpperCase() +
404 (move.vanish.length > move.appear.length ? "x" : "") +
405 finalSquare
406 );
407 }
408 };