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() {
52 if (['p', 'q', 'k'].includes(b
[1])) return b
;
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
);
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
++) {
70 for (let j
= 0; j
< position
[i
].length
; j
++) {
71 switch (position
[i
].charAt(j
)) {
73 this.kingPos
["b"] = [i
, k
];
76 this.kingPos
["w"] = [i
, k
];
79 const num
= parseInt(position
[i
].charAt(j
), 10);
80 if (!isNaN(num
)) k
+= num
- 1;
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]];
98 this.board
[i
][j
] != V
.EMPTY
&&
99 this.getColor(i
, j
) == oppCol
101 if (this.getPiece(i
, j
) == V
.IMMOBILIZER
) return true;
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]];
114 this.getColor(i
, j
) == color
&&
115 this.getPiece(i
, j
) == V
.SHIELD
123 canTake([x1
, y1
], [x2
, y2
]) {
124 return !this.isProtected([x2
, y2
]) && super.canTake([x1
, y1
], [x2
, y2
]);
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
);
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
]);
145 getSlideNJumpMoves([x
, y
], steps
, oneStep
) {
146 const piece
= this.getPiece(x
, y
);
148 outerLoop: for (let step
of steps
) {
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
;
157 // Only queen and king can take on occupied square:
159 [V
.KING
, V
.QUEEN
].includes(piece
) &&
161 this.canTake([x
, y
], [i
, j
])
163 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
169 // "Cannon/grasshopper pawn"
170 getPotentialPawnMoves([x
, y
]) {
172 const oppCol
= V
.GetOppCol(c
);
173 const lastRank
= (c
== 'w' ? 0 : 7);
176 [V
.IMMOBILIZER
]: true,
177 [V
.PUSHME_PULLYOU
]: true,
180 [V
.LONG_LEAPER
]: true,
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;
192 const addPromotions
= sq
=> {
193 // Optional promotion
194 Object
.keys(canResurect
).forEach(p
=> {
195 if (canResurect
[p
]) {
197 this.getBasicMove([x
, y
], [sq
[0], sq
[1]], { c: c
, p: p
}));
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
]);
211 const [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
215 this.board
[ii
][jj
] == V
.EMPTY
||
216 this.getColor(ii
, jj
) == oppCol
&& !this.isProtected([ii
, jj
])
219 moves
.push(this.getBasicMove([x
, y
], [ii
, jj
]));
220 if (ii
== lastRank
) addPromotions([ii
, jj
]);
228 getPotentialKingMoves(sq
) {
229 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
230 return this.getSlideNJumpMoves(sq
, steps
, "oneStep");
233 // NOTE: not really captures, but let's keep the name
234 getSwapperCaptures([x
, y
]) {
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
) {
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
]);
252 p: this.getPiece(i
, j
)
257 i
== x
+ step
[0] && j
== y
+ step
[1] &&
258 !this.isProtected([i
, j
])
260 // Add mutual destruction option:
262 start: { x: x
, y: y
},
265 // TODO: is copying necessary here?
266 vanish: JSON
.parse(JSON
.stringify(m
.vanish
))
275 getPotentialSwapperMoves(sq
) {
277 super.getPotentialQueenMoves(sq
).concat(this.getSwapperCaptures(sq
))
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
);
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
) {
296 this.getColor(i
, j
) == color
||
297 this.isProtected([i
, j
])
301 let [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
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
)})
306 while (V
.OnBoard(ii
, jj
) && this.board
[ii
][jj
] == V
.EMPTY
) {
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
}
322 getPotentialLongLeaperMoves(sq
) {
324 super.getPotentialQueenMoves(sq
).concat(this.getLongLeaperCaptures(sq
))
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]];
339 this.board
[i
][j
] != V
.EMPTY
&&
340 this.getColor(i
, j
) == oppCol
342 capturingDirStart
[step
[0] + "_" + step
[1]] = {
343 p: this.getPiece(i
, j
),
344 canTake: !this.isProtected([i
, j
])
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
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]];
366 // Also test the end (advancer effect)
367 const [i
, j
] = [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
370 this.board
[i
][j
] != V
.EMPTY
&&
371 this.getColor(i
, j
) == oppCol
&&
372 !this.isProtected([i
, j
])
378 p: this.getPiece(i
, j
),
384 // Forbid "double captures"
385 return moves
.filter(m
=> m
.vanish
.length
<= 2);
388 getPotentialPushmePullyuMoves(sq
) {
389 let moves
= super.getPotentialQueenMoves(sq
);
390 return this.completeAndFilterPPcaptures(moves
);
393 getPotentialImmobilizerMoves(sq
) {
394 // Immobilizer doesn't capture
395 return super.getPotentialQueenMoves(sq
);
398 getPotentialArcherMoves([x
, y
]) {
399 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
400 let moves
= this.getSlideNJumpMoves([x
, y
], steps
);
402 const oppCol
= V
.GetOppCol(c
);
404 for (let s
of steps
) {
405 let [i
, j
] = [x
+ s
[0], y
+ s
[1]];
407 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
414 this.getColor(i
, j
) == oppCol
&&
415 !this.isProtected([i
, j
])
417 let shootOk
= (stepCounter
<= 2);
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
) {
434 this.board
[ii
][jj
] != V
.EMPTY
&&
435 this.getColor(ii
, jj
) == c
449 new PiPo({ x: i
, y: j
, c: oppCol
, p: this.getPiece(i
, j
) })
451 start: { x: x
, y: y
},
461 getPotentialShieldMoves(sq
) {
462 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
463 return this.getSlideNJumpMoves(sq
, steps
);
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");
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];
488 // King may have moved, or was swapped
489 for (let a
of move.appear
) {
491 this.kingPos
[a
.c
] = [a
.x
, a
.y
];
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
];
503 // King may have moved, or was swapped
504 for (let i
= 0; i
< move.appear
.length
; i
++) {
505 const a
= move.appear
[i
];
507 const v
= move.vanish
[i
];
508 this.kingPos
[a
.c
] = [v
.x
, v
.y
];
514 static GenRandInitFen(randomness
) {
515 if (randomness
== 0) {
517 "wlqksaui/pppppppp/8/8/8/8/PPPPPPPP/IUASKQLW w 0"
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'];
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
];
535 pieces
["b"].join("") +
536 "/pppppppp/8/8/8/8/PPPPPPPP/" +
537 pieces
["w"].join("").toUpperCase() + " w 0"
541 static get VALUES() {
556 static get SEARCH_DEPTH() {
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':
566 initialSquare
+ (move.vanish
.length
== 1 ? "S" : "D") + finalSquare
569 let notation
= undefined;
570 const symbol
= move.appear
[0].p
.toUpperCase();
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
;
577 notation
= symbol
+ finalSquare
;
578 // Add a capture mark (not describing what is captured...):
579 if (move.vanish
.length
> 1) notation
+= "X";