Fix Maxima (immobilize kings too)
[vchess.git] / client / src / variants / Fugue.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { shuffle } from "@/utils/alea";
4
5 export class FugueRules extends ChessRules {
6
7 static get HasFlags() {
8 return false;
9 }
10
11 static get HasEnpassant() {
12 return false;
13 }
14
15 static get LoseOnRepetition() {
16 return true;
17 }
18
19 static get IMMOBILIZER() {
20 return 'i';
21 }
22 static get PUSHME_PULLYOU() {
23 return 'u';
24 }
25 static get ARCHER() {
26 return 'a';
27 }
28 static get SHIELD() {
29 return 's';
30 }
31 static get LONG_LEAPER() {
32 return 'l';
33 }
34 static get SWAPPER() {
35 return 'w';
36 }
37
38 static get PIECES() {
39 return [
40 V.PAWN,
41 V.QUEEN,
42 V.KING,
43 V.IMMOBILIZER,
44 V.PUSHME_PULLYOU,
45 V.ARCHER,
46 V.SHIELD,
47 V.LONG_LEAPER,
48 V.SWAPPER
49 ];
50 }
51
52 getPpath(b) {
53 if (['p', 'q', 'k'].includes(b[1])) return b;
54 return "Fugue/" + b;
55 }
56
57 getPPpath(m) {
58 // The only "choice" case is between a swap and a mutual destruction:
59 // show empty square in case of mutual destruction.
60 if (m.appear.length == 0) return "Rococo/empty";
61 return this.getPpath(m.appear[0].c + m.appear[0].p);
62 }
63
64 scanKings(fen) {
65 // No castling, but keep track of kings for faster game end checks
66 this.kingPos = { w: [-1, -1], b: [-1, -1] };
67 const fenParts = fen.split(" ");
68 const position = fenParts[0].split("/");
69 for (let i = 0; i < position.length; i++) {
70 let k = 0;
71 for (let j = 0; j < position[i].length; j++) {
72 switch (position[i].charAt(j)) {
73 case "k":
74 this.kingPos["b"] = [i, k];
75 break;
76 case "K":
77 this.kingPos["w"] = [i, k];
78 break;
79 default: {
80 const num = parseInt(position[i].charAt(j), 10);
81 if (!isNaN(num)) k += num - 1;
82 }
83 }
84 k++;
85 }
86 }
87 }
88
89 // Is piece on square (x,y) immobilized?
90 isImmobilized([x, y]) {
91 const piece = this.getPiece(x, y);
92 if (piece == V.IMMOBILIZER) return false;
93 const oppCol = V.GetOppCol(this.getColor(x, y));
94 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
95 for (let step of adjacentSteps) {
96 const [i, j] = [x + step[0], y + step[1]];
97 if (
98 V.OnBoard(i, j) &&
99 this.board[i][j] != V.EMPTY &&
100 this.getColor(i, j) == oppCol
101 ) {
102 if (this.getPiece(i, j) == V.IMMOBILIZER) return true;
103 }
104 }
105 return false;
106 }
107
108 isProtected([x, y]) {
109 const color = this.getColor(x, y);
110 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
111 for (let s of steps) {
112 const [i, j] = [x + s[0], y + s[1]];
113 if (
114 V.OnBoard(i, j) &&
115 this.getColor(i, j) == color &&
116 this.getPiece(i, j) == V.SHIELD
117 ) {
118 return true;
119 }
120 }
121 return false;
122 }
123
124 canTake([x1, y1], [x2, y2]) {
125 return (
126 [V.QUEEN, V.KING].includes(this.getPiece(x1, y1)) &&
127 !this.isProtected([x2, y2]) &&
128 this.getColor(x1, y1) != this.getColor(x2, y2)
129 );
130 }
131
132 getPotentialMovesFrom([x, y]) {
133 // Pre-check: is thing on this square immobilized?
134 if (this.isImmobilized([x, y])) return [];
135 const piece = this.getPiece(x, y);
136 let moves = [];
137 switch (piece) {
138 case V.PAWN: return this.getPotentialPawnMoves([x, y]);
139 case V.IMMOBILIZER: return this.getPotentialImmobilizerMoves([x, y]);
140 case V.PUSHME_PULLYOU: return this.getPotentialPushmePullyuMoves([x, y]);
141 case V.ARCHER: return this.getPotentialArcherMoves([x, y]);
142 case V.SHIELD: return this.getPotentialShieldMoves([x, y]);
143 case V.KING: return this.getPotentialKingMoves([x, y]);
144 case V.QUEEN: return super.getPotentialQueenMoves([x, y]);
145 case V.LONG_LEAPER: return this.getPotentialLongLeaperMoves([x, y]);
146 case V.SWAPPER: return this.getPotentialSwapperMoves([x, y]);
147 }
148 }
149
150 // "Cannon/grasshopper pawn"
151 getPotentialPawnMoves([x, y]) {
152 const c = this.turn;
153 const oppCol = V.GetOppCol(c);
154 const lastRank = (c == 'w' ? 0 : 7);
155 let canResurect = {
156 [V.QUEEN]: true,
157 [V.IMMOBILIZER]: true,
158 [V.PUSHME_PULLYOU]: true,
159 [V.ARCHER]: true,
160 [V.SHIELD]: true,
161 [V.LONG_LEAPER]: true,
162 [V.SWAPPER]: true
163 };
164 for (let i = 0; i < 8; i++) {
165 for (let j = 0; j < 8; j++) {
166 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == c) {
167 const pIJ = this.getPiece(i, j);
168 if (![V.PAWN, V.KING].includes(pIJ)) canResurect[pIJ] = false;
169 }
170 }
171 }
172 let moves = [];
173 const addPromotions = sq => {
174 // Optional promotion
175 Object.keys(canResurect).forEach(p => {
176 if (canResurect[p]) {
177 moves.push(
178 this.getBasicMove([x, y], [sq[0], sq[1]], { c: c, p: p }));
179 }
180 });
181 }
182 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
183 adjacentSteps.forEach(step => {
184 const [i, j] = [x + step[0], y + step[1]];
185 if (V.OnBoard(i, j)) {
186 if (this.board[i][j] == V.EMPTY) {
187 moves.push(this.getBasicMove([x, y], [i, j]));
188 if (i == lastRank) addPromotions([i, j]);
189 }
190 else {
191 // Try to leap over:
192 const [ii, jj] = [i + step[0], j + step[1]];
193 if (
194 V.OnBoard(ii, jj) &&
195 (
196 this.board[ii][jj] == V.EMPTY ||
197 this.getColor(ii, jj) == oppCol && !this.isProtected([ii, jj])
198 )
199 ) {
200 moves.push(this.getBasicMove([x, y], [ii, jj]));
201 if (ii == lastRank) addPromotions([ii, jj]);
202 }
203 }
204 }
205 });
206 return moves;
207 }
208
209 getPotentialKingMoves(sq) {
210 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
211 return this.getSlideNJumpMoves(sq, steps, 1);
212 }
213
214 // NOTE: not really captures, but let's keep the name
215 getSwapperCaptures([x, y]) {
216 let moves = [];
217 const oppCol = V.GetOppCol(this.turn);
218 // Simple: if something is visible, we can swap
219 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
220 let [i, j] = [x + step[0], y + step[1]];
221 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
222 i += step[0];
223 j += step[1];
224 }
225 if (V.OnBoard(i, j) && this.getColor(i, j) == oppCol) {
226 const oppPiece = this.getPiece(i, j);
227 let m = this.getBasicMove([x, y], [i, j]);
228 m.appear.push(
229 new PiPo({
230 x: x,
231 y: y,
232 c: oppCol,
233 p: this.getPiece(i, j)
234 })
235 );
236 moves.push(m);
237 if (
238 i == x + step[0] && j == y + step[1] &&
239 !this.isProtected([i, j])
240 ) {
241 // Add mutual destruction option:
242 m = new Move({
243 start: { x: x, y: y},
244 end: { x: i, y: j },
245 appear: [],
246 // TODO: is copying necessary here?
247 vanish: JSON.parse(JSON.stringify(m.vanish))
248 });
249 moves.push(m);
250 }
251 }
252 });
253 return moves;
254 }
255
256 getPotentialSwapperMoves(sq) {
257 return (
258 super.getPotentialQueenMoves(sq).concat(this.getSwapperCaptures(sq))
259 );
260 }
261
262 getLongLeaperCaptures([x, y]) {
263 // Look in every direction for captures
264 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
265 const color = this.turn;
266 const oppCol = V.GetOppCol(color);
267 let moves = [];
268 const piece = this.getPiece(x, y);
269 outerLoop: for (let step of steps) {
270 let [i, j] = [x + step[0], y + step[1]];
271 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
272 i += step[0];
273 j += step[1];
274 }
275 if (
276 !V.OnBoard(i, j) ||
277 this.getColor(i, j) == color ||
278 this.isProtected([i, j])
279 ) {
280 continue;
281 }
282 let [ii, jj] = [i + step[0], j + step[1]];
283 const vanished = [
284 new PiPo({ x: x, y: y, c: color, p: piece }),
285 new PiPo({ x: i, y: j, c: oppCol, p: this.getPiece(i, j)})
286 ];
287 while (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY) {
288 moves.push(
289 new Move({
290 appear: [new PiPo({ x: ii, y: jj, c: color, p: piece })],
291 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
292 start: { x: x, y: y },
293 end: { x: ii, y: jj }
294 })
295 );
296 ii += step[0];
297 jj += step[1];
298 }
299 }
300 return moves;
301 }
302
303 getPotentialLongLeaperMoves(sq) {
304 return (
305 super.getPotentialQueenMoves(sq).concat(this.getLongLeaperCaptures(sq))
306 );
307 }
308
309 completeAndFilterPPcaptures(moves) {
310 if (moves.length == 0) return [];
311 const [x, y] = [moves[0].start.x, moves[0].start.y];
312 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
313 let capturingDirStart = {};
314 const oppCol = V.GetOppCol(this.turn);
315 // Useful precomputation:
316 adjacentSteps.forEach(step => {
317 const [i, j] = [x + step[0], y + step[1]];
318 if (
319 V.OnBoard(i, j) &&
320 this.board[i][j] != V.EMPTY &&
321 this.getColor(i, j) == oppCol
322 ) {
323 capturingDirStart[step[0] + "_" + step[1]] = {
324 p: this.getPiece(i, j),
325 canTake: !this.isProtected([i, j])
326 };
327 }
328 });
329 moves.forEach(m => {
330 const step = [
331 m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
332 m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
333 ];
334 // TODO: this test should be done only once per direction
335 const capture = capturingDirStart[(-step[0]) + "_" + (-step[1])];
336 if (!!capture && capture.canTake) {
337 const [i, j] = [x - step[0], y - step[1]];
338 m.vanish.push(
339 new PiPo({
340 x: i,
341 y: j,
342 p: capture.p,
343 c: oppCol
344 })
345 );
346 }
347 // Also test the end (advancer effect)
348 const [i, j] = [m.end.x + step[0], m.end.y + step[1]];
349 if (
350 V.OnBoard(i, j) &&
351 this.board[i][j] != V.EMPTY &&
352 this.getColor(i, j) == oppCol &&
353 !this.isProtected([i, j])
354 ) {
355 m.vanish.push(
356 new PiPo({
357 x: i,
358 y: j,
359 p: this.getPiece(i, j),
360 c: oppCol
361 })
362 );
363 }
364 });
365 // Forbid "double captures"
366 return moves.filter(m => m.vanish.length <= 2);
367 }
368
369 getPotentialPushmePullyuMoves(sq) {
370 let moves = super.getPotentialQueenMoves(sq);
371 return this.completeAndFilterPPcaptures(moves);
372 }
373
374 getPotentialImmobilizerMoves(sq) {
375 // Immobilizer doesn't capture
376 return super.getPotentialQueenMoves(sq);
377 }
378
379 getPotentialArcherMoves([x, y]) {
380 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
381 let moves = this.getSlideNJumpMoves([x, y], steps);
382 const c = this.turn;
383 const oppCol = V.GetOppCol(c);
384 // Add captures
385 for (let s of steps) {
386 let [i, j] = [x + s[0], y + s[1]];
387 let stepCounter = 1;
388 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
389 i += s[0];
390 j += s[1];
391 stepCounter++;
392 }
393 if (
394 V.OnBoard(i, j) &&
395 this.getColor(i, j) == oppCol &&
396 !this.isProtected([i, j])
397 ) {
398 let shootOk = (stepCounter <= 2);
399 if (!shootOk) {
400 // try to find a spotting piece:
401 for (let ss of steps) {
402 let [ii, jj] = [i + ss[0], j + ss[1]];
403 if (V.OnBoard(ii, jj)) {
404 if (this.board[ii][jj] != V.EMPTY) {
405 if (this.getColor(ii, jj) == c) {
406 shootOk = true;
407 break;
408 }
409 }
410 else {
411 ii += ss[0];
412 jj += ss[1];
413 if (
414 V.OnBoard(ii, jj) &&
415 this.board[ii][jj] != V.EMPTY &&
416 this.getColor(ii, jj) == c
417 ) {
418 shootOk = true;
419 break;
420 }
421 }
422 }
423 }
424 }
425 if (shootOk) {
426 moves.push(
427 new Move({
428 appear: [],
429 vanish: [
430 new PiPo({ x: i, y: j, c: oppCol, p: this.getPiece(i, j) })
431 ],
432 start: { x: x, y: y },
433 end: { x: i, y: j }
434 })
435 );
436 }
437 }
438 }
439 return moves;
440 }
441
442 getPotentialShieldMoves(sq) {
443 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
444 return this.getSlideNJumpMoves(sq, steps);
445 }
446
447 getCheckSquares() {
448 return [];
449 }
450
451 filterValid(moves) {
452 return moves;
453 }
454
455 getCurrentScore() {
456 const c = this.turn;
457 if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
458 if (this.atLeastOneMove()) return "*";
459 // Stalemate, or checkmate: I lose
460 return (c == 'w' ? "0-1" : "1-0");
461 }
462
463 postPlay(move) {
464 const startIdx = (move.appear.length == 0 ? 0 : 1);
465 for (let i = startIdx; i < move.vanish.length; i++) {
466 const v = move.vanish[i];
467 if (v.p == V.KING) this.kingPos[v.c] = [-1, -1];
468 }
469 // King may have moved, or was swapped
470 for (let a of move.appear) {
471 if (a.p == V.KING) {
472 this.kingPos[a.c] = [a.x, a.y];
473 break;
474 }
475 }
476 }
477
478 postUndo(move) {
479 const startIdx = (move.appear.length == 0 ? 0 : 1);
480 for (let i = startIdx; i < move.vanish.length; i++) {
481 const v = move.vanish[i];
482 if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
483 }
484 // King may have moved, or was swapped
485 for (let i = 0; i < move.appear.length; i++) {
486 const a = move.appear[i];
487 if (a.p == V.KING) {
488 const v = move.vanish[i];
489 this.kingPos[a.c] = [v.x, v.y];
490 break;
491 }
492 }
493 }
494
495 static GenRandInitFen(options) {
496 if (options.randomness == 0) {
497 return (
498 "wlqksaui/pppppppp/8/8/8/8/PPPPPPPP/IUASKQLW w 0"
499 );
500 }
501
502 let pieces = { w: new Array(8), b: new Array(8) };
503 // Shuffle pieces on first and last rank
504 for (let c of ["w", "b"]) {
505 if (c == 'b' && options.randomness == 1) {
506 pieces['b'] = pieces['w'];
507 break;
508 }
509
510 // Get random squares for every piece, totally freely
511 let positions = shuffle(ArrayFun.range(8));
512 const composition = ['w', 'l', 'q', 'k', 's', 'a', 'u', 'i'];
513 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
514 }
515 return (
516 pieces["b"].join("") +
517 "/pppppppp/8/8/8/8/PPPPPPPP/" +
518 pieces["w"].join("").toUpperCase() + " w 0"
519 );
520 }
521
522 static get VALUES() {
523 // Experimental...
524 return {
525 p: 1,
526 q: 9,
527 l: 5,
528 s: 5,
529 a: 5,
530 u: 5,
531 i: 12,
532 w: 3,
533 k: 1000
534 };
535 }
536
537 static get SEARCH_DEPTH() {
538 return 2;
539 }
540
541 getNotation(move) {
542 const initialSquare = V.CoordsToSquare(move.start);
543 const finalSquare = V.CoordsToSquare(move.end);
544 if (move.appear.length == 0) {
545 // Archer shooting 'S' or Mutual destruction 'D':
546 return (
547 initialSquare + (move.vanish.length == 1 ? "S" : "D") + finalSquare
548 );
549 }
550 let notation = undefined;
551 const symbol = move.appear[0].p.toUpperCase();
552 if (symbol == 'P')
553 // Pawn: generally ambiguous short notation, so we use full description
554 notation = "P" + initialSquare + finalSquare;
555 else if (['Q', 'K'].includes(symbol))
556 notation = symbol + (move.vanish.length > 1 ? "x" : "") + finalSquare;
557 else {
558 notation = symbol + finalSquare;
559 // Add a capture mark (not describing what is captured...):
560 if (move.vanish.length > 1) notation += "X";
561 }
562 return notation;
563 }
564
565 };