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