Add unambiguous section in the PGN + some fixes + code formatting and fix typos
[vchess.git] / client / src / variants / Ball.js
CommitLineData
6f2f9437 1import { ChessRules, Move, PiPo } from "@/base_rules";
6f2f9437
BA
2import { ArrayFun } from "@/utils/array";
3import { shuffle } from "@/utils/alea";
4
5export class BallRules extends ChessRules {
6 static get PawnSpecs() {
7 return Object.assign(
8 {},
9 ChessRules.PawnSpecs,
2c5d7b20 10 { promotions: ChessRules.PawnSpecs.promotions.concat([V.PHOENIX]) }
6f2f9437
BA
11 );
12 }
13
14 static get HasFlags() {
15 return false;
16 }
6f2f9437 17
2c5d7b20
BA
18 static get PHOENIX() {
19 return 'h';
6f2f9437
BA
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',
c0250d0a 38 'b': 'd',
6f2f9437
BA
39 'q': 't',
40 'k': 'l',
2c5d7b20 41 'h': 'i'
6f2f9437
BA
42 };
43 }
44
45 static get HAS_BALL_DECODE() {
46 return {
47 's': 'p',
48 'u': 'r',
49 'o': 'n',
c0250d0a 50 'd': 'b',
6f2f9437
BA
51 't': 'q',
52 'l': 'k',
2c5d7b20 53 'i': 'h'
6f2f9437
BA
54 };
55 }
56
57 static get PIECES() {
58 return ChessRules.PIECES
2c5d7b20 59 .concat([V.PHOENIX])
6f2f9437
BA
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)
2c5d7b20 108 .concat([V.PHOENIX])
6f2f9437
BA
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)
c0250d0a 128 return "rnbcqcnbr/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/RNBCQCNBR w 0 -";
6f2f9437
BA
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, totally freely
138 let positions = shuffle(ArrayFun.range(9));
c0250d0a 139 const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'c', 'c', 'q'];
6f2f9437
BA
140 const rem2 = positions[0] % 2;
141 if (rem2 == positions[1] % 2) {
142 // Fix bishops (on different colors)
143 for (let i=2; i<9; i++) {
144 if (positions[i] % 2 != rem2)
145 [positions[1], positions[i]] = [positions[i], positions[1]];
146 }
147 }
148 for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i];
149 }
150 return (
151 pieces["b"].join("") +
152 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
153 pieces["w"].join("").toUpperCase() +
154 // En-passant allowed, but no flags
155 " w 0 -"
156 );
157 }
158
d54f6261 159 scanKings() {}
6f2f9437
BA
160
161 static get size() {
162 return { x: 9, y: 9 };
163 }
164
165 getPiece(i, j) {
166 const p = this.board[i][j].charAt(1);
167 if (Object.keys(V.HAS_BALL_DECODE).includes(p))
168 return V.HAS_BALL_DECODE[p];
169 return p;
170 }
171
172 static get steps() {
d54f6261
BA
173 return Object.assign(
174 {},
175 ChessRules.steps,
2c5d7b20 176 // Add phoenix moves
d54f6261 177 {
2c5d7b20 178 h: [
d54f6261 179 [-2, -2],
d54f6261 180 [-2, 2],
d54f6261 181 [2, -2],
d54f6261
BA
182 [2, 2],
183 [-1, 0],
184 [1, 0],
185 [0, -1],
186 [0, 1]
187 ]
188 }
189 );
6f2f9437
BA
190 }
191
192 // Because of the ball, getPiece() could be wrong:
193 // use board[x][y][1] instead (always valid).
194 getBasicMove([sx, sy], [ex, ey], tr) {
195 const initColor = this.getColor(sx, sy);
196 const initPiece = this.board[sx][sy].charAt(1);
197 let mv = new Move({
198 appear: [
199 new PiPo({
200 x: ex,
201 y: ey,
202 c: tr ? tr.c : initColor,
203 p: tr ? tr.p : initPiece
204 })
205 ],
206 vanish: [
207 new PiPo({
208 x: sx,
209 y: sy,
210 c: initColor,
211 p: initPiece
212 })
213 ]
214 });
215
216 // Fix "ball holding" indication in case of promotions:
217 if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece))
218 mv.appear[0].p = V.HAS_BALL_CODE[tr.p];
219
220 // The opponent piece disappears if we take it
221 if (this.board[ex][ey] != V.EMPTY) {
222 mv.vanish.push(
223 new PiPo({
224 x: ex,
225 y: ey,
226 c: this.getColor(ex, ey),
227 p: this.board[ex][ey].charAt(1)
228 })
229 );
230 }
231
232 // Post-processing: maybe the ball was taken, or a piece + ball
233 if (mv.vanish.length == 2) {
234 if (
235 // Take the ball?
236 mv.vanish[1].c == 'a' ||
237 // Capture a ball-holding piece?
238 Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
239 ) {
240 mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p];
241 } else if (mv.vanish[1].c == mv.vanish[0].c) {
242 // Pass the ball: the passing unit does not disappear
243 mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
244 mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
245 mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p];
246 }
247 // Else: standard capture
248 }
249
250 return mv;
251 }
252
253 // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
254 // So base implementation is fine.
255
256 getPotentialMovesFrom([x, y]) {
2c5d7b20
BA
257 if (this.getPiece(x, y) == V.PHOENIX)
258 return this.getPotentialPhoenixMoves([x, y]);
6f2f9437
BA
259 return super.getPotentialMovesFrom([x, y]);
260 }
261
d54f6261
BA
262 // "Sliders": at most 2 steps
263 getSlideNJumpMoves([x, y], steps, oneStep) {
264 let moves = [];
265 outerLoop: for (let step of steps) {
266 let i = x + step[0];
267 let j = y + step[1];
268 let stepCount = 1;
269 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
270 moves.push(this.getBasicMove([x, y], [i, j]));
271 if (oneStep || stepCount == 2) continue outerLoop;
272 i += step[0];
273 j += step[1];
274 stepCount++;
275 }
276 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
277 moves.push(this.getBasicMove([x, y], [i, j]));
278 }
279 return moves;
280 }
281
2c5d7b20
BA
282 getPotentialPhoenixMoves(sq) {
283 return this.getSlideNJumpMoves(sq, V.steps[V.PHOENIX], "oneStep");
6f2f9437
BA
284 }
285
286 filterValid(moves) {
287 return moves;
288 }
289
290 // isAttacked: unused here (no checks)
291
292 postPlay() {}
293 postUndo() {}
294
295 getCurrentScore() {
296 // Turn has changed:
297 const color = V.GetOppCol(this.turn);
298 const lastRank = (color == "w" ? 0 : 8);
299 if ([3,4,5].some(
300 i => {
301 return (
302 Object.keys(V.HAS_BALL_DECODE).includes(
303 this.board[lastRank][i].charAt(1)) &&
304 this.getColor(lastRank, i) == color
305 );
306 }
307 )) {
308 // Goal scored!
309 return color == "w" ? "1-0" : "0-1";
310 }
311 if (this.atLeastOneMove()) return "*";
312 // Stalemate (quite unlikely?)
313 return "1/2";
314 }
315
316 static get VALUES() {
317 return {
318 p: 1,
c0250d0a 319 r: 3,
2c5d7b20 320 n: 3,
c0250d0a
BA
321 b: 2,
322 q: 5,
2c5d7b20 323 h: 3,
6f2f9437
BA
324 a: 0 //ball: neutral
325 };
326 }
327
328 static get SEARCH_DEPTH() {
329 return 2;
330 }
331
332 evalPosition() {
333 // Count material:
334 let evaluation = super.evalPosition();
335 if (this.board[4][4] == V.BALL)
336 // Ball not captured yet
337 return evaluation;
338 // Ponder depending on ball position
339 for (let i=0; i<9; i++) {
340 for (let j=0; j<9; j++) {
341 if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1]))
342 return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i);
343 }
344 }
345 return 0; //never reached
346 }
347
348 getNotation(move) {
349 const finalSquare = V.CoordsToSquare(move.end);
350 if (move.appear.length == 2)
351 // A pass: special notation
352 return V.CoordsToSquare(move.start) + "P" + finalSquare;
353 const piece = this.getPiece(move.start.x, move.start.y);
354 if (piece == V.PAWN) {
355 // Pawn move
356 let notation = "";
357 if (move.vanish.length > move.appear.length) {
358 // Capture
359 const startColumn = V.CoordToColumn(move.start.y);
360 notation = startColumn + "x" + finalSquare;
361 }
362 else notation = finalSquare;
363 if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
364 // Promotion
365 const promotePiece =
366 V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
367 notation += "=" + promotePiece.toUpperCase();
368 }
369 return notation;
370 }
371 // Piece movement
372 return (
373 piece.toUpperCase() +
374 (move.vanish.length > move.appear.length ? "x" : "") +
375 finalSquare
376 );
377 }
378};