Add Fanorona
[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
7 static get Lines() {
8 return [
9 // White goal:
10 [[0, 3], [0, 6]],
11 [[0, 6], [1, 6]],
12 [[1, 6], [1, 3]],
13 [[1, 3], [0, 3]],
14 // Black goal:
15 [[9, 3], [9, 6]],
16 [[9, 6], [8, 6]],
17 [[8, 6], [8, 3]],
18 [[8, 3], [9, 3]]
19 ];
20 }
21
22 static get PawnSpecs() {
23 return Object.assign(
24 {},
25 ChessRules.PawnSpecs,
26 { promotions: ChessRules.PawnSpecs.promotions.concat([V.PHOENIX]) }
27 );
28 }
29
30 static get HasFlags() {
31 return false;
32 }
33
34 static get PHOENIX() {
35 return 'h';
36 }
37
38 static get BALL() {
39 // 'b' is already taken:
40 return "aa";
41 }
42
43 static get HAS_BALL_CODE() {
44 return {
45 'p': 's',
46 'r': 'u',
47 'n': 'o',
48 'b': 'c',
49 'q': 't',
50 'k': 'l',
51 'h': 'i'
52 };
53 }
54
55 static get HAS_BALL_DECODE() {
56 return {
57 's': 'p',
58 'u': 'r',
59 'o': 'n',
60 'c': 'b',
61 't': 'q',
62 'l': 'k',
63 'i': 'h'
64 };
65 }
66
67 static get PIECES() {
68 return ChessRules.PIECES
69 .concat([V.PHOENIX])
70 .concat(Object.keys(V.HAS_BALL_DECODE))
71 .concat(['a']);
72 }
73
74 static board2fen(b) {
75 if (b == V.BALL) return 'a';
76 return ChessRules.board2fen(b);
77 }
78
79 static fen2board(f) {
80 if (f == 'a') return V.BALL;
81 return ChessRules.fen2board(f);
82 }
83
84 static ParseFen(fen) {
85 return Object.assign(
86 ChessRules.ParseFen(fen),
87 { pmove: fen.split(" ")[4] }
88 );
89 }
90
91 // Check that exactly one ball is on the board
92 // + at least one piece per color.
93 static IsGoodPosition(position) {
94 if (position.length == 0) return false;
95 const rows = position.split("/");
96 if (rows.length != V.size.x) return false;
97 let pieces = { "w": 0, "b": 0 };
98 const withBall = Object.keys(V.HAS_BALL_DECODE).concat(['a']);
99 let ballCount = 0;
100 for (let row of rows) {
101 let sumElts = 0;
102 for (let i = 0; i < row.length; i++) {
103 const lowerRi = row[i].toLowerCase();
104 if (V.PIECES.includes(lowerRi)) {
105 if (lowerRi != 'a') pieces[row[i] == lowerRi ? "b" : "w"]++;
106 if (withBall.includes(lowerRi)) ballCount++;
107 sumElts++;
108 }
109 else {
110 const num = parseInt(row[i], 10);
111 if (isNaN(num)) return false;
112 sumElts += num;
113 }
114 }
115 if (sumElts != V.size.y) return false;
116 }
117 if (ballCount != 1 || Object.values(pieces).some(v => v == 0))
118 return false;
119 return true;
120 }
121
122 static IsGoodFen(fen) {
123 if (!ChessRules.IsGoodFen(fen)) return false;
124 const fenParts = fen.split(" ");
125 if (fenParts.length != 5) return false;
126 if (
127 fenParts[4] != "-" &&
128 !fenParts[4].match(/^([a-i][1-9]){2,2}$/)
129 ) {
130 return false;
131 }
132 return true;
133 }
134
135 getPpath(b) {
136 let prefix = "";
137 const withPrefix =
138 Object.keys(V.HAS_BALL_DECODE)
139 .concat([V.PHOENIX])
140 .concat(['a']);
141 if (withPrefix.includes(b[1])) prefix = "Ball/";
142 return prefix + b;
143 }
144
145 getPPpath(m) {
146 if (
147 m.vanish.length == 2 &&
148 m.appear.length == 2 &&
149 m.appear[0].c != m.appear[1].c
150 ) {
151 // Take ball in place (from opponent)
152 return "Ball/inplace";
153 }
154 return super.getPPpath(m);
155 }
156
157 canTake([x1, y1], [x2, y2]) {
158 if (this.getColor(x1, y1) !== this.getColor(x2, y2)) {
159 // The piece holding the ball cannot capture:
160 return (
161 !(Object.keys(V.HAS_BALL_DECODE)
162 .includes(this.board[x1][y1].charAt(1)))
163 );
164 }
165 // Pass: possible only if one of the friendly pieces has the ball
166 return (
167 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x1][y1].charAt(1)) ||
168 Object.keys(V.HAS_BALL_DECODE).includes(this.board[x2][y2].charAt(1))
169 );
170 }
171
172 getFen() {
173 return super.getFen() + " " + this.getPmoveFen();
174 }
175
176 getFenForRepeat() {
177 return super.getFenForRepeat() + "_" + this.getPmoveFen();
178 }
179
180 getPmoveFen() {
181 const L = this.pmoves.length;
182 if (!this.pmoves[L-1]) return "-";
183 return (
184 V.CoordsToSquare(this.pmoves[L-1].start) +
185 V.CoordsToSquare(this.pmoves[L-1].end)
186 );
187 }
188
189 static GenRandInitFen(randomness) {
190 if (randomness == 0)
191 return "hbnrqrnhb/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/HBNRQRNHB w 0 - -";
192
193 let pieces = { w: new Array(9), b: new Array(9) };
194 for (let c of ["w", "b"]) {
195 if (c == 'b' && randomness == 1) {
196 pieces['b'] = pieces['w'];
197 break;
198 }
199
200 // Get random squares for every piece, with bishops and phoenixes
201 // on different colors:
202 let positions = shuffle(ArrayFun.range(9));
203 const composition = ['b', 'b', 'h', 'h', 'n', 'n', 'r', 'r', 'q'];
204 let rem2 = positions[0] % 2;
205 if (rem2 == positions[1] % 2) {
206 // Fix bishops (on different colors)
207 for (let i=4; i<9; i++) {
208 if (positions[i] % 2 != rem2) {
209 [positions[1], positions[i]] = [positions[i], positions[1]];
210 break;
211 }
212 }
213 }
214 rem2 = positions[2] % 2;
215 if (rem2 == positions[3] % 2) {
216 // Fix phoenixes too:
217 for (let i=4; i<9; i++) {
218 if (positions[i] % 2 != rem2)
219 [positions[3], positions[i]] = [positions[i], positions[3]];
220 }
221 }
222 for (let i = 0; i < 9; i++) pieces[c][positions[i]] = composition[i];
223 }
224 return (
225 pieces["b"].join("") +
226 "/ppppppppp/9/9/4a4/9/9/PPPPPPPPP/" +
227 pieces["w"].join("").toUpperCase() +
228 " w 0 - -"
229 );
230 }
231
232 scanKings() {}
233
234 setOtherVariables(fen) {
235 super.setOtherVariables(fen);
236 const pmove = V.ParseFen(fen).pmove;
237 // Local stack of "pass moves" (no need for appear & vanish)
238 this.pmoves = [
239 pmove != "-"
240 ?
241 {
242 start: V.SquareToCoords(pmove.substr(0, 2)),
243 end: V.SquareToCoords(pmove.substr(2))
244 }
245 : null
246 ];
247 }
248
249 static get size() {
250 return { x: 9, y: 9 };
251 }
252
253 getPiece(i, j) {
254 const p = this.board[i][j].charAt(1);
255 if (Object.keys(V.HAS_BALL_DECODE).includes(p))
256 return V.HAS_BALL_DECODE[p];
257 return p;
258 }
259
260 static get steps() {
261 return Object.assign(
262 {},
263 ChessRules.steps,
264 // Add phoenix moves
265 {
266 h: [
267 [-2, -2],
268 [-2, 2],
269 [2, -2],
270 [2, 2],
271 [-1, 0],
272 [1, 0],
273 [0, -1],
274 [0, 1]
275 ]
276 }
277 );
278 }
279
280 // Because of the ball, getPiece() could be wrong:
281 // use board[x][y][1] instead (always valid).
282 getBasicMove([sx, sy], [ex, ey], tr) {
283 const initColor = this.getColor(sx, sy);
284 const initPiece = this.board[sx][sy].charAt(1);
285 let mv = new Move({
286 appear: [
287 new PiPo({
288 x: ex,
289 y: ey,
290 c: tr ? tr.c : initColor,
291 p: tr ? tr.p : initPiece
292 })
293 ],
294 vanish: [
295 new PiPo({
296 x: sx,
297 y: sy,
298 c: initColor,
299 p: initPiece
300 })
301 ]
302 });
303
304 // Fix "ball holding" indication in case of promotions:
305 if (!!tr && Object.keys(V.HAS_BALL_DECODE).includes(initPiece))
306 mv.appear[0].p = V.HAS_BALL_CODE[tr.p];
307
308 // The opponent piece disappears if we take it
309 if (this.board[ex][ey] != V.EMPTY) {
310 mv.vanish.push(
311 new PiPo({
312 x: ex,
313 y: ey,
314 c: this.getColor(ex, ey),
315 p: this.board[ex][ey].charAt(1)
316 })
317 );
318 }
319
320 // Post-processing: maybe the ball was taken, or a piece + ball,
321 // or maybe a pass (ball <--> piece)
322 if (mv.vanish.length == 2) {
323 if (
324 // Take the ball?
325 mv.vanish[1].c == 'a' ||
326 // Capture a ball-holding piece? If friendly one, then adjust
327 Object.keys(V.HAS_BALL_DECODE).includes(mv.vanish[1].p)
328 ) {
329 mv.appear[0].p = V.HAS_BALL_CODE[mv.appear[0].p];
330 if (mv.vanish[1].c == mv.vanish[0].c) {
331 // "Capturing" self => pass
332 mv.appear[0].x = mv.start.x;
333 mv.appear[0].y = mv.start.y;
334 mv.appear.push(
335 new PiPo({
336 x: mv.end.x,
337 y: mv.end.y,
338 p: V.HAS_BALL_DECODE[mv.vanish[1].p],
339 c: mv.vanish[0].c
340 })
341 );
342 }
343 }
344 else if (mv.vanish[1].c == mv.vanish[0].c) {
345 // Pass the ball: the passing unit does not disappear
346 mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
347 mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
348 mv.appear[1].p = V.HAS_BALL_DECODE[mv.appear[1].p];
349 }
350 // Else: standard capture
351 }
352
353 return mv;
354 }
355
356 // NOTE: if a pawn captures en-passant, he doesn't hold the ball
357 // So base implementation is fine.
358
359 getPotentialMovesFrom([x, y]) {
360 let moves = undefined;
361 const piece = this.getPiece(x, y);
362 if (piece == V.PHOENIX)
363 moves = this.getPotentialPhoenixMoves([x, y]);
364 else moves = super.getPotentialMovesFrom([x, y]);
365 // Add "taking ball in place" move (at most one in list)
366 for (let m of moves) {
367 if (
368 m.vanish.length == 2 &&
369 m.vanish[1].p != 'a' &&
370 m.vanish[0].c != m.vanish[1].c &&
371 Object.keys(V.HAS_BALL_DECODE).includes(m.appear[0].p)
372 ) {
373 const color = this.turn;
374 const oppCol = V.GetOppCol(color);
375 moves.push(
376 new Move({
377 appear: [
378 new PiPo({
379 x: x,
380 y: y,
381 c: color,
382 p: m.appear[0].p
383 }),
384 new PiPo({
385 x: m.vanish[1].x,
386 y: m.vanish[1].y,
387 c: oppCol,
388 p: V.HAS_BALL_DECODE[m.vanish[1].p]
389 })
390 ],
391 vanish: [
392 new PiPo({
393 x: x,
394 y: y,
395 c: color,
396 p: piece
397 }),
398 new PiPo({
399 x: m.vanish[1].x,
400 y: m.vanish[1].y,
401 c: oppCol,
402 p: m.vanish[1].p
403 })
404 ],
405 end: { x: m.end.x, y: m.end.y }
406 })
407 );
408 break;
409 }
410 }
411 return moves;
412 }
413
414 // "Sliders": at most 3 steps
415 getSlideNJumpMoves([x, y], steps, oneStep) {
416 let moves = [];
417 outerLoop: for (let step of steps) {
418 let i = x + step[0];
419 let j = y + step[1];
420 let stepCount = 1;
421 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
422 moves.push(this.getBasicMove([x, y], [i, j]));
423 if (oneStep || stepCount == 3) continue outerLoop;
424 i += step[0];
425 j += step[1];
426 stepCount++;
427 }
428 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
429 moves.push(this.getBasicMove([x, y], [i, j]));
430 }
431 return moves;
432 }
433
434 getPotentialPhoenixMoves(sq) {
435 return this.getSlideNJumpMoves(sq, V.steps[V.PHOENIX], "oneStep");
436 }
437
438 getPmove(move) {
439 if (
440 move.vanish.length == 2 &&
441 move.appear.length == 2 &&
442 move.appear[0].c != move.appear[1].c
443 ) {
444 // In-place pass:
445 return {
446 start: move.start,
447 end: move.end
448 };
449 }
450 return null;
451 }
452
453 oppositePasses(m1, m2) {
454 return (
455 m1.start.x == m2.end.x &&
456 m1.start.y == m2.end.y &&
457 m1.end.x == m2.start.x &&
458 m1.end.y == m2.start.y
459 );
460 }
461
462 filterValid(moves) {
463 const L = this.pmoves.length;
464 const lp = this.pmoves[L-1];
465 if (!lp) return moves;
466 return moves.filter(m => {
467 return (
468 m.vanish.length == 1 ||
469 m.appear.length == 1 ||
470 m.appear[0].c == m.appear[1].c ||
471 !this.oppositePasses(lp, m)
472 );
473 });
474 }
475
476 // isAttacked: unused here (no checks)
477
478 postPlay(move) {
479 this.pmoves.push(this.getPmove(move));
480 }
481
482 postUndo() {
483 this.pmoves.pop();
484 }
485
486 getCheckSquares() {
487 return [];
488 }
489
490 getCurrentScore() {
491 // Turn has changed:
492 const color = V.GetOppCol(this.turn);
493 const lastRank = (color == "w" ? 0 : 8);
494 if ([3,4,5].some(
495 i => {
496 return (
497 Object.keys(V.HAS_BALL_DECODE).includes(
498 this.board[lastRank][i].charAt(1)) &&
499 this.getColor(lastRank, i) == color
500 );
501 }
502 )) {
503 // Goal scored!
504 return color == "w" ? "1-0" : "0-1";
505 }
506 if (this.atLeastOneMove()) return "*";
507 // Stalemate (quite unlikely?)
508 return "1/2";
509 }
510
511 static get VALUES() {
512 return {
513 p: 1,
514 r: 3,
515 n: 3,
516 b: 2,
517 q: 5,
518 h: 3,
519 a: 0 //ball: neutral
520 };
521 }
522
523 static get SEARCH_DEPTH() {
524 return 2;
525 }
526
527 evalPosition() {
528 // Count material:
529 let evaluation = super.evalPosition();
530 if (this.board[4][4] == V.BALL)
531 // Ball not captured yet
532 return evaluation;
533 // Ponder depending on ball position
534 for (let i=0; i<9; i++) {
535 for (let j=0; j<9; j++) {
536 if (Object.keys(V.HAS_BALL_DECODE).includes(this.board[i][j][1]))
537 return evaluation/2 + (this.getColor(i, j) == "w" ? 8 - i : -i);
538 }
539 }
540 return 0; //never reached
541 }
542
543 getNotation(move) {
544 const finalSquare = V.CoordsToSquare(move.end);
545 if (move.appear.length == 2)
546 // A pass: special notation
547 return V.CoordsToSquare(move.start) + "P" + finalSquare;
548 const piece = this.getPiece(move.start.x, move.start.y);
549 if (piece == V.PAWN) {
550 // Pawn move
551 let notation = "";
552 if (move.vanish.length > move.appear.length) {
553 // Capture
554 const startColumn = V.CoordToColumn(move.start.y);
555 notation = startColumn + "x" + finalSquare;
556 }
557 else notation = finalSquare;
558 if (![V.PAWN, V.HAS_BALL_CODE[V.PAWN]].includes(move.appear[0].p)) {
559 // Promotion
560 const promotePiece =
561 V.HAS_BALL_DECODE[move.appear[0].p] || move.appear[0].p;
562 notation += "=" + promotePiece.toUpperCase();
563 }
564 return notation;
565 }
566 // Piece movement
567 return (
568 piece.toUpperCase() +
569 (move.vanish.length > move.appear.length ? "x" : "") +
570 finalSquare
571 );
572 }
573
574 };