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