1 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
2 import { ArrayFun
} from "@/utils/array";
3 import { shuffle
} from "@/utils/alea";
5 export class RococoRules
extends ChessRules
{
6 static get HasFlags() {
10 static get HasEnpassant() {
15 return ChessRules
.PIECES
.concat([V
.IMMOBILIZER
]);
29 //'m' for Immobilizer (I is too similar to 1)
31 return b
; //usual piece
35 // The only "choice" case is between a swap and a mutual destruction:
36 // show empty square in case of mutual destruction.
37 if (m
.appear
.length
== 0) return "Rococo/empty";
38 return m
.appear
[0].c
+ m
.appear
[0].p
;
41 setOtherVariables(fen
) {
42 // No castling, but checks, so keep track of kings
43 this.kingPos
= { w: [-1, -1], b: [-1, -1] };
44 const fenParts
= fen
.split(" ");
45 const position
= fenParts
[0].split("/");
46 for (let i
= 0; i
< position
.length
; i
++) {
48 for (let j
= 0; j
< position
[i
].length
; j
++) {
49 switch (position
[i
].charAt(j
)) {
51 this.kingPos
["b"] = [i
, k
];
54 this.kingPos
["w"] = [i
, k
];
57 const num
= parseInt(position
[i
].charAt(j
), 10);
58 if (!isNaN(num
)) k
+= num
- 1;
64 // Local stack of swaps:
66 const smove
= V
.ParseFen(fen
).smove
;
67 if (smove
== "-") this.smoves
.push(null);
70 start: ChessRules
.SquareToCoords(smove
.substr(0, 2)),
71 end: ChessRules
.SquareToCoords(smove
.substr(2))
76 static ParseFen(fen
) {
78 ChessRules
.ParseFen(fen
),
79 { smove: fen
.split(" ")[3] }
83 static IsGoodFen(fen
) {
84 if (!ChessRules
.IsGoodFen(fen
)) return false;
85 const fenParts
= fen
.split(" ");
86 if (fenParts
.length
!= 4) return false;
87 if (fenParts
[3] != "-" && !fenParts
[3].match(/^([a-h][1-8]){2}$/))
93 if (move.appear
.length
== 2)
94 return { start: move.start
, end: move.end
};
99 // Add the "capturing edge"
100 return { x: 10, y: 10 };
103 static get IMMOBILIZER() {
106 // Although other pieces keep their names here for coding simplicity,
107 // keep in mind that:
108 // - a "rook" is a swapper, exchanging positions and "capturing" by
109 // mutual destruction only.
110 // - a "knight" is a long-leaper, capturing as in draughts
111 // - a "bishop" is a chameleon, capturing as its prey
112 // - a "queen" is a withdrawer+advancer, capturing by moving away from
113 // pieces or advancing in front of them.
115 // Is piece on square (x,y) immobilized?
116 isImmobilized([x
, y
]) {
117 const piece
= this.getPiece(x
, y
);
118 const oppCol
= V
.GetOppCol(this.getColor(x
, y
));
119 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
120 for (let step
of adjacentSteps
) {
121 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
124 this.board
[i
][j
] != V
.EMPTY
&&
125 this.getColor(i
, j
) == oppCol
127 const oppPiece
= this.getPiece(i
, j
);
128 if (oppPiece
== V
.IMMOBILIZER
) return [i
, j
];
129 // Only immobilizers are immobilized by chameleons:
130 if (oppPiece
== V
.BISHOP
&& piece
== V
.IMMOBILIZER
) return [i
, j
];
136 static OnEdge(x
, y
) {
137 return x
== 0 || y
== 0 || x
== V
.size
.x
- 1 || y
== V
.size
.y
- 1;
140 getPotentialMovesFrom([x
, y
]) {
141 // Pre-check: is thing on this square immobilized?
142 const imSq
= this.isImmobilized([x
, y
]);
143 const piece
= this.getPiece(x
, y
);
144 if (!!imSq
&& piece
!= V
.KING
) {
145 // Only option is suicide, if I'm not a king:
148 start: { x: x
, y: y
},
149 end: { x: imSq
[0], y: imSq
[1] },
155 c: this.getColor(x
, y
),
156 p: this.getPiece(x
, y
)
165 moves
= this.getPotentialImmobilizerMoves([x
, y
]);
168 moves
= super.getPotentialMovesFrom([x
, y
]);
170 // Post-processing: prune redundant non-minimal capturing moves,
171 // and non-capturing moves ending on the edge:
173 // Useful precomputation
174 m
.dist
= Math
.abs(m
.end
.x
- m
.start
.x
) + Math
.abs(m
.end
.y
- m
.start
.y
);
176 return moves
.filter(m
=> {
177 if (!V
.OnEdge(m
.end
.x
, m
.end
.y
)) return true;
179 if (m
.vanish
.length
== 1) return false;
180 // Capture or swap: only captures get filtered
181 if (m
.appear
.length
== 2) return true;
182 // Can we find other moves with a shorter path to achieve the same
183 // capture? Apply to queens and knights.
188 mv
.vanish
.length
== m
.vanish
.length
&&
189 mv
.vanish
.every(v
=> {
190 return m
.vanish
.some(vv
=> {
192 vv
.x
== v
.x
&& vv
.y
== v
.y
&& vv
.c
== v
.c
&& vv
.p
== v
.p
203 // NOTE: not removing "dist" field; shouldn't matter much...
206 getSlideNJumpMoves([x
, y
], steps
, oneStep
) {
207 const piece
= this.getPiece(x
, y
);
209 outerLoop: for (let step
of steps
) {
212 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
213 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
214 if (oneStep
!== undefined) continue outerLoop
;
218 // Only king can take on occupied square:
219 if (piece
== V
.KING
&& V
.OnBoard(i
, j
) && this.canTake([x
, y
], [i
, j
]))
220 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
225 // "Cannon/grasshopper pawn"
226 getPotentialPawnMoves([x
, y
]) {
227 const oppCol
= V
.GetOppCol(this.turn
);
229 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
230 adjacentSteps
.forEach(step
=> {
231 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
232 if (V
.OnBoard(i
, j
)) {
233 if (this.board
[i
][j
] == V
.EMPTY
)
234 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
237 const [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
238 if (V
.OnBoard(ii
, jj
) && this.getColor(ii
, jj
) == oppCol
)
239 moves
.push(this.getBasicMove([x
, y
], [ii
, jj
]));
246 // NOTE: not really captures, but let's keep the name
247 getRookCaptures([x
, y
], byChameleon
) {
249 const oppCol
= V
.GetOppCol(this.turn
);
250 // Simple: if something is visible, we can swap
251 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]).forEach(step
=> {
252 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
253 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
257 if (V
.OnBoard(i
, j
) && this.getColor(i
, j
) == oppCol
) {
258 const oppPiece
= this.getPiece(i
, j
);
259 if (!byChameleon
|| oppPiece
== V
.ROOK
) {
260 let m
= this.getBasicMove([x
, y
], [i
, j
]);
266 p: this.getPiece(i
, j
)
270 if (i
== x
+ step
[0] && j
== y
+ step
[1]) {
271 // Add mutual destruction option:
273 start: { x: x
, y: y
},
276 // TODO: is copying necessary here?
277 vanish: JSON
.parse(JSON
.stringify(m
.vanish
))
288 getPotentialRookMoves(sq
) {
289 return super.getPotentialQueenMoves(sq
).concat(this.getRookCaptures(sq
));
292 getKnightCaptures(startSquare
, byChameleon
) {
293 // Look in every direction for captures
294 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
295 const color
= this.turn
;
296 const oppCol
= V
.GetOppCol(color
);
298 const [x
, y
] = [startSquare
[0], startSquare
[1]];
299 const piece
= this.getPiece(x
, y
); //might be a chameleon!
300 outerLoop: for (let step
of steps
) {
301 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
302 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
308 this.getColor(i
, j
) == color
||
309 (!!byChameleon
&& this.getPiece(i
, j
) != V
.KNIGHT
)
313 // last(thing), cur(thing) : stop if "cur" is our color,
314 // or beyond board limits, or if "last" isn't empty and cur neither.
315 // Otherwise, if cur is empty then add move until cur square;
316 // if cur is occupied then stop if !!byChameleon and the square not
317 // occupied by a leaper.
319 let cur
= [i
+ step
[0], j
+ step
[1]];
320 let vanished
= [new PiPo({ x: x
, y: y
, c: color
, p: piece
})];
321 while (V
.OnBoard(cur
[0], cur
[1])) {
322 if (this.board
[last
[0]][last
[1]] != V
.EMPTY
) {
323 const oppPiece
= this.getPiece(last
[0], last
[1]);
324 if (!!byChameleon
&& oppPiece
!= V
.KNIGHT
) continue outerLoop
;
327 new PiPo({ x: last
[0], y: last
[1], c: oppCol
, p: oppPiece
})
330 if (this.board
[cur
[0]][cur
[1]] != V
.EMPTY
) {
332 this.getColor(cur
[0], cur
[1]) == color
||
333 this.board
[last
[0]][last
[1]] != V
.EMPTY
335 //TODO: redundant test
341 appear: [new PiPo({ x: cur
[0], y: cur
[1], c: color
, p: piece
})],
342 vanish: JSON
.parse(JSON
.stringify(vanished
)), //TODO: required?
343 start: { x: x
, y: y
},
344 end: { x: cur
[0], y: cur
[1] }
348 last
= [last
[0] + step
[0], last
[1] + step
[1]];
349 cur
= [cur
[0] + step
[0], cur
[1] + step
[1]];
356 getPotentialKnightMoves(sq
) {
357 return super.getPotentialQueenMoves(sq
).concat(this.getKnightCaptures(sq
));
361 getPotentialBishopMoves([x
, y
]) {
362 const oppCol
= V
.GetOppCol(this.turn
);
364 .getPotentialQueenMoves([x
, y
])
365 .concat(this.getKnightCaptures([x
, y
], "asChameleon"))
366 .concat(this.getRookCaptures([x
, y
], "asChameleon"));
367 // No "king capture" because king cannot remain under check
368 this.addQueenCaptures(moves
, "asChameleon");
369 // Also add pawn captures (as a pawn):
370 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
371 adjacentSteps
.forEach(step
=> {
372 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
373 const [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
374 // Try to leap over (i,j):
377 this.board
[i
][j
] != V
.EMPTY
&&
378 this.board
[ii
][jj
] != V
.EMPTY
&&
379 this.getColor(ii
, jj
) == oppCol
&&
380 this.getPiece(ii
, jj
) == V
.PAWN
382 moves
.push(this.getBasicMove([x
, y
], [ii
, jj
]));
385 // Post-processing: merge similar moves, concatenating vanish arrays
386 let mergedMoves
= {};
388 const key
= m
.end
.x
+ V
.size
.x
* m
.end
.y
;
389 if (!mergedMoves
[key
]) mergedMoves
[key
] = m
;
391 for (let i
= 1; i
< m
.vanish
.length
; i
++)
392 mergedMoves
[key
].vanish
.push(m
.vanish
[i
]);
395 return Object
.values(mergedMoves
);
398 addQueenCaptures(moves
, byChameleon
) {
399 if (moves
.length
== 0) return;
400 const [x
, y
] = [moves
[0].start
.x
, moves
[0].start
.y
];
401 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
402 let capturingDirStart
= {};
403 const oppCol
= V
.GetOppCol(this.turn
);
404 // Useful precomputation:
405 adjacentSteps
.forEach(step
=> {
406 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
409 this.board
[i
][j
] != V
.EMPTY
&&
410 this.getColor(i
, j
) == oppCol
&&
411 (!byChameleon
|| this.getPiece(i
, j
) == V
.QUEEN
)
413 capturingDirStart
[step
[0] + "_" + step
[1]] = this.getPiece(i
, j
);
418 m
.end
.x
!= x
? (m
.end
.x
- x
) / Math
.abs(m
.end
.x
- x
) : 0,
419 m
.end
.y
!= y
? (m
.end
.y
- y
) / Math
.abs(m
.end
.y
- y
) : 0
421 // TODO: this test should be done only once per direction
422 const capture
= capturingDirStart
[(-step
[0]) + "_" + (-step
[1])];
424 const [i
, j
] = [x
- step
[0], y
- step
[1]];
434 // Also test the end (advancer effect)
435 const [i
, j
] = [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
438 this.board
[i
][j
] != V
.EMPTY
&&
439 this.getColor(i
, j
) == oppCol
&&
440 (!byChameleon
|| this.getPiece(i
, j
) == V
.QUEEN
)
446 p: this.getPiece(i
, j
),
454 // Withdrawer + advancer: "pushme-pullyu"
455 getPotentialQueenMoves(sq
) {
456 let moves
= super.getPotentialQueenMoves(sq
);
457 this.addQueenCaptures(moves
);
461 getPotentialImmobilizerMoves(sq
) {
462 // Immobilizer doesn't capture
463 return super.getPotentialQueenMoves(sq
);
466 // Does m2 un-do m1 ? (to disallow undoing swaps)
467 oppositeMoves(m1
, m2
) {
470 m2
.appear
.length
== 2 &&
471 m1
.start
.x
== m2
.start
.x
&&
472 m1
.end
.x
== m2
.end
.x
&&
473 m1
.start
.y
== m2
.start
.y
&&
479 if (moves
.length
== 0) return [];
480 const color
= this.turn
;
484 const L
= this.smoves
.length
; //at least 1: init from FEN
485 return !this.oppositeMoves(this.smoves
[L
- 1], m
);
491 // isAttacked() is OK because the immobilizer doesn't take
493 isAttackedByPawn([x
, y
], color
) {
494 // Attacked if an enemy pawn stands just behind an immediate obstacle:
495 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
496 for (let step
of adjacentSteps
) {
497 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
498 const [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
501 this.board
[i
][j
] != V
.EMPTY
&&
502 this.board
[ii
][jj
] != V
.EMPTY
&&
503 this.getColor(ii
, jj
) == color
&&
504 this.getPiece(ii
, jj
) == V
.PAWN
&&
505 !this.isImmobilized([ii
, jj
])
513 isAttackedByRook([x
, y
], color
) {
514 // The only way a swapper can take is by mutual destruction when the
515 // enemy piece stands just next:
516 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
517 for (let step
of adjacentSteps
) {
518 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
521 this.board
[i
][j
] != V
.EMPTY
&&
522 this.getColor(i
, j
) == color
&&
523 this.getPiece(i
, j
) == V
.ROOK
&&
524 !this.isImmobilized([i
, j
])
532 isAttackedByKnight([x
, y
], color
) {
533 // Square (x,y) must be on same line as a knight,
534 // and there must be empty square(s) behind.
535 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
536 outerLoop: for (let step
of steps
) {
537 const [i0
, j0
] = [x
+ step
[0], y
+ step
[1]];
538 if (V
.OnBoard(i0
, j0
) && this.board
[i0
][j0
] == V
.EMPTY
) {
539 // Try in opposite direction:
540 let [i
, j
] = [x
- step
[0], y
- step
[1]];
541 while (V
.OnBoard(i
, j
)) {
542 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
546 if (V
.OnBoard(i
, j
)) {
547 if (this.getColor(i
, j
) == color
) {
549 this.getPiece(i
, j
) == V
.KNIGHT
&&
550 !this.isImmobilized([i
, j
])
556 // could be captured *if there was an empty space*
557 if (this.board
[i
+ step
[0]][j
+ step
[1]] != V
.EMPTY
)
568 isAttackedByBishop([x
, y
], color
) {
569 // We cheat a little here: since this function is used exclusively for
570 // the king, it's enough to check the immediate surrounding of the square.
571 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
572 for (let step
of adjacentSteps
) {
573 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
576 this.board
[i
][j
] != V
.EMPTY
&&
577 this.getColor(i
, j
) == color
&&
578 this.getPiece(i
, j
) == V
.BISHOP
&&
579 !this.isImmobilized([i
, j
])
587 isAttackedByQueen([x
, y
], color
) {
588 // Is there a queen in view?
589 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
590 for (let step
of adjacentSteps
) {
591 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
592 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
598 this.getColor(i
, j
) == color
&&
599 this.getPiece(i
, j
) == V
.QUEEN
601 // Two cases: the queen is at 2 steps at least, or just close
602 // but maybe with enough space behind to withdraw.
603 let attacked
= false;
604 if (i
== x
+ step
[0] && j
== y
+ step
[1]) {
605 const [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
606 if (V
.OnBoard(ii
, jj
) && this.board
[ii
][jj
] == V
.EMPTY
)
609 else attacked
= true;
610 if (attacked
&& !this.isImmobilized([i
, j
])) return true;
616 isAttackedByKing([x
, y
], color
) {
617 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
618 for (let step
of steps
) {
619 let rx
= x
+ step
[0],
623 this.getPiece(rx
, ry
) === V
.KING
&&
624 this.getColor(rx
, ry
) == color
&&
625 !this.isImmobilized([rx
, ry
])
633 static GenRandInitFen(randomness
) {
634 if (randomness
== 0) {
636 "91/1rqnbknqm1/1pppppppp1/91/91/91/91/1PPPPPPPP1/1MQNBKNQR1/91 w 0 -"
640 let pieces
= { w: new Array(8), b: new Array(8) };
641 // Shuffle pieces on first and last rank
642 for (let c
of ["w", "b"]) {
643 if (c
== 'b' && randomness
== 1) {
644 pieces
['b'] = pieces
['w'];
648 // Get random squares for every piece, totally freely
649 let positions
= shuffle(ArrayFun
.range(8));
650 const composition
= ['r', 'm', 'n', 'n', 'q', 'q', 'b', 'k'];
651 for (let i
= 0; i
< 8; i
++) pieces
[c
][positions
[i
]] = composition
[i
];
654 "91/1" + pieces
["b"].join("") +
655 "1/1pppppppp1/91/91/91/91/1PPPPPPPP1/1" +
656 pieces
["w"].join("").toUpperCase() + "1/91 w 0 -"
661 const L
= this.smoves
.length
;
665 : ChessRules
.CoordsToSquare(this.smoves
[L
- 1].start
) +
666 ChessRules
.CoordsToSquare(this.smoves
[L
- 1].end
)
671 return super.getFen() + " " + this.getSmoveFen();
675 return super.getFenForRepeat() + "_" + this.getSmoveFen();
679 super.postPlay(move);
680 this.smoves
.push(this.getSmove(move));
684 super.postUndo(move);
688 static get VALUES() {
700 static get SEARCH_DEPTH() {
705 const initialSquare
= V
.CoordsToSquare(move.start
);
706 const finalSquare
= V
.CoordsToSquare(move.end
);
707 if (move.appear
.length
== 0) {
708 // Suicide 'S' or mutual destruction 'D':
710 initialSquare
+ (move.vanish
.length
== 1 ? "S" : "D" + finalSquare
)
713 let notation
= undefined;
714 if (move.appear
[0].p
== V
.PAWN
) {
715 // Pawn: generally ambiguous short notation, so we use full description
716 notation
= "P" + initialSquare
+ finalSquare
;
717 } else if (move.appear
[0].p
== V
.KING
)
718 notation
= "K" + (move.vanish
.length
> 1 ? "x" : "") + finalSquare
;
719 else notation
= move.appear
[0].p
.toUpperCase() + finalSquare
;
720 // Add a capture mark (not describing what is captured...):
721 if (move.vanish
.length
> 1 && move.appear
[0].p
!= V
.KING
) notation
+= "X";