Backward compatibility for Ball variant (wildebeest)
[vchess.git] / client / src / variants / Emergo.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3 import { ArrayFun } from "@/utils/array";
4
5 export class EmergoRules extends ChessRules {
6
7 // Simple encoding: A to L = 1 to 12, from left to right, if white controls.
8 // Lowercase if black controls.
9 // Single piece (no prisoners): A@ to L@ (+ lowercase)
10
11 static get HasFlags() {
12 return false;
13 }
14
15 static get HasEnpassant() {
16 return false;
17 }
18
19 static get DarkBottomRight() {
20 return true;
21 }
22
23 // board element == file name:
24 static board2fen(b) {
25 return b;
26 }
27 static fen2board(f) {
28 return f;
29 }
30
31 static IsGoodPosition(position) {
32 if (position.length == 0) return false;
33 const rows = position.split("/");
34 if (rows.length != V.size.x) return false;
35 for (let row of rows) {
36 let sumElts = 0;
37 for (let i = 0; i < row.length; i++) {
38 // Add only 0.5 per symbol because 2 per piece
39 if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5;
40 else {
41 const num = parseInt(row[i], 10);
42 if (isNaN(num) || num <= 0) return false;
43 sumElts += num;
44 }
45 }
46 if (sumElts != V.size.y) return false;
47 }
48 return true;
49 }
50
51 static GetBoard(position) {
52 const rows = position.split("/");
53 let board = ArrayFun.init(V.size.x, V.size.y, "");
54 for (let i = 0; i < rows.length; i++) {
55 let j = 0;
56 for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
57 const character = rows[i][indexInRow];
58 const num = parseInt(character, 10);
59 // If num is a number, just shift j:
60 if (!isNaN(num)) j += num;
61 else
62 // Something at position i,j
63 board[i][j++] = V.fen2board(character + rows[i][++indexInRow]);
64 }
65 }
66 return board;
67 }
68
69 getPpath(b) {
70 return "Emergo/" + b;
71 }
72
73 getColor(x, y) {
74 if (x >= V.size.x) return x == V.size.x ? "w" : "b";
75 if (this.board[x][y].charCodeAt(0) < 97) return 'w';
76 return 'b';
77 }
78
79 getPiece() {
80 return V.PAWN; //unused
81 }
82
83 static IsGoodFen(fen) {
84 if (!ChessRules.IsGoodFen(fen)) return false;
85 const fenParsed = V.ParseFen(fen);
86 // 3) Check reserves
87 if (
88 !fenParsed.reserve ||
89 !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/)
90 ) {
91 return false;
92 }
93 return true;
94 }
95
96 static ParseFen(fen) {
97 const fenParts = fen.split(" ");
98 return Object.assign(
99 ChessRules.ParseFen(fen),
100 { reserve: fenParts[3] }
101 );
102 }
103
104 static get size() {
105 return { x: 9, y: 9 };
106 }
107
108 static GenRandInitFen(randomness) {
109 return "9/9/9/9/9/9/9/9/9 w 0 12,12";
110 }
111
112 getFen() {
113 return super.getFen() + " " + this.getReserveFen();
114 }
115
116 getFenForRepeat() {
117 return super.getFenForRepeat() + "_" + this.getReserveFen();
118 }
119
120 getReserveFen() {
121 return (
122 (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," +
123 (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN])
124 );
125 }
126
127 getReservePpath(index, color) {
128 return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@';
129 }
130
131 static get RESERVE_PIECES() {
132 return [V.PAWN]; //only array length matters
133 }
134
135 setOtherVariables(fen) {
136 const reserve =
137 V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10));
138 this.reserve = { w: null, b: null };
139 if (reserve[0] > 0) this.reserve['w'] = { [V.PAWN]: reserve[0] };
140 if (reserve[1] > 0) this.reserve['b'] = { [V.PAWN]: reserve[1] };
141 // Local stack of captures during a turn (squares + directions)
142 this.captures = [ [] ];
143 }
144
145 atLeastOneCaptureFrom([x, y], color, forbiddenStep) {
146 for (let s of V.steps[V.BISHOP]) {
147 if (
148 !forbiddenStep ||
149 (s[0] != -forbiddenStep[0] || s[1] != -forbiddenStep[1])
150 ) {
151 const [i, j] = [x + s[0], y + s[1]];
152 if (
153 V.OnBoard(i + s[0], j + s[1]) &&
154 this.board[i][j] != V.EMPTY &&
155 this.getColor(i, j) != color &&
156 this.board[i + s[0]][j + s[1]] == V.EMPTY
157 ) {
158 return true;
159 }
160 }
161 }
162 return false;
163 }
164
165 atLeastOneCapture(color) {
166 const L0 = this.captures.length;
167 const captures = this.captures[L0 - 1];
168 const L = captures.length;
169 if (L > 0) {
170 return (
171 this.atLeastOneCaptureFrom(
172 captures[L-1].square, color, captures[L-1].step)
173 );
174 }
175 for (let i = 0; i < V.size.x; i++) {
176 for (let j=0; j< V.size.y; j++) {
177 if (
178 this.board[i][j] != V.EMPTY &&
179 this.getColor(i, j) == color &&
180 this.atLeastOneCaptureFrom([i, j], color)
181 ) {
182 return true;
183 }
184 }
185 }
186 return false;
187 }
188
189 maxLengthIndices(caps) {
190 let maxLength = 0;
191 let res = [];
192 for (let i = 0; i < caps.length; i++) {
193 if (caps[i].length > maxLength) {
194 res = [i];
195 maxLength = caps[i].length;
196 }
197 else if (caps[i].length == maxLength) res.push(i);
198 }
199 return res;
200 };
201
202 getLongestCaptures_aux([x, y], color, locSteps) {
203 let res = [];
204 const L = locSteps.length;
205 const lastStep = (L > 0 ? locSteps[L-1] : null);
206 for (let s of V.steps[V.BISHOP]) {
207 if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
208 const [i, j] = [x + s[0], y + s[1]];
209 if (
210 V.OnBoard(i + s[0], j + s[1]) &&
211 this.board[i + s[0]][j + s[1]] == V.EMPTY &&
212 this.board[i][j] != V.EMPTY &&
213 this.getColor(i, j) != color
214 ) {
215 const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
216 locSteps.push(s);
217 V.PlayOnBoard(this.board, move);
218 const nextRes =
219 this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps);
220 res.push(1 + nextRes);
221 locSteps.pop();
222 V.UndoOnBoard(this.board, move);
223 }
224 }
225 if (res.length == 0) return 0;
226 return Math.max(...res);
227 }
228
229 getLongestCapturesFrom([x, y], color, locSteps) {
230 let res = [];
231 const L = locSteps.length;
232 const lastStep = (L > 0 ? locSteps[L-1] : null);
233 for (let s of V.steps[V.BISHOP]) {
234 if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
235 const [i, j] = [x + s[0], y + s[1]];
236 if (
237 V.OnBoard(i + s[0], j + s[1]) &&
238 this.board[i + s[0]][j + s[1]] == V.EMPTY &&
239 this.board[i][j] != V.EMPTY &&
240 this.getColor(i, j) != color
241 ) {
242 const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
243 locSteps.push(s);
244 V.PlayOnBoard(this.board, move);
245 const stepRes =
246 this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps);
247 res.push({ step: s, length: 1 + stepRes });
248 locSteps.pop();
249 V.UndoOnBoard(this.board, move);
250 }
251 }
252 return this.maxLengthIndices(res).map(i => res[i]);;
253 }
254
255 getAllLongestCaptures(color) {
256 const L0 = this.captures.length;
257 const captures = this.captures[L0 - 1];
258 const L = captures.length;
259 let caps = [];
260 if (L > 0) {
261 let locSteps = [ captures[L-1].step ];
262 let res =
263 this.getLongestCapturesFrom(captures[L-1].square, color, locSteps);
264 Array.prototype.push.apply(
265 caps,
266 res.map(r => Object.assign({ square: captures[L-1].square }, r))
267 );
268 }
269 else {
270 for (let i = 0; i < V.size.x; i++) {
271 for (let j=0; j < V.size.y; j++) {
272 if (
273 this.board[i][j] != V.EMPTY &&
274 this.getColor(i, j) == color
275 ) {
276 let locSteps = [];
277 let res = this.getLongestCapturesFrom([i, j], color, locSteps);
278 Array.prototype.push.apply(
279 caps,
280 res.map(r => Object.assign({ square: [i, j] }, r))
281 );
282 }
283 }
284 }
285 }
286 return this.maxLengthIndices(caps).map(i => caps[i]);
287 }
288
289 getBasicMove([x1, y1], [x2, y2], capt) {
290 const cp1 = this.board[x1][y1];
291 if (!capt) {
292 return new Move({
293 appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ],
294 vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ]
295 });
296 }
297 // Compute resulting types based on jumped + jumping pieces
298 const color = this.getColor(x1, y1);
299 const firstCodes = (color == 'w' ? [65, 97] : [97, 65]);
300 const cpCapt = this.board[capt[0]][capt[1]];
301 let count1 = [cp1.charCodeAt(0) - firstCodes[0], -1];
302 if (cp1[1] != '@') count1[1] = cp1.charCodeAt(1) - firstCodes[0];
303 let countC = [cpCapt.charCodeAt(0) - firstCodes[1], -1];
304 if (cpCapt[1] != '@') countC[1] = cpCapt.charCodeAt(1) - firstCodes[1];
305 count1[1]++;
306 countC[0]--;
307 let colorChange = false,
308 captVanish = false;
309 if (countC[0] < 0) {
310 if (countC[1] >= 0) {
311 colorChange = true;
312 countC = [countC[1], -1];
313 }
314 else captVanish = true;
315 }
316 const incPrisoners = String.fromCharCode(firstCodes[0] + count1[1]);
317 let mv = new Move({
318 appear: [
319 new PiPo({
320 x: x2,
321 y: y2,
322 c: cp1[0],
323 p: incPrisoners
324 })
325 ],
326 vanish: [
327 new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }),
328 new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] })
329 ]
330 });
331 if (!captVanish) {
332 mv.appear.push(
333 new PiPo({
334 x: capt[0],
335 y: capt[1],
336 c: String.fromCharCode(
337 firstCodes[(colorChange ? 0 : 1)] + countC[0]),
338 p: (colorChange ? '@' : cpCapt[1]),
339 })
340 );
341 }
342 return mv;
343 }
344
345 getReserveMoves(x) {
346 const color = this.turn;
347 if (!this.reserve[color] || this.atLeastOneCapture(color)) return [];
348 let moves = [];
349 const shadowPiece =
350 this.reserve[V.GetOppCol(color)] == null
351 ? this.reserve[color][V.PAWN] - 1
352 : 0;
353 const appearColor = String.fromCharCode(
354 (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece);
355 const addMove = ([i, j]) => {
356 moves.push(
357 new Move({
358 appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ],
359 vanish: [],
360 start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 }
361 })
362 );
363 };
364 const oppCol = V.GetOppCol(color);
365 const opponentCanCapture = this.atLeastOneCapture(oppCol);
366 for (let i = 0; i < V.size.x; i++) {
367 for (let j = i % 2; j < V.size.y; j += 2) {
368 if (
369 this.board[i][j] == V.EMPTY &&
370 // prevent playing on central square at move 1:
371 (this.movesCount >= 1 || i != 4 || j != 4)
372 ) {
373 if (opponentCanCapture) addMove([i, j]);
374 else {
375 let canAddMove = true;
376 for (let s of V.steps[V.BISHOP]) {
377 if (
378 V.OnBoard(i + s[0], j + s[1]) &&
379 V.OnBoard(i - s[0], j - s[1]) &&
380 this.board[i + s[0]][j + s[1]] != V.EMPTY &&
381 this.board[i - s[0]][j - s[1]] == V.EMPTY &&
382 this.getColor(i + s[0], j + s[1]) == oppCol
383 ) {
384 canAddMove = false;
385 break;
386 }
387 }
388 if (canAddMove) addMove([i, j]);
389 }
390 }
391 }
392 }
393 return moves;
394 }
395
396 getPotentialMovesFrom([x, y], longestCaptures) {
397 if (x >= V.size.x) {
398 if (longestCaptures.length == 0) return this.getReserveMoves(x);
399 return [];
400 }
401 const color = this.turn;
402 if (!!this.reserve[color] && !this.atLeastOneCapture(color)) return [];
403 const L0 = this.captures.length;
404 const captures = this.captures[L0 - 1];
405 const L = captures.length;
406 let moves = [];
407 if (longestCaptures.length > 0) {
408 if (
409 L > 0 &&
410 (x != captures[L-1].square[0] || y != captures[L-1].square[1])
411 ) {
412 return [];
413 }
414 longestCaptures.forEach(lc => {
415 if (lc.square[0] == x && lc.square[1] == y) {
416 const s = lc.step;
417 const [i, j] = [x + s[0], y + s[1]];
418 moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]));
419 }
420 });
421 return moves;
422 }
423 // Just search simple moves:
424 for (let s of V.steps[V.BISHOP]) {
425 const [i, j] = [x + s[0], y + s[1]];
426 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
427 moves.push(this.getBasicMove([x, y], [i, j]));
428 }
429 return moves;
430 }
431
432 getAllValidMoves() {
433 const color = this.turn;
434 const longestCaptures = this.getAllLongestCaptures(color);
435 let potentialMoves = [];
436 for (let i = 0; i < V.size.x; i++) {
437 for (let j = 0; j < V.size.y; j++) {
438 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
439 Array.prototype.push.apply(
440 potentialMoves,
441 this.getPotentialMovesFrom([i, j], longestCaptures)
442 );
443 }
444 }
445 }
446 // Add reserve moves
447 potentialMoves = potentialMoves.concat(
448 this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1))
449 );
450 return potentialMoves;
451 }
452
453 getPossibleMovesFrom([x, y]) {
454 const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y));
455 return this.getPotentialMovesFrom([x, y], longestCaptures);
456 }
457
458 filterValid(moves) {
459 return moves;
460 }
461
462 getCheckSquares() {
463 return [];
464 }
465
466 play(move) {
467 const color = this.turn;
468 move.turn = color; //for undo
469 V.PlayOnBoard(this.board, move);
470 if (move.vanish.length == 2) {
471 const L0 = this.captures.length;
472 let captures = this.captures[L0 - 1];
473 captures.push({
474 square: [move.end.x, move.end.y],
475 step: [(move.end.x - move.start.x)/2, (move.end.y - move.start.y)/2]
476 });
477 if (this.atLeastOneCapture(color))
478 // There could be other captures (mandatory)
479 move.notTheEnd = true;
480 }
481 else if (move.vanish == 0) {
482 const firstCode = (color == 'w' ? 65 : 97);
483 // Generally, reserveCount == 1 (except for shadow piece)
484 const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1;
485 this.reserve[color][V.PAWN] -= reserveCount;
486 if (this.reserve[color][V.PAWN] == 0) this.reserve[color] = null;
487 }
488 if (!move.notTheEnd) {
489 this.turn = V.GetOppCol(color);
490 this.movesCount++;
491 this.captures.push([]);
492 }
493 }
494
495 undo(move) {
496 V.UndoOnBoard(this.board, move);
497 if (!move.notTheEnd) {
498 this.turn = move.turn;
499 this.movesCount--;
500 this.captures.pop();
501 }
502 if (move.vanish.length == 0) {
503 const color = (move.appear[0].c == 'A' ? 'w' : 'b');
504 const firstCode = (color == 'w' ? 65 : 97);
505 const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1;
506 if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 0 };
507 this.reserve[color][V.PAWN] += reserveCount;
508 }
509 else if (move.vanish.length == 2) {
510 const L0 = this.captures.length;
511 let captures = this.captures[L0 - 1];
512 captures.pop();
513 }
514 }
515
516 atLeastOneMove() {
517 const color = this.turn;
518 if (this.atLeastOneCapture(color)) return true;
519 for (let i = 0; i < V.size.x; i++) {
520 for (let j = 0; j < V.size.y; j++) {
521 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
522 const moves = this.getPotentialMovesFrom([i, j], []);
523 if (moves.length > 0) return true;
524 }
525 }
526 }
527 const reserveMoves =
528 this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
529 return (reserveMoves.length > 0);
530 }
531
532 getCurrentScore() {
533 const color = this.turn;
534 // If no pieces on board + reserve, I lose
535 if (!!this.reserve[color]) return "*";
536 let atLeastOnePiece = false;
537 outerLoop: for (let i=0; i < V.size.x; i++) {
538 for (let j=0; j < V.size.y; j++) {
539 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
540 atLeastOnePiece = true;
541 break outerLoop;
542 }
543 }
544 }
545 if (!atLeastOnePiece) return (color == 'w' ? "0-1" : "1-0");
546 if (!this.atLeastOneMove()) return "1/2";
547 return "*";
548 }
549
550 getComputerMove() {
551 // Random mover for now (TODO)
552 const color = this.turn;
553 let mvArray = [];
554 let mv = null;
555 while (this.turn == color) {
556 const moves = this.getAllValidMoves();
557 mv = moves[randInt(moves.length)];
558 mvArray.push(mv);
559 this.play(mv);
560 }
561 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
562 return (mvArray.length > 1 ? mvArray : mvArray[0]);
563 }
564
565 getNotation(move) {
566 if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end);
567 const L0 = this.captures.length;
568 if (this.captures[L0 - 1].length > 0) return V.CoordsToSquare(move.end);
569 return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
570 }
571
572 };