Add Rollerball variant
[vchess.git] / client / src / variants / Rollerball.js
1 import { ChessRules } from "@/base_rules";
2
3 export class RollerballRules extends ChessRules {
4
5 static get HasEnpassant() {
6 return false;
7 }
8
9 static get HasCastle() {
10 return false;
11 }
12
13 static get DarkBottomRight() {
14 return true;
15 }
16
17 static get PIECES() {
18 return [V.PAWN, V.KING, V.ROOK, V.BISHOP];
19 }
20
21 static get size() {
22 return { x: 7, y: 7 };
23 }
24
25 // TODO: the wall position should be checked too
26 static IsGoodPosition(position) {
27 if (position.length == 0) return false;
28 const rows = position.split("/");
29 if (rows.length != V.size.x) return false;
30 let kings = { "k": 0, "K": 0 };
31 for (let row of rows) {
32 let sumElts = 0;
33 for (let i = 0; i < row.length; i++) {
34 if (['K','k'].includes(row[i])) kings[row[i]]++;
35 if (['x'].concat(V.PIECES).includes(row[i].toLowerCase())) sumElts++;
36 else {
37 const num = parseInt(row[i], 10);
38 if (isNaN(num)) return false;
39 sumElts += num;
40 }
41 }
42 if (sumElts != V.size.y) return false;
43 }
44 if (Object.values(kings).some(v => v != 1)) return false;
45 return true;
46 }
47
48 // NOTE: canTake() is wrong, but next method is enough
49 static OnBoard(x, y) {
50 return (
51 (x >= 0 && x <= 6 && y >= 0 && y <= 6) &&
52 (![2, 3, 4].includes(x) || ![2, 3, 4].includes(y))
53 );
54 }
55
56 static IsGoodFlags(flags) {
57 // 2 for kings: last zone reached
58 return !!flags.match(/^[0-7]{2,2}$/);
59 }
60
61 setFlags(fenflags) {
62 this.kingFlags = {
63 w: parseInt(fenflags.charAt(0), 10),
64 b: parseInt(fenflags.charAt(1), 10)
65 };
66 }
67
68 aggregateFlags() {
69 return this.kingFlags;
70 }
71
72 disaggregateFlags(flags) {
73 this.kingFlags = flags;
74 }
75
76 getFlagsFen() {
77 return this.kingFlags['w'].toString() + this.kingFlags['b'].toString();
78 }
79
80 // For space in the middle:
81 static get NOTHING() {
82 return "xx";
83 }
84
85 static board2fen(b) {
86 if (b[0] == 'x') return 'x';
87 return ChessRules.board2fen(b);
88 }
89
90 static fen2board(f) {
91 if (f == 'x') return V.NOTHING;
92 return ChessRules.fen2board(f);
93 }
94
95 getPpath(b) {
96 if (b[0] == 'x') return "Omega/nothing";
97 return b;
98 }
99
100 static GenRandInitFen() {
101 return "2rbp2/2rkp2/2xxx2/2xxx2/2xxx2/2PKR2/2PBR2 w 0 00";
102 }
103
104 getPotentialMovesFrom(sq) {
105 switch (this.getPiece(sq[0], sq[1])) {
106 case V.PAWN: return this.getPotentialPawnMoves(sq);
107 case V.ROOK: return this.getPotentialRookMoves(sq);
108 case V.BISHOP: return this.getPotentialBishopMoves(sq);
109 case V.KING: return super.getPotentialKingMoves(sq);
110 }
111 return [];
112 }
113
114 getPotentialPawnMoves([x, y]) {
115 const c = this.turn;
116 // Need to know pawn area to deduce move options
117 const inMiddleX = [2, 3, 4].includes(x);
118 const inMiddleY = [2, 3, 4].includes(y);
119 // In rectangular areas on the sides?
120 if (inMiddleX) {
121 const forward = (y <= 1 ? -1 : 1);
122 return (
123 super.getSlideNJumpMoves(
124 [x, y], [[forward, -1], [forward, 0], [forward, 1]], "oneStep")
125 );
126 }
127 if (inMiddleY) {
128 const forward = (x <= 1 ? 1 : -1);
129 let moves =
130 super.getSlideNJumpMoves(
131 [x, y], [[-1, forward], [0, forward], [1, forward]], "oneStep");
132 // Promotions may happen:
133 let extraMoves = [];
134 moves.forEach(m => {
135 if (
136 (c == 'w' && x <= 1 && m.end.y == 4) ||
137 (c == 'b' && x >= 5 && m.end.y == 2)
138 ) {
139 m.appear[0].p = V.ROOK;
140 let m2 = JSON.parse(JSON.stringify(m));
141 m2.appear[0].p = V.BISHOP;
142 extraMoves.push(m2);
143 }
144 });
145 Array.prototype.push.apply(moves, extraMoves);
146 return moves;
147 }
148 // In a corner:
149 const toRight = (x == 0 && [0, 1, 5].includes(y)) || (x == 1 && y == 1);
150 const toLeft = (x == 6 && [1, 5, 6].includes(y)) || (x == 5 && y == 5);
151 const toUp = (y == 0 && [1, 5, 6].includes(x)) || (x == 5 && y == 1);
152 const toBottom = (y == 6 && [0, 1, 5].includes(x)) || (x == 1 && y == 5);
153 if (toRight || toLeft) {
154 const forward = (toRight ? 1 : -1);
155 return (
156 super.getSlideNJumpMoves(
157 [x, y], [[-1, forward], [0, forward], [1, forward]], "oneStep")
158 );
159 }
160 const forward = (toUp ? -1 : 1);
161 return (
162 super.getSlideNJumpMoves(
163 [x, y], [[forward, -1], [forward, 0], [forward, 1]], "oneStep")
164 );
165 }
166
167 getPotentialRookMoves([x, y]) {
168 let multiStep = [],
169 oneStep = [];
170 if (x <= 1) multiStep.push([0, 1]);
171 else oneStep.push([0, 1]);
172 if (y <= 1) multiStep.push([-1, 0]);
173 else oneStep.push([-1, 0]);
174 if (x >= 5) multiStep.push([0, -1]);
175 else oneStep.push([0, -1]);
176 if (y >= 5) multiStep.push([1, 0]);
177 else oneStep.push([1, 0]);
178 const c = this.turn;
179 let moves = super.getSlideNJumpMoves([x, y], oneStep, "oneStep");
180 for (let step of multiStep) {
181 let [i, j] = [x + step[0], y + step[1]];
182 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
183 moves.push(this.getBasicMove([x, y], [i, j]));
184 i += step[0];
185 j += step[1];
186 }
187 if (V.OnBoard(i, j)) {
188 if (this.getColor(i, j) != c)
189 moves.push(this.getBasicMove([x, y], [i, j]));
190 }
191 else {
192 i -= step[0];
193 j -= step[1];
194 // Potential rebound if away from initial square
195 if (i != x || j != y) {
196 // Corners check
197 let nextStep = null;
198 if (i == 0 && j == 0) nextStep = [0, 1];
199 else if (i == 0 && j == 6) nextStep = [1, 0];
200 else if (i == 6 && j == 6) nextStep = [0, -1];
201 else if (i == 6 && j == 0) nextStep = [-1, 0];
202 if (!!nextStep) {
203 i += nextStep[0];
204 j += nextStep[1];
205 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
206 moves.push(this.getBasicMove([x, y], [i, j]));
207 i += nextStep[0];
208 j += nextStep[1];
209 }
210 if (V.OnBoard(i, j) && this.getColor(i, j) != c)
211 moves.push(this.getBasicMove([x, y], [i, j]));
212 }
213 }
214 }
215 }
216 return moves;
217 }
218
219 static get DictBishopSteps() {
220 return {
221 "-1_-1": [-1, -1],
222 "-1_1": [-1, 1],
223 "1_-1": [1, -1],
224 "1_1": [1, 1]
225 };
226 }
227
228 getPotentialBishopMoves([x, y]) {
229 let multiStep = {};
230 if (x <= 1) {
231 multiStep["-1_1"] = [-1, 1];
232 multiStep["1_1"] = [1, 1];
233 }
234 if (y <= 1) {
235 multiStep["-1_-1"] = [-1, -1];
236 if (!multiStep["-1_1"]) multiStep["-1_1"] = [-1, 1];
237 }
238 if (x >= 5) {
239 multiStep["1_-1"] = [1, -1];
240 if (!multiStep["-1_-1"]) multiStep["-1_-1"] = [-1, -1];
241 }
242 if (y >= 5) {
243 if (!multiStep["1_-1"]) multiStep["1_-1"] = [1, -1];
244 if (!multiStep["1_1"]) multiStep["1_1"] = [1, 1];
245 }
246 let oneStep = [];
247 Object.keys(V.DictBishopSteps).forEach(str => {
248 if (!multiStep[str]) oneStep.push(V.DictBishopSteps[str]);
249 });
250 const c = this.turn;
251 let moves = super.getSlideNJumpMoves([x, y], oneStep, "oneStep");
252 for (let step of Object.values(multiStep)) {
253 let [i, j] = [x + step[0], y + step[1]];
254 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
255 moves.push(this.getBasicMove([x, y], [i, j]));
256 i += step[0];
257 j += step[1];
258 }
259 if (V.OnBoard(i, j)) {
260 if (this.getColor(i, j) != c)
261 moves.push(this.getBasicMove([x, y], [i, j]));
262 }
263 else {
264 i -= step[0];
265 j -= step[1];
266 // Rebound, if we moved away from initial square
267 if (i != x || j != y) {
268 let nextStep = null;
269 if (step[0] == -1 && step[1] == -1) {
270 if (j == 0) nextStep = [-1, 1];
271 else nextStep = [1, -1];
272 }
273 else if (step[0] == -1 && step[1] == 1) {
274 if (i == 0) nextStep = [1, 1];
275 else nextStep = [-1, -1];
276 }
277 else if (step[0] == 1 && step[1] == -1) {
278 if (i == 6) nextStep = [-1, -1];
279 else nextStep = [1, 1];
280 }
281 else {
282 // step == [1, 1]
283 if (j == 6) nextStep = [1, -1];
284 else nextStep = [-1, 1];
285 }
286 i += nextStep[0];
287 j += nextStep[1];
288 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
289 moves.push(this.getBasicMove([x, y], [i, j]));
290 i += nextStep[0];
291 j += nextStep[1];
292 }
293 if (V.OnBoard(i, j) && this.getColor(i, j) != c)
294 moves.push(this.getBasicMove([x, y], [i, j]));
295 }
296 }
297 }
298 return moves;
299 }
300
301 isAttacked(sq, color) {
302 return (
303 super.isAttackedByKing(sq, color) ||
304 this.isAttackedByRook(sq, color) ||
305 this.isAttackedByBishop(sq, color) ||
306 this.isAttackedByPawn(sq, color)
307 );
308 }
309
310 isAttackedByPawn([x, y], color) {
311 // Determine zone, shifted according to pawn movement
312 let attackDir = "";
313 let forward = 0;
314 if (
315 ([1, 2, 3, 4].includes(x) && y <= 1) ||
316 (x == 5 && y == 0)
317 ) {
318 attackDir = "vertical";
319 forward = 1;
320 }
321 else if (
322 ([2, 3, 4, 5].includes(x) && [5, 6].includes(y)) ||
323 (x == 1 && y == 6)
324 ) {
325 attackDir = "vertical";
326 forward = -1;
327 }
328 else if (
329 (x <= 1 && [2, 3, 4, 5].includes(y)) ||
330 (x == 0 && y == 1)
331 ) {
332 attackDir = "horizontal";
333 forward = -1;
334 }
335 else if (
336 (x >= 5 && [1, 2, 3, 4].includes(y)) ||
337 (x == 6 && y == 5)
338 ) {
339 attackDir = "horizontal";
340 forward = 1;
341 }
342 if (forward != 0) {
343 const steps =
344 attackDir == "vertical"
345 ? [ [forward, -1], [forward, 0], [forward, 1] ]
346 : [ [-1, forward], [0, forward], [1, forward] ];
347 return (
348 super.isAttackedBySlideNJump([x, y], color, V.PAWN, steps, "oneStep")
349 );
350 }
351 // In a corner: can be attacked by one square only
352 let step = null;
353 if (x == 0) {
354 if (y == 0) step = [1, 0];
355 else step = [0, -1];
356 }
357 else {
358 if (y == 0) step = [0, 1];
359 else step = [-1, 0];
360 }
361 return (
362 super.isAttackedBySlideNJump([x, y], color, V.PAWN, [step], "oneStep")
363 );
364 }
365
366 isAttackedByRook([x, y], color) {
367 // "Reversing" the code of getPotentialRookMoves()
368 let multiStep = [],
369 oneStep = [];
370 if (x <= 1) multiStep.push([0, -1]);
371 else oneStep.push([0, -1]);
372 if (y <= 1) multiStep.push([1, 0]);
373 else oneStep.push([1, 0]);
374 if (x >= 5) multiStep.push([0, 1]);
375 else oneStep.push([0, 1]);
376 if (y >= 5) multiStep.push([-1, 0]);
377 else oneStep.push([-1, 0]);
378 if (
379 super.isAttackedBySlideNJump([x, y], color, V.ROOK, oneStep, "oneStep")
380 ) {
381 return true;
382 }
383 for (let step of multiStep) {
384 let [i, j] = [x + step[0], y + step[1]];
385 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
386 i += step[0];
387 j += step[1];
388 }
389 if (V.OnBoard(i, j)) {
390 if (this.getColor(i, j) == color && this.getPiece(i, j) == V.ROOK)
391 return true;
392 }
393 else {
394 i -= step[0];
395 j -= step[1];
396 if (i != x || j != y) {
397 let nextStep = null;
398 if (i == 0 && j == 0) nextStep = [1, 0];
399 else if (i == 0 && j == 6) nextStep = [0, -1];
400 else if (i == 6 && j == 6) nextStep = [-1, 0];
401 else if (i == 6 && j == 0) nextStep = [0, 1];
402 if (!!nextStep) {
403 i += nextStep[0];
404 j += nextStep[1];
405 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
406 i += nextStep[0];
407 j += nextStep[1];
408 }
409 if (
410 V.OnBoard(i, j) &&
411 this.getColor(i, j) == color &&
412 this.getPiece(i, j) == V.ROOK
413 ) {
414 return true;
415 }
416 }
417 }
418 }
419 }
420 return false;
421 }
422
423 isAttackedByBishop([x, y], color) {
424 // "Reversing" the code of getPotentiaBishopMoves()
425 let multiStep = {};
426 if (x <= 1) {
427 multiStep["1_-1"] = [1, -1];
428 multiStep["-1_-1"] = [-1, -1];
429 }
430 if (y <= 1) {
431 multiStep["1_1"] = [1, 1];
432 if (!multiStep["1_-1"]) multiStep["1_-1"] = [1, -1];
433 }
434 if (x >= 5) {
435 multiStep["-1_1"] = [-1, 1];
436 if (!multiStep["1_1"]) multiStep["1_1"] = [1, 1];
437 }
438 if (y >= 5) {
439 if (!multiStep["-1_-1"]) multiStep["-1_-1"] = [-1, -1];
440 if (!multiStep["-1_1"]) multiStep["-1_1"] = [-1, 1];
441 }
442 let oneStep = [];
443 Object.keys(V.DictBishopSteps).forEach(str => {
444 if (!multiStep[str]) oneStep.push(V.DictBishopSteps[str]);
445 });
446 if (
447 super.isAttackedBySlideNJump([x, y], color, V.BISHOP, oneStep, "oneStep")
448 ) {
449 return true;
450 }
451 for (let step of Object.values(multiStep)) {
452 let [i, j] = [x + step[0], y + step[1]];
453 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
454 i += step[0];
455 j += step[1];
456 }
457 if (V.OnBoard(i, j)) {
458 if (this.getColor(i, j) == color && this.getPiece(i, j) == V.BISHOP)
459 return true;
460 }
461 else {
462 i -= step[0];
463 j -= step[1];
464 if (i != x || j != y) {
465 let nextStep = null;
466 if (step[0] == -1 && step[1] == -1) {
467 if (j == 0) nextStep = [-1, 1];
468 else nextStep = [1, -1];
469 }
470 else if (step[0] == -1 && step[1] == 1) {
471 if (i == 0) nextStep = [1, 1];
472 else nextStep = [-1, -1];
473 }
474 else if (step[0] == 1 && step[1] == -1) {
475 if (i == 6) nextStep = [-1, -1];
476 else nextStep = [1, 1];
477 }
478 else {
479 // step == [1, 1]
480 if (j == 6) nextStep = [1, -1];
481 else nextStep = [-1, 1];
482 }
483 i += nextStep[0];
484 j += nextStep[1];
485 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
486 i += nextStep[0];
487 j += nextStep[1];
488 }
489 if (
490 V.OnBoard(i, j) &&
491 this.getColor(i, j) == color &&
492 this.getPiece(i, j) == V.BISHOP
493 ) {
494 return true;
495 }
496 }
497 }
498 }
499 return false;
500 }
501
502 // The board is divided in areas determined by "distance to target"
503 // A zone n+1 must be reached from a zone n.
504 getKingZone([x, y], color) {
505 if (color == 'w') {
506 if (y >= 4) return -1; //"out of zone"
507 if (y == 3 && [5, 6].includes(x)) return 0;
508 if (x == 6) return 1;
509 if (x == 5) return 2;
510 if (x == 4) return 3;
511 if (x == 3 || y == 0) return 4;
512 if (y == 1) return 5;
513 if (x == 0 || y == 2) return 6;
514 return 7; //x == 1 && y == 3
515 }
516 // color == 'b':
517 if (y <= 2) return -1; //"out of zone"
518 if (y == 3 && [0, 1].includes(x)) return 0;
519 if (x == 0) return 1;
520 if (x == 1) return 2;
521 if (x == 2) return 3;
522 if (x == 3 || y == 6) return 4;
523 if (y == 5) return 5;
524 if (x == 6 || y == 4) return 6;
525 return 7; //x == 5 && y == 3
526 }
527
528 postPlay(move) {
529 super.postPlay(move);
530 if (move.vanish[0].p == V.KING) {
531 const c = move.vanish[0].c;
532 const z1 = this.getKingZone([move.vanish[0].x, move.vanish[0].y], c),
533 z2 = this.getKingZone([move.appear[0].x, move.appear[0].y], c);
534 if (
535 z1 >= 0 && z2 >= 0 && z1 < z2 &&
536 // There exist "zone jumps" (0 to 2 for example),
537 // so the following test "flag >= z1" is required.
538 this.kingFlags[c] >= z1 && this.kingFlags[c] < z2
539 ) {
540 this.kingFlags[c] = z2;
541 }
542 }
543 }
544
545 getCurrentScore() {
546 const oppCol = V.GetOppCol(this.turn);
547 if (this.kingFlags[oppCol] == 7) return (oppCol == 'w' ? "1-0" : "0-1");
548 return super.getCurrentScore();
549 }
550
551 static get SEARCH_DEPTH() {
552 return 4;
553 }
554
555 evalPosition() {
556 let evaluation = 0;
557 for (let i = 0; i < V.size.x; i++) {
558 for (let j = 0; j < V.size.y; j++) {
559 if (this.board[i][j] != V.EMPTY) {
560 const sign = this.getColor(i, j) == "w" ? 1 : -1;
561 const piece = this.getPiece(i, j);
562 if (piece != 'x') evaluation += sign * V.VALUES[piece];
563 }
564 }
565 }
566 // Taking flags into account in a rather naive way
567 return evaluation + this.kingFlags['w'] - this.kingFlags['b'];
568 }
569
570 };