fix(engine): hash the en passant file only when a legal capture exists #547

Merged
root merged 1 commit from fix/432-threefold-en-passant into main 2026-10-15 23:47:38 +00:00
root commented 2026-10-15 22:42:28 +00:00 (Migrated from gitlab.core-project.space)

Summary

FIDE counts two Positions as the same unless an En passant capture is actually legal. The key now hashes the en passant file only then.

 SearchBoard.makeMove(move)
   apply edits, flip Turn, set ep target
-  key ^= epFile(epBefore) ^ epFile(epAfter)
+  capturableEp = ep if some pawn of the side to move can take there
+                 without leaving its king in check, else undefined
+  key ^= epFile(capturableEpBefore) ^ epFile(capturableEpAfter)
 Game.apply / Game constructor / statusOf default
-  zobristKey(position)           # hashed position.ep as is
+  positionKey(position)          # SearchBoard.fromPosition(position).key()

zobristKey now takes a KeyedPosition with capturableEp instead of a Position, so a raw ep target can no longer be hashed by mistake. The FEN ep field is unchanged.

Evidence

  • Before:
    Game.fromUci("e2e4 e7e5 g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1 c6b8").status() → ongoing (test fails)
    pinned-pawn line from 4k3/2p5/8/KP5r/8/8/8/6N1 b - - 0 1, c7c5 then two knight/king cycles → ongoing (test fails)
  • After: both → { kind: "draw", reason: "threefold" }; the cycle without a double push stays a draw; e2e4 g8f6 e4e5 d7d5 + two cycles (exd6 legal) stays ongoing. SearchBoard tests cover the key for a capturable, unreachable and pinned ep target, and incremental vs fresh keys after a capturable double push. All five gate commands green locally.

Merge Danger

Door: two-way

Zobrist keys are computed in memory from a Game's history; none are persisted.

Blast Radius: engine

Status of in-progress Games with a double push in their history may now report a threefold draw where it did not before, which is the fix. Each double push now runs a small legality check for en passant captures.

Proposed follow-up (not edited here): GLOSSARY.md's Threefold repetition row says "en passant target"; it could read "en passant capture right" to match FIDE.

Closes #432

## Summary FIDE counts two **Positions** as the same unless an **En passant** capture is actually legal. The key now hashes the en passant file only then. ```diff SearchBoard.makeMove(move) apply edits, flip Turn, set ep target - key ^= epFile(epBefore) ^ epFile(epAfter) + capturableEp = ep if some pawn of the side to move can take there + without leaving its king in check, else undefined + key ^= epFile(capturableEpBefore) ^ epFile(capturableEpAfter) ``` ```diff Game.apply / Game constructor / statusOf default - zobristKey(position) # hashed position.ep as is + positionKey(position) # SearchBoard.fromPosition(position).key() ``` `zobristKey` now takes a `KeyedPosition` with `capturableEp` instead of a `Position`, so a raw ep target can no longer be hashed by mistake. The FEN ep field is unchanged. ## Evidence - **Before:** `Game.fromUci("e2e4 e7e5 g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1 c6b8").status()` → `ongoing` (test fails) pinned-pawn line from `4k3/2p5/8/KP5r/8/8/8/6N1 b - - 0 1`, `c7c5` then two knight/king cycles → `ongoing` (test fails) - **After:** both → `{ kind: "draw", reason: "threefold" }`; the cycle without a double push stays a draw; `e2e4 g8f6 e4e5 d7d5` + two cycles (exd6 legal) stays `ongoing`. SearchBoard tests cover the key for a capturable, unreachable and pinned ep target, and incremental vs fresh keys after a capturable double push. All five gate commands green locally. ## Merge Danger **Door:** two-way Zobrist keys are computed in memory from a Game's history; none are persisted. **Blast Radius:** engine Status of in-progress Games with a double push in their history may now report a threefold draw where it did not before, which is the fix. Each double push now runs a small legality check for en passant captures. Proposed follow-up (not edited here): `GLOSSARY.md`'s **Threefold repetition** row says "en passant target"; it could read "en passant capture right" to match FIDE. Closes #432
project_1_bot_a8dd9262857d850eef8dca5206e61de5 commented 2026-10-15 23:06:32 +00:00 (Migrated from gitlab.core-project.space)

Screenshots

Before is production, after is this merge request's review app at 10f7ed9.

Desktop

Page Before After
Home
Local game
Room

Mobile

Page Before After
Home
Local game
Room
<!-- chess-screenshots --> ### Screenshots Before is production, after is this merge request's review app at 10f7ed9. #### Desktop | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/before/home-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/after/home-desktop.png" width="400"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/before/local-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/after/local-desktop.png" width="400"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/before/room-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/after/room-desktop.png" width="400"> | #### Mobile | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/before/home-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/after/home-mobile.png" width="200"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/before/local-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/after/local-mobile.png" width="200"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/before/room-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1750/artifacts/raw/review-screenshots/after/room-mobile.png" width="200"> |
root commented 2026-10-15 23:37:13 +00:00 (Migrated from gitlab.core-project.space)

added 20 commits

  • 8d34a312...bcc826d3 - 19 commits from branch main
  • 10f7ed9c - fix(engine): hash the en passant file only when a legal capture exists

Compare with previous version

added 20 commits <ul><li>8d34a312...bcc826d3 - 19 commits from branch <code>main</code></li><li>10f7ed9c - fix(engine): hash the en passant file only when a legal capture exists</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/47/diffs?diff_id=301&start_sha=8d34a3120b4111bcdcab3adc6f7221b98c0906b8)
root (Migrated from gitlab.core-project.space) merged commit into main 2026-10-15 23:47:38 +00:00
Sign in to join this conversation.
No description provided.