Skip to content

Proposal: Integrate shakmaty for Correct Chess Logic #20

@nloding

Description

@nloding

This proposal was generated by Claude Code - Opus 4.6. Take it with a grain of salt.


Summary

The library currently implements its own board representation, SAN generation, SAN parsing, check/checkmate detection, and move tracking. Several of these are incomplete or incorrect. shakmaty — a mature, widely-used Rust chess library — provides correct, battle-tested implementations of all of them. This document evaluates the case for integration and proposes a plan.


Current Bugs and Gaps

Issue Location Description
Broken checkmate detection game.rs:657-698 Only tests if the king can escape; never tests if another piece can block or capture the attacker. Some # symbols in PGN output are wrong.
No stalemate detection Completely absent. Stalemate games produce incorrect output.
Minimal move legality pgn.rs:450-633 parse_san_move accepts moves that leave the king in check, that don't have a clear path for sliding pieces, etc.
En passant heuristic pgn.rs:626-630 Infers the captured pawn's rank rather than tracking the EP square precisely.

These are real correctness bugs, not hypothetical gaps.


What shakmaty Provides

shakmaty is licensed MIT / Apache-2.0 (compatible with this project's MIT license) and has three small transitive dependencies: arrayvec, bitflags, btoi.

Current code shakmaty replacement
Custom Square, Piece, Color enums shakmaty::{Square, Role, Color}
Custom Board / position.rs shakmaty::Chess (full legal position with legal move generation)
generate_san in game.rs:496-604 shakmaty::san::SanPlus::from_move(&pos, &mv)
parse_san_move in pgn.rs:450-633 shakmaty::san::San::from_ascii().to_move(&pos) (validates legality for free)
Broken is_checkmate in game.rs:657-698 pos.is_checkmate()
Missing stalemate detection pos.is_stalemate()

shakmaty does not know the SCID binary format — all SCID-specific code is kept.


What Stays (shakmaty Has No Equivalent)

  • All SCID binary file I/O (gfile.rs, nfile.rs, index.rs, bytebuf.rs)
  • PieceList in position.rs — the SCID binary format encodes each move as (piece_number, move_value) where piece numbers (0–15) are positional assignments set at game start. This is SCID-specific and must be maintained separately to decode/encode moves.
  • Date, namebase, ECO, header logic (date.rs, namebase.rs, etc.)
  • PGN tag parsing/generation (header sections in pgn.rs, to_pgn in game.rs)
  • NAG, comment, variation tree logic

Architecture After Integration

SCID binary bytes
      │
      ▼
 mov.rs decode fns          ← keep; output shakmaty::Move
  + PieceList tracking      ← keep; maps piece# → shakmaty::Square
      │
      ▼
shakmaty::Chess.play(mv)    ← replaces Board + make_move_on_board
      │
      ├── san::SanPlus::from_move()      ← replaces generate_san
      ├── is_checkmate() / is_stalemate() ← replaces broken logic
      └── outcome()                       ← correct game-end detection

Why Not a "Thin Wrapper" Approach?

A parallel approach (keep the custom Board, add shakmaty alongside it) would maintain two positions in sync for every move, doubling board state updates and creating a new class of divergence bugs. Full replacement is cleaner and eliminates all the broken custom logic.


Implementation Steps

Step 1 — Add dependency

Cargo.toml (workspace):

[workspace.dependencies]
shakmaty = "0.27"   # verify latest stable on crates.io

crates/scidtopgn/Cargo.toml:

shakmaty = { workspace = true }

Step 2 — Replace common.rs types

  • Remove custom Square, Piece (role), Color enums
  • Re-export or use shakmaty::{Square, Role, Color} throughout
  • Keep GameResult; map to shakmaty::Outcome where appropriate

Step 3 — Slim down position.rs

  • Remove Board struct entirely (replaced by shakmaty::Chess)
  • Keep PieceList — required for SCID binary decode
    • Update Square field to shakmaty::Square
  • Remove is_attacked, is_in_check, is_path_clear, is_checkmate — shakmaty handles these

Step 4 — Update mov.rs decode functions

  • Change return types to produce shakmaty::Move
  • Decode functions take PieceList + the current shakmaty::Chess position
  • King castling → shakmaty::Move::Castle { king, rook }
  • Pawn promotions → shakmaty::Move::Normal { promotion: Some(role), … }
  • En passant → shakmaty::Move::EnPassant { from, to }

Step 5 — Update game.rs

  • Hold a shakmaty::Chess instead of custom Board
  • make_move_on_board → call pos.play(&mv) (returns Result<Chess, …>)
  • Remove generate_san; replace all call sites with:
    let san = shakmaty::san::SanPlus::from_move(&pos, &mv);
  • Remove broken is_checkmate; use pos.is_checkmate() / pos.is_stalemate()
  • Standard starting position: Chess::default(); FEN positions: Chess::from_setup(setup)

Step 6 — Update pgn.rs SAN parsing

  • Remove parse_san_move; replace with:
    let san = shakmaty::san::San::from_ascii(token)?;
    let mv = san.to_move(&pos)?;
  • Move legality validation is now automatic (returns Err on illegal moves)
  • En passant detection is automatic (shakmaty tracks the EP square)

Step 7 — FEN tag support (bonus)

For PGN games with a [FEN "…"] tag:

let fen: shakmaty::fen::Fen = token.parse()?;
let pos = Chess::from_setup(fen.into_position(CastlingMode::Standard)?)?;

Step 8 — Update tests

  • test_move_encoding.rs — update expected Move types
  • test_position.rs — update to use Chess instead of Board
  • test_pgn.rs / test_game.rs — verify SAN output still matches expected PGN strings

Critical Files

File Change
Cargo.toml (workspace) Add shakmaty to [workspace.dependencies]
crates/scidtopgn/Cargo.toml Add shakmaty dep
crates/scidtopgn/src/common.rs Remove Square, Piece, Color; use shakmaty's
crates/scidtopgn/src/position.rs Remove Board; keep PieceList with shakmaty squares
crates/scidtopgn/src/mov.rs Return shakmaty::Move from decode functions
crates/scidtopgn/src/game.rs Use Chess, remove generate_san, remove broken is_checkmate
crates/scidtopgn/src/pgn.rs Replace parse_san_move with San::from_ascii().to_move()
crates/scidtopgn/tests/*.rs Update type expectations

Verification

# Build — no compile errors
cargo build --workspace

# All tests pass (including encoding round-trips)
cargo test --workspace

# Spot-check SAN output on a known game (e.g., Immortal Game, Opera Game)
# Output should include correct '#' at the end and correct disambiguation throughout

# Specifically verify checkmate detection: before this change, mates delivered
# by a piece where the king has escape squares but another piece could interpose
# would have been misdetected.

Version Note

Verify the latest stable version on crates.io/crates/shakmaty before pinning. As of early 2026 it is 0.27.x. The san module, Chess, Move, and Position trait have been stable API for several releases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions