1 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
2 import { ArrayFun
} from "@/utils/array";
3 import { shuffle
} from "@/utils/alea";
5 export class FugueRules
extends ChessRules
{
7 static get HasFlags() {
11 static get HasEnpassant() {
15 static get LoseOnRepetition() {
19 static get IMMOBILIZER() {
22 static get PUSHME_PULLYOU() {
31 static get LONG_LEAPER() {
34 static get SWAPPER() {
53 if (['p', 'q', 'k'].includes(b
[1])) return b
;
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
);
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
++) {
71 for (let j
= 0; j
< position
[i
].length
; j
++) {
72 switch (position
[i
].charAt(j
)) {
74 this.kingPos
["b"] = [i
, k
];
77 this.kingPos
["w"] = [i
, k
];
80 const num
= parseInt(position
[i
].charAt(j
), 10);
81 if (!isNaN(num
)) k
+= num
- 1;
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]];
99 this.board
[i
][j
] != V
.EMPTY
&&
100 this.getColor(i
, j
) == oppCol
102 if (this.getPiece(i
, j
) == V
.IMMOBILIZER
) return true;
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]];
115 this.getColor(i
, j
) == color
&&
116 this.getPiece(i
, j
) == V
.SHIELD
124 canTake([x1
, y1
], [x2
, y2
]) {
126 [V
.QUEEN
, V
.KING
].includes(this.getPiece(x1
, y1
)) &&
127 !this.isProtected([x2
, y2
]) &&
128 this.getColor(x1
, y1
) != this.getColor(x2
, y2
)
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
);
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
]);
150 // "Cannon/grasshopper pawn"
151 getPotentialPawnMoves([x
, y
]) {
153 const oppCol
= V
.GetOppCol(c
);
154 const lastRank
= (c
== 'w' ? 0 : 7);
157 [V
.IMMOBILIZER
]: true,
158 [V
.PUSHME_PULLYOU
]: true,
161 [V
.LONG_LEAPER
]: true,
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;
173 const addPromotions
= sq
=> {
174 // Optional promotion
175 Object
.keys(canResurect
).forEach(p
=> {
176 if (canResurect
[p
]) {
178 this.getBasicMove([x
, y
], [sq
[0], sq
[1]], { c: c
, p: p
}));
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
]);
192 const [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
196 this.board
[ii
][jj
] == V
.EMPTY
||
197 this.getColor(ii
, jj
) == oppCol
&& !this.isProtected([ii
, jj
])
200 moves
.push(this.getBasicMove([x
, y
], [ii
, jj
]));
201 if (ii
== lastRank
) addPromotions([ii
, jj
]);
209 getPotentialKingMoves(sq
) {
210 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
211 return this.getSlideNJumpMoves(sq
, steps
, 1);
214 // NOTE: not really captures, but let's keep the name
215 getSwapperCaptures([x
, y
]) {
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
) {
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
]);
233 p: this.getPiece(i
, j
)
238 i
== x
+ step
[0] && j
== y
+ step
[1] &&
239 !this.isProtected([i
, j
])
241 // Add mutual destruction option:
243 start: { x: x
, y: y
},
246 // TODO: is copying necessary here?
247 vanish: JSON
.parse(JSON
.stringify(m
.vanish
))
256 getPotentialSwapperMoves(sq
) {
258 super.getPotentialQueenMoves(sq
).concat(this.getSwapperCaptures(sq
))
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
);
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
) {
277 this.getColor(i
, j
) == color
||
278 this.isProtected([i
, j
])
282 let [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
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
)})
287 while (V
.OnBoard(ii
, jj
) && this.board
[ii
][jj
] == V
.EMPTY
) {
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
}
303 getPotentialLongLeaperMoves(sq
) {
305 super.getPotentialQueenMoves(sq
).concat(this.getLongLeaperCaptures(sq
))
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]];
320 this.board
[i
][j
] != V
.EMPTY
&&
321 this.getColor(i
, j
) == oppCol
323 capturingDirStart
[step
[0] + "_" + step
[1]] = {
324 p: this.getPiece(i
, j
),
325 canTake: !this.isProtected([i
, j
])
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
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]];
347 // Also test the end (advancer effect)
348 const [i
, j
] = [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
351 this.board
[i
][j
] != V
.EMPTY
&&
352 this.getColor(i
, j
) == oppCol
&&
353 !this.isProtected([i
, j
])
359 p: this.getPiece(i
, j
),
365 // Forbid "double captures"
366 return moves
.filter(m
=> m
.vanish
.length
<= 2);
369 getPotentialPushmePullyuMoves(sq
) {
370 let moves
= super.getPotentialQueenMoves(sq
);
371 return this.completeAndFilterPPcaptures(moves
);
374 getPotentialImmobilizerMoves(sq
) {
375 // Immobilizer doesn't capture
376 return super.getPotentialQueenMoves(sq
);
379 getPotentialArcherMoves([x
, y
]) {
380 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
381 let moves
= this.getSlideNJumpMoves([x
, y
], steps
);
383 const oppCol
= V
.GetOppCol(c
);
385 for (let s
of steps
) {
386 let [i
, j
] = [x
+ s
[0], y
+ s
[1]];
388 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
395 this.getColor(i
, j
) == oppCol
&&
396 !this.isProtected([i
, j
])
398 let shootOk
= (stepCounter
<= 2);
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
) {
415 this.board
[ii
][jj
] != V
.EMPTY
&&
416 this.getColor(ii
, jj
) == c
430 new PiPo({ x: i
, y: j
, c: oppCol
, p: this.getPiece(i
, j
) })
432 start: { x: x
, y: y
},
442 getPotentialShieldMoves(sq
) {
443 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
444 return this.getSlideNJumpMoves(sq
, steps
);
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");
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];
469 // King may have moved, or was swapped
470 for (let a
of move.appear
) {
472 this.kingPos
[a
.c
] = [a
.x
, a
.y
];
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
];
484 // King may have moved, or was swapped
485 for (let i
= 0; i
< move.appear
.length
; i
++) {
486 const a
= move.appear
[i
];
488 const v
= move.vanish
[i
];
489 this.kingPos
[a
.c
] = [v
.x
, v
.y
];
495 static GenRandInitFen(options
) {
496 if (options
.randomness
== 0) {
498 "wlqksaui/pppppppp/8/8/8/8/PPPPPPPP/IUASKQLW w 0"
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'];
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
];
516 pieces
["b"].join("") +
517 "/pppppppp/8/8/8/8/PPPPPPPP/" +
518 pieces
["w"].join("").toUpperCase() + " w 0"
522 static get VALUES() {
537 static get SEARCH_DEPTH() {
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':
547 initialSquare
+ (move.vanish
.length
== 1 ? "S" : "D") + finalSquare
550 let notation
= undefined;
551 const symbol
= move.appear
[0].p
.toUpperCase();
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
;
558 notation
= symbol
+ finalSquare
;
559 // Add a capture mark (not describing what is captured...):
560 if (move.vanish
.length
> 1) notation
+= "X";