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.
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
game.rs:657-698#symbols in PGN output are wrong.pgn.rs:450-633parse_san_moveaccepts moves that leave the king in check, that don't have a clear path for sliding pieces, etc.pgn.rs:626-630These 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.Square,Piece,Colorenumsshakmaty::{Square, Role, Color}Board/position.rsshakmaty::Chess(full legal position with legal move generation)generate_saningame.rs:496-604shakmaty::san::SanPlus::from_move(&pos, &mv)parse_san_moveinpgn.rs:450-633shakmaty::san::San::from_ascii().to_move(&pos)(validates legality for free)is_checkmateingame.rs:657-698pos.is_checkmate()pos.is_stalemate()shakmaty does not know the SCID binary format — all SCID-specific code is kept.
What Stays (shakmaty Has No Equivalent)
gfile.rs,nfile.rs,index.rs,bytebuf.rs)PieceListinposition.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.rs,namebase.rs, etc.)pgn.rs,to_pgningame.rs)Architecture After Integration
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):crates/scidtopgn/Cargo.toml:Step 2 — Replace
common.rstypesSquare,Piece(role),Colorenumsshakmaty::{Square, Role, Color}throughoutGameResult; map toshakmaty::Outcomewhere appropriateStep 3 — Slim down
position.rsBoardstruct entirely (replaced byshakmaty::Chess)PieceList— required for SCID binary decodeSquarefield toshakmaty::Squareis_attacked,is_in_check,is_path_clear,is_checkmate— shakmaty handles theseStep 4 — Update
mov.rsdecode functionsshakmaty::MovePieceList+ the currentshakmaty::Chesspositionshakmaty::Move::Castle { king, rook }shakmaty::Move::Normal { promotion: Some(role), … }shakmaty::Move::EnPassant { from, to }Step 5 — Update
game.rsshakmaty::Chessinstead of customBoardmake_move_on_board→ callpos.play(&mv)(returnsResult<Chess, …>)generate_san; replace all call sites with:is_checkmate; usepos.is_checkmate()/pos.is_stalemate()Chess::default(); FEN positions:Chess::from_setup(setup)Step 6 — Update
pgn.rsSAN parsingparse_san_move; replace with:Erron illegal moves)Step 7 — FEN tag support (bonus)
For PGN games with a
[FEN "…"]tag:Step 8 — Update tests
test_move_encoding.rs— update expectedMovetypestest_position.rs— update to useChessinstead ofBoardtest_pgn.rs/test_game.rs— verify SAN output still matches expected PGN stringsCritical Files
Cargo.toml(workspace)shakmatyto[workspace.dependencies]crates/scidtopgn/Cargo.tomlshakmatydepcrates/scidtopgn/src/common.rsSquare,Piece,Color; use shakmaty'scrates/scidtopgn/src/position.rsBoard; keepPieceListwith shakmaty squarescrates/scidtopgn/src/mov.rsshakmaty::Movefrom decode functionscrates/scidtopgn/src/game.rsChess, removegenerate_san, remove brokenis_checkmatecrates/scidtopgn/src/pgn.rsparse_san_movewithSan::from_ascii().to_move()crates/scidtopgn/tests/*.rsVerification
Version Note
Verify the latest stable version on crates.io/crates/shakmaty before pinning. As of early 2026 it is
0.27.x. Thesanmodule,Chess,Move, andPositiontrait have been stable API for several releases.