From 4c53eb7bc0c06ec620afd0f92546b2f556e3f824 Mon Sep 17 00:00:00 2001 From: grepfuldead <299977258+grepfuldead@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:48:48 +0300 Subject: [PATCH] Fix is_legal accepting non-canonical castling encodings is_legal derives the castling kind from to.file() == File::G alone and never checks that to is the actual landing square, so with castling rights and a clear path it accepts (king_from, arbitrary to, Castling) for 63 bogus encodings per position. Playing one hits the unreachable in get_castling_rook. Search is unaffected (UCI and the TT path only see canonical encodings), but islegalperft feeds every accepted encoding to make_move and currently panics on any position with castling rights: position fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1 islegalperft 1 -> panicked: entered unreachable code Require to == kind.landing_square(), making is_legal an exact mirror of generate_all_moves and restoring the diagnostic (kiwipete islegalperft 1 now completes: 48). Matches the convention in Stockfish, where special-move validity is generated-set membership. No functional change. Bench: 2526370 --- src/board.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/board.rs b/src/board.rs index 311942f98..7791f579e 100644 --- a/src/board.rs +++ b/src/board.rs @@ -363,7 +363,8 @@ impl Board { if mv.is_castling() { let kind = CastlingKind::KINDS[stm][(to.file() == File::G) as usize]; - return self.castling().is_allowed(kind) + return to == kind.landing_square() + && self.castling().is_allowed(kind) && (self.castling_path[kind] & self.occupancies()).is_empty() && (self.castling_threat[kind] & self.all_threats()).is_empty() && !self.pinned(stm).contains(self.castling_rooks[kind]);