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