e10bb3d48027e673fc92c403ce655d0f4f5460e2
[vchess.git] / client / src / variants / Bario.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { randInt } from "@/utils/alea";
4
5 export class BarioRules extends ChessRules {
6
7 // Does not really seem necessary (although the author mention it)
8 // Instead, first move = pick a square for the king.
9 static get HasFlags() {
10 return false;
11 }
12
13 // Undetermined piece form:
14 static get UNDEFINED() {
15 return 'u';
16 }
17
18 static get PIECES() {
19 return ChessRules.PIECES.concat(V.UNDEFINED);
20 }
21
22 getPpath(b) {
23 if (b[1] == V.UNDEFINED) return "Bario/" + b;
24 return b;
25 }
26
27 canIplay(side, [x, y]) {
28 if (this.movesCount >= 2) return super.canIplay(side, [x, y]);
29 return (
30 this.turn == side &&
31 (
32 (side == 'w' && x == 7) ||
33 (side == 'b' && x == 0)
34 )
35 );
36 }
37
38 hoverHighlight([x, y]) {
39 const c = this.turn;
40 return (
41 this.movesCount <= 1 &&
42 (
43 (c == 'w' && x == 7) ||
44 (c == 'b' && x == 0)
45 )
46 );
47 }
48
49 onlyClick([x, y]) {
50 return (
51 this.movesCount <= 1 ||
52 // TODO: next line theoretically shouldn't be required...
53 (this.movesCount == 2 && this.getColor(x, y) != this.turn)
54 );
55 }
56
57 // Initiate the game by choosing a square for the king:
58 doClick(square) {
59 const c = this.turn;
60 if (
61 this.movesCount >= 2 ||
62 (
63 (c == 'w' && square[0] != 7) ||
64 (c == 'b' && square[0] != 0)
65 )
66 ) {
67 return null;
68 }
69 return new Move({
70 appear: [
71 new PiPo({ x: square[0], y: square[1], c: c, p: V.KING })
72 ],
73 vanish: [
74 new PiPo({ x: square[0], y: square[1], c: c, p: V.UNDEFINED })
75 ],
76 start: { x: -1, y: -1 },
77 });
78 }
79
80 // Do not check kings (TODO: something more subtle!)
81 static IsGoodPosition(position) {
82 if (position.length == 0) return false;
83 const rows = position.split("/");
84 if (rows.length != V.size.x) return false;
85 for (let row of rows) {
86 let sumElts = 0;
87 for (let i = 0; i < row.length; i++) {
88 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
89 else {
90 const num = parseInt(row[i], 10);
91 if (isNaN(num) || num <= 0) return false;
92 sumElts += num;
93 }
94 }
95 if (sumElts != V.size.y) return false;
96 }
97 return true;
98 }
99
100 static IsGoodFen(fen) {
101 if (!ChessRules.IsGoodFen(fen)) return false;
102 const fenParsed = V.ParseFen(fen);
103 if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{8,8}$/))
104 if (!fenParsed.capture) return false;
105 return true;
106 }
107
108 static ParseFen(fen) {
109 const fenParts = fen.split(" ");
110 return Object.assign(
111 {
112 reserve: fenParts[4],
113 capture: fenParts[5]
114 },
115 ChessRules.ParseFen(fen)
116 );
117 }
118
119 getReserveFen() {
120 let counts = new Array(8);
121 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
122 counts[i] = this.reserve["w"][V.RESERVE_PIECES[i]];
123 counts[4 + i] = this.reserve["b"][V.RESERVE_PIECES[i]];
124 }
125 return counts.join("");
126 }
127
128 getCaptureFen() {
129 const L = this.captureUndefined.length;
130 const cu = this.captureUndefined[L-1];
131 return (!!cu ? V.CoordsToSquare(cu) : "-");
132 }
133
134 getFen() {
135 return (
136 super.getFen() + " " +
137 this.getReserveFen() + " " +
138 this.getCaptureFen()
139 );
140 }
141
142 getFenForRepeat() {
143 return (
144 super.getFenForRepeat() + "_" +
145 this.getReserveFen() + "_" +
146 this.getCaptureFen()
147 );
148 }
149
150 static GenRandInitFen() {
151 return "uuuuuuuu/pppppppp/8/8/8/8/PPPPPPPP/UUUUUUUU w 0 - 22212221 -";
152 }
153
154 setOtherVariables(fen) {
155 super.setOtherVariables(fen);
156 const reserve =
157 V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
158 this.reserve = {
159 w: {
160 [V.ROOK]: reserve[0],
161 [V.KNIGHT]: reserve[1],
162 [V.BISHOP]: reserve[2],
163 [V.QUEEN]: reserve[3]
164 },
165 b: {
166 [V.ROOK]: reserve[4],
167 [V.KNIGHT]: reserve[5],
168 [V.BISHOP]: reserve[6],
169 [V.QUEEN]: reserve[7]
170 }
171 };
172 const cu = V.ParseFen(fen).capture;
173 this.captureUndefined = [cu == '-' ? null : V.SquareToCoords(cu)];
174 this.subTurn = (cu == "-" ? 1 : 0);
175 // Local stack of pieces' definitions
176 this.definitions = [];
177 }
178
179 getColor(i, j) {
180 if (i >= V.size.x) return i == V.size.x ? "w" : "b";
181 return this.board[i][j].charAt(0);
182 }
183
184 getPiece(i, j) {
185 if (i >= V.size.x) return V.RESERVE_PIECES[j];
186 return this.board[i][j].charAt(1);
187 }
188
189 getReservePpath(index, color) {
190 return color + V.RESERVE_PIECES[index];
191 }
192
193 static get RESERVE_PIECES() {
194 return [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
195 }
196
197 getReserveMoves([x, y]) {
198 const color = this.turn;
199 const p = V.RESERVE_PIECES[y];
200 if (this.reserve[color][p] == 0) return [];
201 // 2 cases, subTurn == 0 => target this.captureUndefined only (one square)
202 if (this.subTurn == 0) {
203 const L = this.captureUndefined.length;
204 const cu = this.captureUndefined[L-1];
205 return [
206 // Nothing changes on the board, just mark start.p for reserve update
207 new Move({
208 appear: [],
209 vanish: [],
210 start: { x: x, y: y, p: p },
211 end: { x: cu.x, y: cu.y }
212 })
213 ];
214 }
215 // or, subTurn == 1 => target any undefined piece that we own.
216 let moves = [];
217 for (let i = 0; i < V.size.x; i++) {
218 for (let j = 0; j < V.size.y; j++) {
219 if (
220 this.board[i][j] != V.EMPTY &&
221 this.getColor(i, j) == color &&
222 this.getPiece(i, j) == V.UNDEFINED
223 ) {
224 let mv = new Move({
225 appear: [
226 new PiPo({ x: i, y: j, c: color, p: p })
227 ],
228 vanish: [
229 new PiPo({ x: i, y: j, c: color, p: V.UNDEFINED })
230 ],
231 start: { x: x, y: y },
232 end: { x: i, y: j }
233 });
234 moves.push(mv);
235 }
236 }
237 }
238 return moves;
239 }
240
241 getPotentialMovesFrom([x, y]) {
242 if (this.subTurn == 0) {
243 if (x < V.size.x) return [];
244 return this.getReserveMoves([x, y]);
245 }
246 if (this.subTurn == 1) {
247 // Both normal move (from defined piece) and definition allowed
248 if (x >= V.size.x) return this.getReserveMoves([x, y]);
249 if (this.getPiece(x, y) == V.UNDEFINED) return [];
250 }
251 // subTurn == 1 and we move any piece, or
252 // subTurn == 2 and we can only move the just-defined piece
253 if (this.subTurn == 2) {
254 const L = this.definitions.length; //at least 1
255 const df = this.definitions[L-1];
256 if (x != df.x || y != df.y) return [];
257 }
258 return super.getPotentialMovesFrom([x, y]);
259 }
260
261 getAllPotentialMoves() {
262 const color = this.turn;
263 if (this.movesCount <= 1) {
264 // Just put the king on the board
265 const firstRank = (color == 'w' ? 7 : 0);
266 return [...Array(8)].map((x, j) => {
267 return new Move({
268 appear: [
269 new PiPo({ x: firstRank, y: j, c: color, p: V.KING })
270 ],
271 vanish: [
272 new PiPo({ x: firstRank, y: j, c: color, p: V.UNDEFINED })
273 ],
274 start: { x: -1, y: -1 }
275 });
276 });
277 }
278 const getAllReserveMoves = () => {
279 let moves = [];
280 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
281 moves = moves.concat(
282 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
283 );
284 }
285 return moves;
286 }
287 if (this.subTurn == 0) return getAllReserveMoves();
288 let moves = super.getAllPotentialMoves();
289 if (this.subTurn == 1)
290 moves = moves.concat(getAllReserveMoves());
291 return this.filterValid(moves);
292 }
293
294 filterValid(moves) {
295 if (this.movesCount <= 1) return moves;
296 const color = this.turn;
297 return moves.filter(m => {
298 if (m.vanish.length == 0) {
299 // subTurn == 0: need to check if a move exists at subTurn == 1
300 this.play(m);
301 const res = this.filterValid(this.getAllPotentialMoves()).length > 0;
302 this.undo(m);
303 return res;
304 }
305 const start = { x: m.vanish[0].x, y: m.vanish[0].y };
306 const end = { x: m.appear[0].x, y: m.appear[0].y };
307 if (start.x == end.x && start.y == end.y) {
308 // Unfinished turn: require careful check
309 this.play(m);
310 let res = false;
311 if (this.subTurn == 1)
312 // Can either play a move, or specialize a piece
313 res = this.filterValid(this.getAllPotentialMoves()).length > 0;
314 else {
315 // subTurn == 2: can only play a specialized piece
316 res = this.filterValid(
317 this.getPotentialMovesFrom([m.end.x, m.end.y])).length > 0;
318 }
319 this.undo(m);
320 return res;
321 }
322 this.play(m);
323 const res = !this.underCheck(color);
324 this.undo(m);
325 return res;
326 });
327 }
328
329 atLeastOneMove() {
330 const atLeastOneReserveMove = () => {
331 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
332 let moves = this.filterValid(
333 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
334 );
335 if (moves.length > 0) return true;
336 }
337 return false;
338 };
339 if (this.subTurn == 0) return atLeastOneReserveMove();
340 const canMoveSomething = super.atLeastOneMove();
341 if (this.subTurn == 2) return canMoveSomething;
342 return (canMoveSomething || atLeastOneReserveMove());
343 }
344
345 underCheck(color) {
346 if (super.underCheck(color)) return true;
347 // Aux func for piece attack on king (no pawn)
348 const pieceAttackOn = (p, [x1, y1], [x2, y2]) => {
349 const shift = [x2 - x1, y2 - y1];
350 const absShift = shift.map(Math.abs);
351 if (
352 (
353 p == V.KNIGHT &&
354 (absShift[0] + absShift[1] != 3 || shift[0] == 0 || shift[1] == 0)
355 ) ||
356 (p == V.ROOK && shift[0] != 0 && shift[1] != 0) ||
357 (p == V.BISHOP && absShift[0] != absShift[1]) ||
358 (
359 p == V.QUEEN &&
360 shift[0] != 0 && shift[1] != 0 && absShift[0] != absShift[1]
361 )
362 ) {
363 return false;
364 }
365 // Step is compatible with piece:
366 const step = [
367 shift[0] / Math.abs(shift[0]) || 0,
368 shift[1] / Math.abs(shift[1]) || 0
369 ];
370 let [i, j] = [x1 + step[0], y1 + step[1]];
371 while (i != x2 || j != y2) {
372 if (!V.OnBoard(i, j) || this.board[i][j] != V.EMPTY) return false;
373 i += step[0];
374 j += step[1];
375 }
376 return true;
377 };
378 // Check potential specializations of undefined using reserve:
379 const oppCol = V.GetOppCol(color);
380 for (let i=0; i<8; i++) {
381 for (let j=0; j<8; j++) {
382 if (
383 this.board[i][j] != V.EMPTY &&
384 this.getColor(i, j) == oppCol &&
385 this.getPiece(i, j) == V.UNDEFINED
386 ) {
387 for (let p of V.RESERVE_PIECES) {
388 if (
389 this.reserve[oppCol][p] >= 1 &&
390 pieceAttackOn(p, [i, j], this.kingPos[color])
391 ) {
392 return true;
393 }
394 }
395 }
396 }
397 }
398 return false;
399 }
400
401 getCheckSquares() {
402 if (this.movesCount <= 2) return [];
403 return super.getCheckSquares();
404 }
405
406 play(move) {
407 move.turn = [this.turn, this.subTurn]; //easier undo (TODO?)
408 const toNextPlayer = () => {
409 V.PlayOnBoard(this.board, move);
410 this.turn = V.GetOppCol(this.turn);
411 this.subTurn =
412 (move.vanish.length == 2 && move.vanish[1].p == V.UNDEFINED ? 0 : 1);
413 this.movesCount++;
414 this.postPlay(move);
415 };
416 if (this.movesCount <= 1) toNextPlayer();
417 else if (move.vanish.length == 0) {
418 // Removal (subTurn == 0 --> 1)
419 this.reserve[this.turn][move.start.p]--;
420 this.subTurn++;
421 }
422 else {
423 const start = { x: move.vanish[0].x, y: move.vanish[0].y };
424 const end = { x: move.appear[0].x, y: move.appear[0].y };
425 if (start.x == end.x && start.y == end.y) {
426 // Specialisation (subTurn == 1 before 2)
427 this.reserve[this.turn][move.appear[0].p]--;
428 V.PlayOnBoard(this.board, move);
429 this.definitions.push(move.end);
430 this.subTurn++;
431 }
432 else {
433 // Normal move (subTurn 1 or 2: change turn)
434 this.epSquares.push(this.getEpSquare(move));
435 toNextPlayer();
436 }
437 }
438 }
439
440 postPlay(move) {
441 const color = V.GetOppCol(this.turn);
442 if (this.movesCount <= 2) this.kingPos[color] = [move.end.x, move.end.y];
443 else {
444 if (move.vanish.length == 2 && move.vanish[1].p == V.UNDEFINED)
445 this.captureUndefined.push(move.end);
446 else this.captureUndefined.push(null);
447 if (move.appear[0].p == V.KING) super.postPlay(move);
448 else {
449 // If now all my pieces are defined, back to undefined state,
450 // only if at least two different kind of pieces on board!
451 // Store current state in move (cannot infer it after!)
452 if (
453 this.board.every(b => {
454 return b.every(cell => {
455 return (
456 cell == V.EMPTY ||
457 cell[0] != color ||
458 cell[1] != V.UNDEFINED
459 );
460 });
461 })
462 ) {
463 const piecesList = [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
464 const oppCol = this.turn;
465 let definedPieces = { w: {}, b: {} };
466 for (let i=0; i<8; i++) {
467 for (let j=0; j<8; j++) {
468 if (this.board[i][j] != V.EMPTY) {
469 const p = this.getPiece(i, j);
470 const c = this.getColor(i, j);
471 if (piecesList.includes(p)) {
472 definedPieces[c][p] =
473 (!definedPieces[c][p] ? 1 : definedPieces[c][p] + 1);
474 }
475 }
476 }
477 }
478 const my_pk = Object.keys(definedPieces[color]);
479 const opp_pk = Object.keys(definedPieces[oppCol]);
480 const oppRevert = (
481 opp_pk.length >= 2 ||
482 (
483 // Only one opponent's piece is defined, but
484 // at least a different piece wait in reserve:
485 opp_pk.length == 1 &&
486 Object.keys(this.reserve[oppCol]).some(k => {
487 return (k != opp_pk[0] && this.reserve[oppCol][k] >= 1);
488 })
489 )
490 );
491 if (my_pk.length >= 2 || oppRevert) {
492 // NOTE: necessary HACK... because the move is played already.
493 V.UndoOnBoard(this.board, move);
494 move.position = this.getBaseFen();
495 move.reserve = JSON.parse(JSON.stringify(this.reserve));
496 V.PlayOnBoard(this.board, move);
497 for (
498 let cp of [{ c: color, pk: my_pk }, { c: oppCol, pk: opp_pk }]
499 ) {
500 if (cp.pk.length >= 2 || (cp.c == oppCol && oppRevert)) {
501 for (let p of cp.pk)
502 this.reserve[cp.c][p] += definedPieces[cp.c][p];
503 for (let i=0; i<8; i++) {
504 for (let j=0; j<8; j++) {
505 if (
506 this.board[i][j] != V.EMPTY &&
507 this.getColor(i, j) == cp.c &&
508 piecesList.includes(this.getPiece(i, j))
509 ) {
510 this.board[i][j] = cp.c + V.UNDEFINED;
511 }
512 }
513 }
514 }
515 }
516 }
517 }
518 }
519 }
520 }
521
522 undo(move) {
523 const toPrevPlayer = () => {
524 V.UndoOnBoard(this.board, move);
525 [this.turn, this.subTurn] = move.turn;
526 this.movesCount--;
527 this.postUndo(move);
528 };
529 if (this.movesCount <= 2 && move.appear[0].p == V.KING) toPrevPlayer();
530 else if (move.vanish.length == 0) {
531 this.reserve[this.turn][move.start.p]++;
532 this.subTurn = move.turn[1];
533 }
534 else {
535 const start = { x: move.vanish[0].x, y: move.vanish[0].y };
536 const end = { x: move.appear[0].x, y: move.appear[0].y };
537 if (start.x == end.x && start.y == end.y) {
538 this.reserve[this.turn][move.appear[0].p]++;
539 V.UndoOnBoard(this.board, move);
540 this.definitions.pop();
541 this.subTurn = move.turn[1];
542 }
543 else {
544 this.epSquares.pop();
545 toPrevPlayer();
546 }
547 }
548 }
549
550 postUndo(move) {
551 const color = this.turn;
552 if (this.movesCount <= 1) this.kingPos[color] = [-1, -1];
553 else {
554 this.captureUndefined.pop();
555 if (move.appear[0].p == V.KING) super.postUndo(move);
556 else {
557 if (!!move.position) {
558 this.board = V.GetBoard(move.position);
559 this.reserve = move.reserve;
560 }
561 }
562 }
563 }
564
565 getComputerMove() {
566 let initMoves = this.getAllValidMoves();
567 if (initMoves.length == 0) return null;
568 // Loop until valid move is found (no un-specifiable piece...)
569 const color = this.turn;
570 while (true) {
571 let moves = JSON.parse(JSON.stringify(initMoves));
572 let mvArray = [];
573 let mv = null;
574 // Just play random moves (for now at least. TODO?)
575 while (moves.length > 0) {
576 mv = moves[randInt(moves.length)];
577 mvArray.push(mv);
578 this.play(mv);
579 if (this.turn == color) {
580 if (this.subTurn == 1) moves = this.getAllValidMoves();
581 else {
582 // subTurn == 2
583 moves = this.filterValid(
584 this.getPotentialMovesFrom([mv.end.x, mv.end.y]));
585 }
586 }
587 else break;
588 }
589 const thisIsTheEnd = (this.turn != color);
590 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
591 if (thisIsTheEnd) return (mvArray.length > 1 ? mvArray : mvArray[0]);
592 }
593 return null; //never reached
594 }
595
596 static get VALUES() {
597 return Object.assign({ u: 0 }, ChessRules.VALUES);
598 }
599
600 // NOTE: evalPosition is wrong, but unused (random mover)
601
602 getNotation(move) {
603 const end = { x: move.end.x, y: move.end.y };
604 const endSquare = V.CoordsToSquare(end);
605 if (move.appear.length == 0)
606 // Removal
607 return move.start.p.toUpperCase() + endSquare + "X";
608 if (move.vanish.length == 0) return "K@" + endSquare;
609 const start = { x: move.vanish[0].x, y: move.vanish[0].y };
610 if (start.x == end.x && start.y == end.y)
611 // Something is specialized
612 return move.appear[0].p.toUpperCase() + "@" + endSquare;
613 // Normal move
614 return super.getNotation(move);
615 }
616
617 };