5a23e9f83b7f7022d8f6346de5920218f7572090
[vchess.git] / client / src / variants / Ball.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { WildebeestRules } from "@/variants/Wildebeest";
3 import { ArrayFun } from "@/utils/array";
4 import { shuffle } from "@/utils/alea";
5
6 export class BallRules extends ChessRules {
7 static get PawnSpecs() {
8 return Object.assign(
9 {},
10 ChessRules.PawnSpecs,
11 { promotions: ChessRules.PawnSpecs.promotions.concat([V.WILDEBEEST]) }
12 );
13 }
14
15 static get HasFlags() {
16 return false;
17 }
18 static get HasCastle() {
19 return false;
20 }
21
22 static get WILDEBEEST() {
23 return 'w';
24 }
25
26 static get BALL() {
27 // 'b' is already taken:
28 return "aa";
29 }
30
31 // Special code for "something to fill space" (around goals)
32 // --> If goal is outside the board (current prototype: it's inside)
33 // static get FILL() {
34 // return "ff";
35 // }
36
37 static get HAS_BALL_CODE() {
38 return {
39 'p': 's',
40 'r': 'u',
41 'n': 'o',
42 'b': 'c',
43 'q': 't',
44 'k': 'l',
45 'w': 'y'
46 };
47 }
48
49 static get HAS_BALL_DECODE() {
50 return {
51 's': 'p',
52 'u': 'r',
53 'o': 'n',
54 'c': 'b',
55 't': 'q',
56 'l': 'k',
57 'y': 'w'
58 };
59 }
60
61 static get PIECES() {
62 return ChessRules.PIECES
63 .concat([V.WILDEBEEST])
64 .concat(Object.keys(V.HAS_BALL_DECODE))
65 .concat(['a']);
66 }
67
68 static board2fen(b) {
69 if (b == V.BALL) return 'a';
70 return ChessRules.board2fen(b);
71 }
72
73 static fen2board(f) {
74 if (f == 'a') return V.BALL;
75 return ChessRules.fen2board(f);
76 }
77
78 // Check that exactly one ball is on the board
79 // + at least one piece per color.
80 static IsGoodPosition(position) {
81 if (position.length == 0) return false;
82 const rows = position.split("/");
83 if (rows.length != V.size.x) return false;
84 let pieces = { "w": 0, "b": 0 };
85 const withBall = Object.keys(V.HAS_BALL_DECODE).concat([V.BALL]);
86 let ballCount = 0;
87 for (let row of rows) {
88 let sumElts = 0;
89 for (let i = 0; i < row.length; i++) {
90 const lowerRi = row[i].toLowerCase();
91 if (V.PIECES.includes(lowerRi)) {
92 if (lowerRi != V.BALL) pieces[row[i] == lowerRi ? "b" : "w"]++;
93 if (withBall.includes(lowerRi)) ballCount++;
94 sumElts++;
95 } else {
96 const num = parseInt(row[i]);
97 if (isNaN(num)) return false;
98 sumElts += num;
99 }
100 }
101 if (sumElts != V.size.y) return false;
102 }
103 if (ballCount != 1 || Object.values(pieces).some(v => v == 0))
104 return false;
105 return true;
106 }
107
108 getPpath(b) {
109 let prefix = "";
110 const withPrefix =
111 Object.keys(V.HAS_BALL_DECODE)
112 .concat([V.WILDEBEEST])
113 .concat(['a']);
114 if (withPrefix.includes(b[1])) prefix = "Ball/";
115 return prefix + b;
116 }
117
118 canTake([x1, y1], [x2, y2]) {
119 // Capture enemy or pass ball to friendly pieces
120 return (
121 this.getColor(x1, y1) !== this.getColor(x2, y2) ||
122 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1))
123 );
124 }
125
126 getCheckSquares(color) {
127 return [];
128 }
129
130 static GenRandInitFen(randomness) {
131 if (randomness == 0)
132 return "rnbqkwnbr/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/RNBQKWNBR w 0 -";
133
134 let pieces = { w: new Array(9), b: new Array(9) };
135 for (let c of ["w", "b"]) {
136 if (c == 'b' && randomness == 1) {
137 pieces['b'] = pieces['w'];
138 break;
139 }
140
141 // Get random squares for every piece, totally freely
142 let positions = shuffle(ArrayFun.range(9));
143 const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q', 'w'];
144 const rem2 = positions[0] % 2;
145 if (rem2 == positions[1] % 2) {
146 // Fix bishops (on different colors)
147 for (let i=2; i<9; i++) {
148 if (positions[i] % 2 != rem2)
149 [positions[1], positions[i]] = [positions[i], positions[1]];
150 }
151 }
152 for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i];
153 }
154 return (
155 pieces["b"].join("") +
156 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
157 pieces["w"].join("").toUpperCase() +
158 // En-passant allowed, but no flags
159 " w 0 -"
160 );
161 }
162
163 scanKings(fen) {}
164
165 static get size() {
166 return { x: 9, y: 9 };
167 }
168
169 getPiece(i, j) {
170 const p = this.board[i][j].charAt(1);
171 if (Object.keys(V.HAS_BALL_DECODE).includes(p))
172 return V.HAS_BALL_DECODE[p];
173 return p;
174 }
175
176 static get steps() {
177 return WildebeestRules.steps;
178 }
179
180 // Because of the ball, getPiece() could be wrong:
181 // use board[x][y][1] instead (always valid).
182 getBasicMove([sx, sy], [ex, ey], tr) {
183 const initColor = this.getColor(sx, sy);
184 const initPiece = this.board[sx][sy].charAt(1);
185 let mv = new Move({
186 appear: [
187 new PiPo({
188 x: ex,
189 y: ey,
190 c: tr ? tr.c : initColor,
191 p: tr ? tr.p : initPiece
192 })
193 ],
194 vanish: [
195 new PiPo({
196 x: sx,
197 y: sy,
198 c: initColor,
199 p: initPiece
200 })
201 ]
202 });
203
204 // Fix "ball holding" indication in case of promotions:
205 if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece))
206 mv.appear[0].p = V.HAS_BALL_CODE[tr.p];
207
208 // The opponent piece disappears if we take it
209 if (this.board[ex][ey] != V.EMPTY) {
210 mv.vanish.push(
211 new PiPo({
212 x: ex,
213 y: ey,
214 c: this.getColor(ex, ey),
215 p: this.board[ex][ey].charAt(1)
216 })
217 );
218 }
219
220 // Post-processing: maybe the ball was taken, or a piece + ball
221 if (mv.vanish.length == 2) {
222 if (
223 // Take the ball?
224 mv.vanish[1].c == 'a' ||
225 // Capture a ball-holding piece?
226 Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
227 ) {
228 mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p];
229 } else if (mv.vanish[1].c == mv.vanish[0].c) {
230 // Pass the ball: the passing unit does not disappear
231 mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
232 mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
233 mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p];
234 }
235 // Else: standard capture
236 }
237
238 return mv;
239 }
240
241 // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
242 // So base implementation is fine.
243
244 getPotentialMovesFrom([x, y]) {
245 if (this.getPiece(x, y) == V.WILDEBEEST)
246 return this.getPotentialWildebeestMoves([x, y]);
247 return super.getPotentialMovesFrom([x, y]);
248 }
249
250 getPotentialWildebeestMoves(sq) {
251 return this.getSlideNJumpMoves(
252 sq,
253 V.steps[V.KNIGHT].concat(V.steps[WildebeestRules.CAMEL]),
254 "oneStep"
255 );
256 }
257
258 filterValid(moves) {
259 return moves;
260 }
261
262 // isAttacked: unused here (no checks)
263
264 postPlay() {}
265 postUndo() {}
266
267 getCurrentScore() {
268 // Turn has changed:
269 const color = V.GetOppCol(this.turn);
270 const lastRank = (color == "w" ? 0 : 8);
271 if ([3,4,5].some(
272 i => {
273 return (
274 Object.keys(V.HAS_BALL_DECODE).includes(
275 this.board[lastRank][i].charAt(1)) &&
276 this.getColor(lastRank, i) == color
277 );
278 }
279 )) {
280 // Goal scored!
281 return color == "w" ? "1-0" : "0-1";
282 }
283 if (this.atLeastOneMove()) return "*";
284 // Stalemate (quite unlikely?)
285 return "1/2";
286 }
287
288 static get VALUES() {
289 return {
290 p: 1,
291 r: 5,
292 n: 3,
293 b: 3,
294 q: 9,
295 w: 7,
296 k: 5,
297 a: 0 //ball: neutral
298 };
299 }
300
301 static get SEARCH_DEPTH() {
302 return 2;
303 }
304
305 evalPosition() {
306 // Count material:
307 let evaluation = super.evalPosition();
308 if (this.board[4][4] == V.BALL)
309 // Ball not captured yet
310 return evaluation;
311 // Ponder depending on ball position
312 for (let i=0; i<9; i++) {
313 for (let j=0; j<9; j++) {
314 if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1]))
315 return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i);
316 }
317 }
318 return 0; //never reached
319 }
320
321 getNotation(move) {
322 const finalSquare = V.CoordsToSquare(move.end);
323 if (move.appear.length == 2)
324 // A pass: special notation
325 return V.CoordsToSquare(move.start) + "P" + finalSquare;
326 const piece = this.getPiece(move.start.x, move.start.y);
327 if (piece == V.PAWN) {
328 // Pawn move
329 let notation = "";
330 if (move.vanish.length > move.appear.length) {
331 // Capture
332 const startColumn = V.CoordToColumn(move.start.y);
333 notation = startColumn + "x" + finalSquare;
334 }
335 else notation = finalSquare;
336 if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
337 // Promotion
338 const promotePiece =
339 V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
340 notation += "=" + promotePiece.toUpperCase();
341 }
342 return notation;
343 }
344 // Piece movement
345 return (
346 piece.toUpperCase() +
347 (move.vanish.length > move.appear.length ? "x" : "") +
348 finalSquare
349 );
350 }
351 };