caa2e982e1fea779b598e2dfae6d1f631d1f4853
[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 = {
139 w: { [V.PAWN]: reserve[0] },
140 b: { [V.PAWN]: reserve[1] }
141 };
142 // Local stack of captures during a turn (squares + directions)
143 this.captures = [ [] ];
144 }
145
146 atLeastOneCaptureFrom([x, y], color) {
147 for (let s of V.steps[V.BISHOP]) {
148 const [i, j] = [x + s[0], y + s[1]];
149 if (
150 V.OnBoard(i + s[0], j + s[1]) &&
151 this.board[i][j] != V.EMPTY &&
152 this.getColor(i, j) != color &&
153 this.board[i + s[0]][j + s[1]] == V.EMPTY
154 ) {
155 return true;
156 }
157 }
158 return false;
159 }
160
161 atLeastOneCapture(color) {
162 const L0 = this.captures.length;
163 const captures = this.captures[L0 - 1];
164 const L = captures.length;
165 if (L > 0) return this.atLeastOneCaptureFrom(captures[L-1].square, color);
166 for (let i = 0; i < V.size.x; i++) {
167 for (let j=0; j< V.size.y; j++) {
168 if (
169 this.board[i][j] != V.EMPTY &&
170 this.getColor(i, j) == color &&
171 this.atLeastOneCaptureFrom([i, j], color)
172 ) {
173 return true;
174 }
175 }
176 }
177 return false;
178 }
179
180 maxLengthIndices(caps) {
181 let maxLength = 0;
182 let res = [];
183 for (let i = 0; i < caps.length; i++) {
184 if (caps[i].length > maxLength) {
185 res = [i];
186 maxLength = caps[i].length;
187 }
188 else if (caps[i].length == maxLength) res.push(i);
189 }
190 return res;
191 };
192
193 getLongestCapturesFrom([x, y], color, locSteps) {
194 //
195 // TODO: debug here, from
196 // 9/9/2a@1a@4/5A@3/9/3aa1A@3/9/9/8A@ w 10 8,9
197 // White to move, double capture.
198 //
199 let res = [];
200 const L = locSteps.length;
201 const lastStep = (L > 0 ? locSteps[L-1] : null);
202 for (let s of V.steps[V.BISHOP]) {
203 if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
204 const [i, j] = [x + s[0], y + s[1]];
205 if (
206 V.OnBoard(i + s[0], j + s[1]) &&
207 this.board[i + s[0]][j + s[1]] == V.EMPTY &&
208 this.board[i][j] != V.EMPTY &&
209 this.getColor(i, j) != color
210 ) {
211 const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
212 locSteps.push(s);
213 V.PlayOnBoard(this.board, move);
214 const sRes = this.getLongestCapturesFrom(
215 [i + s[0], j + s[1]], color, locSteps);
216 res.push({
217 step: s,
218 length: 1 + (sRes.length == 0 ? 0 : sRes[0].length)
219 });
220 locSteps.pop();
221 V.UndoOnBoard(this.board, move);
222 }
223 }
224 return this.maxLengthIndices(res).map(i => res[i]);
225 }
226
227 getAllLongestCaptures(color) {
228 const L0 = this.captures.length;
229 const captures = this.captures[L0 - 1];
230 const L = captures.length;
231 if (L > 0) {
232 let locSteps = [];
233 const caps = Object.assign(
234 { square: captures[L-1].square },
235 this.getLongestCapturesFrom(captures[L-1].square, color, locSteps)
236 );
237 return this.maxLengthIndices(caps).map(i => caps[i]);
238 }
239 let caps = [];
240 for (let i = 0; i < V.size.x; i++) {
241 for (let j=0; j < V.size.y; j++) {
242 if (
243 this.board[i][j] != V.EMPTY &&
244 this.getColor(i, j) == color
245 ) {
246 let locSteps = [];
247 let res = this.getLongestCapturesFrom([i, j], color, locSteps);
248 Array.prototype.push.apply(
249 caps,
250 res.map(r => Object.assign({ square: [i, j] }, r))
251 );
252 }
253 }
254 }
255
256 console.log(caps);
257
258 return this.maxLengthIndices(caps).map(i => caps[i]);
259 }
260
261 getBasicMove([x1, y1], [x2, y2], capt) {
262 const cp1 = this.board[x1][y1];
263 if (!capt) {
264 return new Move({
265 appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ],
266 vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ]
267 });
268 }
269 // Compute resulting types based on jumped + jumping pieces
270 const cpCapt = this.board[capt[0]][capt[1]];
271 const newAtCapt = cpCapt.charCodeAt(0) - 1;
272 const newAtDest =
273 cp1[1] == '@'
274 ? (cp1.charCodeAt(0) < 97 ? 65 : 97)
275 : (cp1.charCodeAt(1) + 1);
276 const color = this.turn;
277 let mv = new Move({
278 appear: [
279 new PiPo({
280 x: x2,
281 y: y2,
282 c: cp1[0],
283 p: String.fromCharCode(newAtDest)
284 })
285 ],
286 vanish: [
287 new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }),
288 new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] })
289 ]
290 });
291 if ([64, 96].includes(newAtCapt)) {
292 // Enemy units vanish from capturing square
293 if (cpCapt.charAt(1) != '@') {
294 // Out units remain:
295 mv.appear.push(
296 new PiPo({
297 x: capt[0],
298 y: capt[1],
299 c: cpCapt[0],
300 p: '@'
301 })
302 );
303 }
304 }
305 else {
306 mv.appear.push(
307 new PiPo({
308 x: capt[0],
309 y: capt[1],
310 c: String.fromCharCode(newAtCapt),
311 p: cpCapt[1]
312 })
313 );
314 }
315 return mv;
316 }
317
318 getReserveMoves(x) {
319 const color = this.turn;
320 if (!this.reserve[color] || this.atLeastOneCapture(color)) return [];
321 let moves = [];
322 const shadowPiece =
323 this.reserve[V.GetOppCol(color)] == null
324 ? this.reserve[color][V.PAWN] - 1
325 : 0;
326 const appearColor = String.fromCharCode(
327 (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece);
328 const addMove = ([i, j]) => {
329 moves.push(
330 new Move({
331 appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ],
332 vanish: [],
333 start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 }
334 })
335 );
336 };
337 const oppCol = V.GetOppCol(color);
338 const opponentCanCapture = this.atLeastOneCapture(oppCol);
339 for (let i = 0; i < V.size.x; i++) {
340 for (let j = i % 2; j < V.size.y; j += 2) {
341 if (
342 this.board[i][j] == V.EMPTY &&
343 // prevent playing on central square at move 1:
344 (this.movesCount >= 1 || i != 4 || j != 4)
345 ) {
346 if (opponentCanCapture) addMove([i, j]);
347 else {
348 let canAddMove = true;
349 for (let s of V.steps[V.BISHOP]) {
350 if (
351 V.OnBoard(i + s[0], j + s[1]) &&
352 V.OnBoard(i - s[0], j - s[1]) &&
353 this.board[i + s[0]][j + s[1]] != V.EMPTY &&
354 this.board[i - s[0]][j - s[1]] == V.EMPTY &&
355 this.getColor(i + s[0], j + s[1]) == oppCol
356 ) {
357 canAddMove = false;
358 break;
359 }
360 }
361 if (canAddMove) addMove([i, j]);
362 }
363 }
364 }
365 }
366 return moves;
367 }
368
369 getPotentialMovesFrom([x, y], longestCaptures) {
370 if (x >= V.size.x) {
371 if (longestCaptures.length == 0) return this.getReserveMoves(x);
372 return [];
373 }
374 const color = this.turn;
375 const L0 = this.captures.length;
376 const captures = this.captures[L0 - 1];
377 const L = captures.length;
378 let moves = [];
379 if (longestCaptures.length > 0) {
380 if (
381 L > 0 &&
382 (x != captures[L-1].square[0] || y != captures[L-1].square[1])
383 ) {
384 return [];
385 }
386 longestCaptures.forEach(lc => {
387 if (lc.square[0] == x && lc.square[1] == y) {
388 const s = lc.step;
389 const [i, j] = [x + s[0], y + s[1]];
390 moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]));
391 }
392 });
393 return moves;
394 }
395 // Just search simple moves:
396 for (let s of V.steps[V.BISHOP]) {
397 const [i, j] = [x + s[0], y + s[1]];
398 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
399 moves.push(this.getBasicMove([x, y], [i, j]));
400 }
401 return moves;
402 }
403
404 getAllValidMoves() {
405 const color = this.turn;
406 const longestCaptures = this.getAllLongestCaptures(color);
407 let potentialMoves = [];
408 for (let i = 0; i < V.size.x; i++) {
409 for (let j = 0; j < V.size.y; j++) {
410 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
411 Array.prototype.push.apply(
412 potentialMoves,
413 this.getPotentialMovesFrom([i, j], longestCaptures)
414 );
415 }
416 }
417 }
418 // Add reserve moves
419 potentialMoves = potentialMoves.concat(
420 this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1))
421 );
422 return potentialMoves;
423 }
424
425 getPossibleMovesFrom([x, y]) {
426 const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y));
427 return this.getPotentialMovesFrom([x, y], longestCaptures);
428 }
429
430 filterValid(moves) {
431 return moves;
432 }
433
434 getCheckSquares() {
435 return [];
436 }
437
438 play(move) {
439 const color = this.turn;
440 move.turn = color; //for undo
441 V.PlayOnBoard(this.board, move);
442 if (move.vanish.length == 2) {
443 const L0 = this.captures.length;
444 let captures = this.captures[L0 - 1];
445 captures.push({
446 square: [move.start.x, move.start.y],
447 step: [move.end.x - move.start.x, move.end.y - move.start.y]
448 });
449 if (this.atLeastOneCapture())
450 // There could be other captures (optional)
451 move.notTheEnd = true;
452 }
453 else if (move.vanish == 0) {
454 if (--this.reserve[color][V.PAWN] == 0) this.reserve[color] = null;
455 }
456 if (!move.notTheEnd) {
457 this.turn = V.GetOppCol(color);
458 this.movesCount++;
459 this.captures.push([]);
460 }
461 }
462
463 undo(move) {
464 V.UndoOnBoard(this.board, move);
465 if (!move.notTheEnd) {
466 this.turn = move.turn;
467 this.movesCount--;
468 this.captures.pop();
469 }
470 if (move.vanish.length == 0) {
471 const color = (move.appear[0].c == 'A' ? 'w' : 'b');
472 if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 1 };
473 else this.reserve[color][V.PAWN]++;
474 }
475 else if (move.vanish.length == 2) {
476 const L0 = this.captures.length;
477 let captures = this.captures[L0 - 1];
478 captures.pop();
479 }
480 }
481
482 atLeastOneMove() {
483 if (this.atLeastOneCapture()) return true;
484 const color = this.turn;
485 for (let i = 0; i < V.size.x; i++) {
486 for (let j = 0; j < V.size.y; j++) {
487 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
488 const moves = this.getPotentialMovesFrom([i, j], []);
489 if (moves.length > 0) return true;
490 }
491 }
492 }
493 const reserveMoves =
494 this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
495 return (reserveMoves.length > 0);
496 }
497
498 getCurrentScore() {
499 const color = this.turn;
500 // If no pieces on board + reserve, I lose
501 if (
502 !this.reserve[color] &&
503 this.board.every(b => {
504 return b.every(cell => {
505 return (cell == "" || cell[0] != color);
506 });
507 })
508 ) {
509 return (color == 'w' ? "0-1" : "1-0");
510 }
511 if (!this.atLeastOneMove()) return "1/2";
512 return "*";
513 }
514
515 getComputerMove() {
516 // Random mover for now (TODO)
517 const color = this.turn;
518 let mvArray = [];
519 let mv = null;
520 while (this.turn == color) {
521 const moves = this.getAllValidMoves();
522 mv = moves[randInt(moves.length)];
523 mvArray.push(mv);
524 this.play(mv);
525 }
526 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
527 return (mvArray.length > 1 ? mvArray : mvArray[0]);
528 }
529
530 getNotation(move) {
531 if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end);
532 return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
533 }
534
535 };