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