Some fixes, and add 2 variants: Checkless and Parachute
[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
18 static get CHAMPION() {
19 return 'c';
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 'c': 'h'
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 'h': 'c'
54 };
55 }
56
57 static get PIECES() {
58 return ChessRules.PIECES
59 .concat([V.CHAMPION])
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.CHAMPION])
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 "rnbcqcnbr/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/RNBCQCNBR 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, totally freely
138 let positions = shuffle(ArrayFun.range(9));
139 const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'c', 'c', 'q'];
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
159 scanKings() {}
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() {
173 return Object.assign(
174 {},
175 ChessRules.steps,
176 // Add champion moves
177 {
178 c: [
179 [-2, -2],
180 [-2, 0],
181 [-2, 2],
182 [0, -2],
183 [0, 2],
184 [2, -2],
185 [2, 0],
186 [2, 2],
187 [-1, 0],
188 [1, 0],
189 [0, -1],
190 [0, 1]
191 ]
192 }
193 );
194 }
195
196 // Because of the ball, getPiece() could be wrong:
197 // use board[x][y][1] instead (always valid).
198 getBasicMove([sx, sy], [ex, ey], tr) {
199 const initColor = this.getColor(sx, sy);
200 const initPiece = this.board[sx][sy].charAt(1);
201 let mv = new Move({
202 appear: [
203 new PiPo({
204 x: ex,
205 y: ey,
206 c: tr ? tr.c : initColor,
207 p: tr ? tr.p : initPiece
208 })
209 ],
210 vanish: [
211 new PiPo({
212 x: sx,
213 y: sy,
214 c: initColor,
215 p: initPiece
216 })
217 ]
218 });
219
220 // Fix "ball holding" indication in case of promotions:
221 if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece))
222 mv.appear[0].p = V.HAS_BALL_CODE[tr.p];
223
224 // The opponent piece disappears if we take it
225 if (this.board[ex][ey] != V.EMPTY) {
226 mv.vanish.push(
227 new PiPo({
228 x: ex,
229 y: ey,
230 c: this.getColor(ex, ey),
231 p: this.board[ex][ey].charAt(1)
232 })
233 );
234 }
235
236 // Post-processing: maybe the ball was taken, or a piece + ball
237 if (mv.vanish.length == 2) {
238 if (
239 // Take the ball?
240 mv.vanish[1].c == 'a' ||
241 // Capture a ball-holding piece?
242 Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
243 ) {
244 mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p];
245 } else if (mv.vanish[1].c == mv.vanish[0].c) {
246 // Pass the ball: the passing unit does not disappear
247 mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
248 mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
249 mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p];
250 }
251 // Else: standard capture
252 }
253
254 return mv;
255 }
256
257 // NOTE: if a pawn is captured en-passant, he doesn't hold the ball
258 // So base implementation is fine.
259
260 getPotentialMovesFrom([x, y]) {
261 if (this.getPiece(x, y) == V.CHAMPION)
262 return this.getPotentialChampionMoves([x, y]);
263 return super.getPotentialMovesFrom([x, y]);
264 }
265
266 // "Sliders": at most 2 steps
267 getSlideNJumpMoves([x, y], steps, oneStep) {
268 let moves = [];
269 outerLoop: for (let step of steps) {
270 let i = x + step[0];
271 let j = y + step[1];
272 let stepCount = 1;
273 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
274 moves.push(this.getBasicMove([x, y], [i, j]));
275 if (oneStep || stepCount == 2) continue outerLoop;
276 i += step[0];
277 j += step[1];
278 stepCount++;
279 }
280 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
281 moves.push(this.getBasicMove([x, y], [i, j]));
282 }
283 return moves;
284 }
285
286 getPotentialChampionMoves(sq) {
287 return this.getSlideNJumpMoves(sq, V.steps[V.CHAMPION], "oneStep");
288 }
289
290 filterValid(moves) {
291 return moves;
292 }
293
294 // isAttacked: unused here (no checks)
295
296 postPlay() {}
297 postUndo() {}
298
299 getCurrentScore() {
300 // Turn has changed:
301 const color = V.GetOppCol(this.turn);
302 const lastRank = (color == "w" ? 0 : 8);
303 if ([3,4,5].some(
304 i => {
305 return (
306 Object.keys(V.HAS_BALL_DECODE).includes(
307 this.board[lastRank][i].charAt(1)) &&
308 this.getColor(lastRank, i) == color
309 );
310 }
311 )) {
312 // Goal scored!
313 return color == "w" ? "1-0" : "0-1";
314 }
315 if (this.atLeastOneMove()) return "*";
316 // Stalemate (quite unlikely?)
317 return "1/2";
318 }
319
320 static get VALUES() {
321 return {
322 p: 1,
323 r: 3,
324 n: 4,
325 b: 2,
326 q: 5,
327 c: 4,
328 a: 0 //ball: neutral
329 };
330 }
331
332 static get SEARCH_DEPTH() {
333 return 2;
334 }
335
336 evalPosition() {
337 // Count material:
338 let evaluation = super.evalPosition();
339 if (this.board[4][4] == V.BALL)
340 // Ball not captured yet
341 return evaluation;
342 // Ponder depending on ball position
343 for (let i=0; i<9; i++) {
344 for (let j=0; j<9; j++) {
345 if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1]))
346 return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i);
347 }
348 }
349 return 0; //never reached
350 }
351
352 getNotation(move) {
353 const finalSquare = V.CoordsToSquare(move.end);
354 if (move.appear.length == 2)
355 // A pass: special notation
356 return V.CoordsToSquare(move.start) + "P" + finalSquare;
357 const piece = this.getPiece(move.start.x, move.start.y);
358 if (piece == V.PAWN) {
359 // Pawn move
360 let notation = "";
361 if (move.vanish.length > move.appear.length) {
362 // Capture
363 const startColumn = V.CoordToColumn(move.start.y);
364 notation = startColumn + "x" + finalSquare;
365 }
366 else notation = finalSquare;
367 if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
368 // Promotion
369 const promotePiece =
370 V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
371 notation += "=" + promotePiece.toUpperCase();
372 }
373 return notation;
374 }
375 // Piece movement
376 return (
377 piece.toUpperCase() +
378 (move.vanish.length > move.appear.length ? "x" : "") +
379 finalSquare
380 );
381 }
382 };