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
]) {
125 return !this.isProtected([x2
, y2
]) && super.canTake([x1
, y1
], [x2
, y2
]);
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
);
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
]);
146 getSlideNJumpMoves([x
, y
], steps
, oneStep
) {
147 const piece
= this.getPiece(x
, y
);
149 outerLoop: for (let step
of steps
) {
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
;
158 // Only queen and king can take on occupied square:
160 [V
.KING
, V
.QUEEN
].includes(piece
) &&
162 this.canTake([x
, y
], [i
, j
])
164 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
170 // "Cannon/grasshopper pawn"
171 getPotentialPawnMoves([x
, y
]) {
173 const oppCol
= V
.GetOppCol(c
);
174 const lastRank
= (c
== 'w' ? 0 : 7);
177 [V
.IMMOBILIZER
]: true,
178 [V
.PUSHME_PULLYOU
]: true,
181 [V
.LONG_LEAPER
]: true,
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;
193 const addPromotions
= sq
=> {
194 // Optional promotion
195 Object
.keys(canResurect
).forEach(p
=> {
196 if (canResurect
[p
]) {
198 this.getBasicMove([x
, y
], [sq
[0], sq
[1]], { c: c
, p: p
}));
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
]);
212 const [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
216 this.board
[ii
][jj
] == V
.EMPTY
||
217 this.getColor(ii
, jj
) == oppCol
&& !this.isProtected([ii
, jj
])
220 moves
.push(this.getBasicMove([x
, y
], [ii
, jj
]));
221 if (ii
== lastRank
) addPromotions([ii
, jj
]);
229 getPotentialKingMoves(sq
) {
230 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
231 return this.getSlideNJumpMoves(sq
, steps
, "oneStep");
234 // NOTE: not really captures, but let's keep the name
235 getSwapperCaptures([x
, y
]) {
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
) {
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
]);
253 p: this.getPiece(i
, j
)
258 i
== x
+ step
[0] && j
== y
+ step
[1] &&
259 !this.isProtected([i
, j
])
261 // Add mutual destruction option:
263 start: { x: x
, y: y
},
266 // TODO: is copying necessary here?
267 vanish: JSON
.parse(JSON
.stringify(m
.vanish
))
276 getPotentialSwapperMoves(sq
) {
278 super.getPotentialQueenMoves(sq
).concat(this.getSwapperCaptures(sq
))
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
);
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
) {
297 this.getColor(i
, j
) == color
||
298 this.isProtected([i
, j
])
302 let [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
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
)})
307 while (V
.OnBoard(ii
, jj
) && this.board
[ii
][jj
] == V
.EMPTY
) {
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
}
323 getPotentialLongLeaperMoves(sq
) {
325 super.getPotentialQueenMoves(sq
).concat(this.getLongLeaperCaptures(sq
))
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]];
340 this.board
[i
][j
] != V
.EMPTY
&&
341 this.getColor(i
, j
) == oppCol
343 capturingDirStart
[step
[0] + "_" + step
[1]] = {
344 p: this.getPiece(i
, j
),
345 canTake: !this.isProtected([i
, j
])
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
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]];
367 // Also test the end (advancer effect)
368 const [i
, j
] = [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
371 this.board
[i
][j
] != V
.EMPTY
&&
372 this.getColor(i
, j
) == oppCol
&&
373 !this.isProtected([i
, j
])
379 p: this.getPiece(i
, j
),
385 // Forbid "double captures"
386 return moves
.filter(m
=> m
.vanish
.length
<= 2);
389 getPotentialPushmePullyuMoves(sq
) {
390 let moves
= super.getPotentialQueenMoves(sq
);
391 return this.completeAndFilterPPcaptures(moves
);
394 getPotentialImmobilizerMoves(sq
) {
395 // Immobilizer doesn't capture
396 return super.getPotentialQueenMoves(sq
);
399 getPotentialArcherMoves([x
, y
]) {
400 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
401 let moves
= this.getSlideNJumpMoves([x
, y
], steps
);
403 const oppCol
= V
.GetOppCol(c
);
405 for (let s
of steps
) {
406 let [i
, j
] = [x
+ s
[0], y
+ s
[1]];
408 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
415 this.getColor(i
, j
) == oppCol
&&
416 !this.isProtected([i
, j
])
418 let shootOk
= (stepCounter
<= 2);
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
) {
435 this.board
[ii
][jj
] != V
.EMPTY
&&
436 this.getColor(ii
, jj
) == c
450 new PiPo({ x: i
, y: j
, c: oppCol
, p: this.getPiece(i
, j
) })
452 start: { x: x
, y: y
},
462 getPotentialShieldMoves(sq
) {
463 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
464 return this.getSlideNJumpMoves(sq
, steps
);
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");
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];
489 // King may have moved, or was swapped
490 for (let a
of move.appear
) {
492 this.kingPos
[a
.c
] = [a
.x
, a
.y
];
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
];
504 // King may have moved, or was swapped
505 for (let i
= 0; i
< move.appear
.length
; i
++) {
506 const a
= move.appear
[i
];
508 const v
= move.vanish
[i
];
509 this.kingPos
[a
.c
] = [v
.x
, v
.y
];
515 static GenRandInitFen(randomness
) {
516 if (randomness
== 0) {
518 "wlqksaui/pppppppp/8/8/8/8/PPPPPPPP/IUASKQLW w 0"
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'];
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
];
536 pieces
["b"].join("") +
537 "/pppppppp/8/8/8/8/PPPPPPPP/" +
538 pieces
["w"].join("").toUpperCase() + " w 0"
542 static get VALUES() {
557 static get SEARCH_DEPTH() {
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':
567 initialSquare
+ (move.vanish
.length
== 1 ? "S" : "D") + finalSquare
570 let notation
= undefined;
571 const symbol
= move.appear
[0].p
.toUpperCase();
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
;
578 notation
= symbol
+ finalSquare
;
579 // Add a capture mark (not describing what is captured...):
580 if (move.vanish
.length
> 1) notation
+= "X";