c29324ace0893406499705b9c2d0b173215c82a0
[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 !this.isProtected([x2, y2]) && super.canTake([x1, y1], [x2, y2]);
126 }
127
128 getPotentialMovesFrom([x, y]) {
129 // Pre-check: is thing on this square immobilized?
130 if (this.isImmobilized([x, y])) return [];
131 const piece = this.getPiece(x, y);
132 let moves = [];
133 switch (piece) {
134 case V.PAWN: return this.getPotentialPawnMoves([x, y]);
135 case V.IMMOBILIZER: return this.getPotentialImmobilizerMoves([x, y]);
136 case V.PUSHME_PULLYOU: return this.getPotentialPushmePullyuMoves([x, y]);
137 case V.ARCHER: return this.getPotentialArcherMoves([x, y]);
138 case V.SHIELD: return this.getPotentialShieldMoves([x, y]);
139 case V.KING: return this.getPotentialKingMoves([x, y]);
140 case V.QUEEN: return super.getPotentialQueenMoves([x, y]);
141 case V.LONG_LEAPER: return this.getPotentialLongLeaperMoves([x, y]);
142 case V.SWAPPER: return this.getPotentialSwapperMoves([x, y]);
143 }
144 }
145
146 getSlideNJumpMoves([x, y], steps, oneStep) {
147 const piece = this.getPiece(x, y);
148 let moves = [];
149 outerLoop: for (let step of steps) {
150 let i = x + step[0];
151 let j = y + step[1];
152 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
153 moves.push(this.getBasicMove([x, y], [i, j]));
154 if (oneStep !== undefined) continue outerLoop;
155 i += step[0];
156 j += step[1];
157 }
158 // Only queen and king can take on occupied square:
159 if (
160 [V.KING, V.QUEEN].includes(piece) &&
161 V.OnBoard(i, j) &&
162 this.canTake([x, y], [i, j])
163 ) {
164 moves.push(this.getBasicMove([x, y], [i, j]));
165 }
166 }
167 return moves;
168 }
169
170 // "Cannon/grasshopper pawn"
171 getPotentialPawnMoves([x, y]) {
172 const c = this.turn;
173 const oppCol = V.GetOppCol(c);
174 const lastRank = (c == 'w' ? 0 : 7);
175 let canResurect = {
176 [V.QUEEN]: true,
177 [V.IMMOBILIZER]: true,
178 [V.PUSHME_PULLYOU]: true,
179 [V.ARCHER]: true,
180 [V.SHIELD]: true,
181 [V.LONG_LEAPER]: true,
182 [V.SWAPPER]: true
183 };
184 for (let i = 0; i < 8; i++) {
185 for (let j = 0; j < 8; j++) {
186 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == c) {
187 const pIJ = this.getPiece(i, j);
188 if (![V.PAWN, V.KING].includes(pIJ)) canResurect[pIJ] = false;
189 }
190 }
191 }
192 let moves = [];
193 const addPromotions = sq => {
194 // Optional promotion
195 Object.keys(canResurect).forEach(p => {
196 if (canResurect[p]) {
197 moves.push(
198 this.getBasicMove([x, y], [sq[0], sq[1]], { c: c, p: p }));
199 }
200 });
201 }
202 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
203 adjacentSteps.forEach(step => {
204 const [i, j] = [x + step[0], y + step[1]];
205 if (V.OnBoard(i, j)) {
206 if (this.board[i][j] == V.EMPTY) {
207 moves.push(this.getBasicMove([x, y], [i, j]));
208 if (i == lastRank) addPromotions([i, j]);
209 }
210 else {
211 // Try to leap over:
212 const [ii, jj] = [i + step[0], j + step[1]];
213 if (
214 V.OnBoard(ii, jj) &&
215 (
216 this.board[ii][jj] == V.EMPTY ||
217 this.getColor(ii, jj) == oppCol && !this.isProtected([ii, jj])
218 )
219 ) {
220 moves.push(this.getBasicMove([x, y], [ii, jj]));
221 if (ii == lastRank) addPromotions([ii, jj]);
222 }
223 }
224 }
225 });
226 return moves;
227 }
228
229 getPotentialKingMoves(sq) {
230 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
231 return this.getSlideNJumpMoves(sq, steps, "oneStep");
232 }
233
234 // NOTE: not really captures, but let's keep the name
235 getSwapperCaptures([x, y]) {
236 let moves = [];
237 const oppCol = V.GetOppCol(this.turn);
238 // Simple: if something is visible, we can swap
239 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
240 let [i, j] = [x + step[0], y + step[1]];
241 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
242 i += step[0];
243 j += step[1];
244 }
245 if (V.OnBoard(i, j) && this.getColor(i, j) == oppCol) {
246 const oppPiece = this.getPiece(i, j);
247 let m = this.getBasicMove([x, y], [i, j]);
248 m.appear.push(
249 new PiPo({
250 x: x,
251 y: y,
252 c: oppCol,
253 p: this.getPiece(i, j)
254 })
255 );
256 moves.push(m);
257 if (
258 i == x + step[0] && j == y + step[1] &&
259 !this.isProtected([i, j])
260 ) {
261 // Add mutual destruction option:
262 m = new Move({
263 start: { x: x, y: y},
264 end: { x: i, y: j },
265 appear: [],
266 // TODO: is copying necessary here?
267 vanish: JSON.parse(JSON.stringify(m.vanish))
268 });
269 moves.push(m);
270 }
271 }
272 });
273 return moves;
274 }
275
276 getPotentialSwapperMoves(sq) {
277 return (
278 super.getPotentialQueenMoves(sq).concat(this.getSwapperCaptures(sq))
279 );
280 }
281
282 getLongLeaperCaptures([x, y]) {
283 // Look in every direction for captures
284 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
285 const color = this.turn;
286 const oppCol = V.GetOppCol(color);
287 let moves = [];
288 const piece = this.getPiece(x, y);
289 outerLoop: for (let step of steps) {
290 let [i, j] = [x + step[0], y + step[1]];
291 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
292 i += step[0];
293 j += step[1];
294 }
295 if (
296 !V.OnBoard(i, j) ||
297 this.getColor(i, j) == color ||
298 this.isProtected([i, j])
299 ) {
300 continue;
301 }
302 let [ii, jj] = [i + step[0], j + step[1]];
303 const vanished = [
304 new PiPo({ x: x, y: y, c: color, p: piece }),
305 new PiPo({ x: i, y: j, c: oppCol, p: this.getPiece(i, j)})
306 ];
307 while (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY) {
308 moves.push(
309 new Move({
310 appear: [new PiPo({ x: ii, y: jj, c: color, p: piece })],
311 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
312 start: { x: x, y: y },
313 end: { x: ii, y: jj }
314 })
315 );
316 ii += step[0];
317 jj += step[1];
318 }
319 }
320 return moves;
321 }
322
323 getPotentialLongLeaperMoves(sq) {
324 return (
325 super.getPotentialQueenMoves(sq).concat(this.getLongLeaperCaptures(sq))
326 );
327 }
328
329 completeAndFilterPPcaptures(moves) {
330 if (moves.length == 0) return [];
331 const [x, y] = [moves[0].start.x, moves[0].start.y];
332 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
333 let capturingDirStart = {};
334 const oppCol = V.GetOppCol(this.turn);
335 // Useful precomputation:
336 adjacentSteps.forEach(step => {
337 const [i, j] = [x + step[0], y + step[1]];
338 if (
339 V.OnBoard(i, j) &&
340 this.board[i][j] != V.EMPTY &&
341 this.getColor(i, j) == oppCol
342 ) {
343 capturingDirStart[step[0] + "_" + step[1]] = {
344 p: this.getPiece(i, j),
345 canTake: !this.isProtected([i, j])
346 };
347 }
348 });
349 moves.forEach(m => {
350 const step = [
351 m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
352 m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
353 ];
354 // TODO: this test should be done only once per direction
355 const capture = capturingDirStart[(-step[0]) + "_" + (-step[1])];
356 if (!!capture && capture.canTake) {
357 const [i, j] = [x - step[0], y - step[1]];
358 m.vanish.push(
359 new PiPo({
360 x: i,
361 y: j,
362 p: capture.p,
363 c: oppCol
364 })
365 );
366 }
367 // Also test the end (advancer effect)
368 const [i, j] = [m.end.x + step[0], m.end.y + step[1]];
369 if (
370 V.OnBoard(i, j) &&
371 this.board[i][j] != V.EMPTY &&
372 this.getColor(i, j) == oppCol &&
373 !this.isProtected([i, j])
374 ) {
375 m.vanish.push(
376 new PiPo({
377 x: i,
378 y: j,
379 p: this.getPiece(i, j),
380 c: oppCol
381 })
382 );
383 }
384 });
385 // Forbid "double captures"
386 return moves.filter(m => m.vanish.length <= 2);
387 }
388
389 getPotentialPushmePullyuMoves(sq) {
390 let moves = super.getPotentialQueenMoves(sq);
391 return this.completeAndFilterPPcaptures(moves);
392 }
393
394 getPotentialImmobilizerMoves(sq) {
395 // Immobilizer doesn't capture
396 return super.getPotentialQueenMoves(sq);
397 }
398
399 getPotentialArcherMoves([x, y]) {
400 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
401 let moves = this.getSlideNJumpMoves([x, y], steps);
402 const c = this.turn;
403 const oppCol = V.GetOppCol(c);
404 // Add captures
405 for (let s of steps) {
406 let [i, j] = [x + s[0], y + s[1]];
407 let stepCounter = 1;
408 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
409 i += s[0];
410 j += s[1];
411 stepCounter++;
412 }
413 if (
414 V.OnBoard(i, j) &&
415 this.getColor(i, j) == oppCol &&
416 !this.isProtected([i, j])
417 ) {
418 let shootOk = (stepCounter <= 2);
419 if (!shootOk) {
420 // try to find a spotting piece:
421 for (let ss of steps) {
422 let [ii, jj] = [i + ss[0], j + ss[1]];
423 if (V.OnBoard(ii, jj)) {
424 if (this.board[ii][jj] != V.EMPTY) {
425 if (this.getColor(ii, jj) == c) {
426 shootOk = true;
427 break;
428 }
429 }
430 else {
431 ii += ss[0];
432 jj += ss[1];
433 if (
434 V.OnBoard(ii, jj) &&
435 this.board[ii][jj] != V.EMPTY &&
436 this.getColor(ii, jj) == c
437 ) {
438 shootOk = true;
439 break;
440 }
441 }
442 }
443 }
444 }
445 if (shootOk) {
446 moves.push(
447 new Move({
448 appear: [],
449 vanish: [
450 new PiPo({ x: i, y: j, c: oppCol, p: this.getPiece(i, j) })
451 ],
452 start: { x: x, y: y },
453 end: { x: i, y: j }
454 })
455 );
456 }
457 }
458 }
459 return moves;
460 }
461
462 getPotentialShieldMoves(sq) {
463 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
464 return this.getSlideNJumpMoves(sq, steps);
465 }
466
467 getCheckSquares() {
468 return [];
469 }
470
471 filterValid(moves) {
472 return moves;
473 }
474
475 getCurrentScore() {
476 const c = this.turn;
477 if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
478 if (this.atLeastOneMove()) return "*";
479 // Stalemate, or checkmate: I lose
480 return (c == 'w' ? "0-1" : "1-0");
481 }
482
483 postPlay(move) {
484 const startIdx = (move.appear.length == 0 ? 0 : 1);
485 for (let i = startIdx; i < move.vanish.length; i++) {
486 const v = move.vanish[i];
487 if (v.p == V.KING) this.kingPos[v.c] = [-1, -1];
488 }
489 // King may have moved, or was swapped
490 for (let a of move.appear) {
491 if (a.p == V.KING) {
492 this.kingPos[a.c] = [a.x, a.y];
493 break;
494 }
495 }
496 }
497
498 postUndo(move) {
499 const startIdx = (move.appear.length == 0 ? 0 : 1);
500 for (let i = startIdx; i < move.vanish.length; i++) {
501 const v = move.vanish[i];
502 if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
503 }
504 // King may have moved, or was swapped
505 for (let i = 0; i < move.appear.length; i++) {
506 const a = move.appear[i];
507 if (a.p == V.KING) {
508 const v = move.vanish[i];
509 this.kingPos[a.c] = [v.x, v.y];
510 break;
511 }
512 }
513 }
514
515 static GenRandInitFen(randomness) {
516 if (randomness == 0) {
517 return (
518 "wlqksaui/pppppppp/8/8/8/8/PPPPPPPP/IUASKQLW w 0"
519 );
520 }
521
522 let pieces = { w: new Array(8), b: new Array(8) };
523 // Shuffle pieces on first and last rank
524 for (let c of ["w", "b"]) {
525 if (c == 'b' && randomness == 1) {
526 pieces['b'] = pieces['w'];
527 break;
528 }
529
530 // Get random squares for every piece, totally freely
531 let positions = shuffle(ArrayFun.range(8));
532 const composition = ['w', 'l', 'q', 'k', 's', 'a', 'u', 'i'];
533 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
534 }
535 return (
536 pieces["b"].join("") +
537 "/pppppppp/8/8/8/8/PPPPPPPP/" +
538 pieces["w"].join("").toUpperCase() + " w 0"
539 );
540 }
541
542 static get VALUES() {
543 // Experimental...
544 return {
545 p: 1,
546 q: 9,
547 l: 5,
548 s: 5,
549 a: 5,
550 u: 5,
551 i: 12,
552 w: 3,
553 k: 1000
554 };
555 }
556
557 static get SEARCH_DEPTH() {
558 return 2;
559 }
560
561 getNotation(move) {
562 const initialSquare = V.CoordsToSquare(move.start);
563 const finalSquare = V.CoordsToSquare(move.end);
564 if (move.appear.length == 0) {
565 // Archer shooting 'S' or Mutual destruction 'D':
566 return (
567 initialSquare + (move.vanish.length == 1 ? "S" : "D") + finalSquare
568 );
569 }
570 let notation = undefined;
571 const symbol = move.appear[0].p.toUpperCase();
572 if (symbol == 'P')
573 // Pawn: generally ambiguous short notation, so we use full description
574 notation = "P" + initialSquare + finalSquare;
575 else if (['Q', 'K'].includes(symbol))
576 notation = symbol + (move.vanish.length > 1 ? "x" : "") + finalSquare;
577 else {
578 notation = symbol + finalSquare;
579 // Add a capture mark (not describing what is captured...):
580 if (move.vanish.length > 1) notation += "X";
581 }
582 return notation;
583 }
584
585 };