Skip to content

Fix draw handling: illegal castling, repetition, 50-move, insufficient material#27

Merged
metaember merged 6 commits into
masterfrom
worktree-threefold
Feb 23, 2026
Merged

Fix draw handling: illegal castling, repetition, 50-move, insufficient material#27
metaember merged 6 commits into
masterfrom
worktree-threefold

Conversation

@metaember

Copy link
Copy Markdown
Owner

Summary

  • Fix illegal castling from TT: is_pseudo_legal_compact now checks that the king isn't in check and doesn't pass through attacked squares when validating cached castling moves. Previously the engine would send illegal castling moves to Lichess, causing lichess-bot to resign from winning positions.
  • Add threefold repetition detection: Thread position history (zobrist hashes) through search. Positions already seen in the game or search path score as 0 (draw), preventing the engine from shuffling pieces into threefold repetition.
  • Add fifty-move rule detection: Check halfmove_clock >= 100 at each search node, returning draw score.
  • Add insufficient material detection: Check for K vs K, K+B vs K, K+N vs K etc. at each search node, returning draw score.

Test plan

  • 3 tests verify engine avoids repeating moves from actual Lichess games (6N3BgiGZ, OFj0tHpW, LknqEJkx)
  • 2 tests verify illegal castling is rejected (out-of-check, through-check) from actual games (fOovyP9Y, aRrRTGwj)
  • Fifty-move rule test: position with halfmove_clock=100 returns score 0
  • Insufficient material test: K vs K returns score 0
  • 362 tests passing, no performance regression (±3% within noise, identical node counts)

Two bugs caused losses on Lichess:

1. TT-cached castling moves bypassed legality checks in
   is_pseudo_legal_compact — no verification that the king wasn't in
   check or passing through attacked squares. This caused the engine
   to send illegal moves, triggering lichess-bot to resign from
   winning positions.

2. Search had zero repetition awareness, so the engine would
   shuffle pieces indefinitely in won positions, drawing by
   threefold repetition.

Fixes:
- Add opponent attack map checks for castle-out-of-check and
  castle-through-check in is_pseudo_legal_compact
- Thread position_history (Vec<u64> of zobrist hashes) through
  negamax search; treat twofold repetition as score 0 (draw)
- Build position history from UCI "position ... moves" command
- Push current hash at node entry, pop on exit, so descendants
  see all ancestor positions for repetition detection

Verified against 5 actual Lichess games that exhibited these bugs.
No performance regression (±3%, identical node counts).
Both draw types were tracked in the board but never checked during
search. The engine could waste time searching K vs K endgames at
full depth, or miss draws by the fifty-move rule.

Both checks are O(1) (bitboard count_ones and integer comparison)
and sit right next to the existing repetition detection.
Runs full test suite on push/PR to master, skipping tests that
require the Cerebellum opening book file (not in repo).
The old src/bin/uci.rs used raw minimax without time control,
position history, or draw detection. All production use goes
through uci_movepicker. Retarget integration tests accordingly
and relax movetime lower bound (iterative deepening finishes
faster than fixed-depth search).
@metaember metaember merged commit 9d30f22 into master Feb 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant