Deterministic chess truth for grounding language models (board core + Stockfish + Maia)
Project description
lucena-engine
Deterministic chess truth for grounding language models.
lucena-engine turns a chess position into grounded facts — a piece list, a material count, a
strategic read, tactical motifs, move evaluations, and a plain-language briefing — computed by
Stockfish and a Rust board core, never guessed. It exists so an LLM can interpret chess instead of
calculating it: you hand the model the facts, and it explains, teaches, or narrates — without
hallucinating whose move it is, miscounting material, inventing a piece on f6, or imagining a fork that
isn't there.
If you're building anything where an LLM talks about chess positions — a coach, an annotator, a commentary bot, a puzzle explainer — this is the layer that keeps it honest.
Why
LLMs are fluent about chess and quietly wrong. Ask one "what's going on here?" with a FEN and it will confidently misread the side to move, hang a piece in analysis, or narrate a tactic from a different game. The fix isn't a bigger prompt — it's grounding: compute everything the model could get wrong, hand it over as facts, and instruct it to reason only from those.
lucena-engine is the grounding source. It emits two shapes of truth:
- Structured facts — typed records with squares, scores, and provenance (for your own logic, board arrows, or tool-calling).
- A natural-language fact sheet — a short list of deterministic English statements you can drop straight into a prompt.
Every output is reproducible given the engine limit. No randomness in the facts.
Install
pip install lucena-engine # library + `lucena-engine` CLI
brew install stockfish # required engine (or apt-get install stockfish)
lucena-engine doctor # check the runtime is wired up
lucena-engine analyze "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
Optional extras: pip install 'lucena-engine[grpc]' for the server; human-move prediction (Maia) is
opt-in via lucena-engine setup-maia — see Human-move prediction.
From source (development): needs a Rust toolchain.
pip install maturin && maturin develop --release # builds the Rust board core
export LUCENA_STOCKFISH=$(which stockfish) # defaults to `stockfish` on PATH
Quickstart — a grounded fact sheet in five lines
from lucena_engine import Engine
from lucena_engine.board import Board
from lucena_engine.analysis import build_analysis
engine = Engine() # spawns Stockfish
board = Board("r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/2N2N2/PPPP1PPP/R1B1K2R b KQkq - 0 4")
for line in build_analysis(board, engine, movetime_ms=500):
print(line)
The position is roughly equal.
Material is equal.
White's pieces are more active than Black's.
The center is contested.
Tactics: the knight on f6 can capture on e4.
That list is the grounding. Put it in a prompt:
facts = build_analysis(board, engine, movetime_ms=500)
prompt = (
"You are a chess coach. Ground every claim ONLY in these engine facts — "
"never invent a piece, square, line, or number:\n" + "\n".join(facts) +
f"\n\nStudent asked: what should I be thinking about here?"
)
# → hand `prompt` to your LLM. It interprets; it does not calculate.
What's in the fact sheet
build_analysis composes, in order:
- Evaluation — translated to words ("roughly equal", "White is clearly better"); never raw centipawns or win%.
- Material — a standing ("Black is up a pawn").
- Strategic read — king safety, piece activity, pawns, and center, leads first.
- Tactics — the salient motifs on the board, joined into one line.
Each piece is also available on its own if you'd rather build your own briefing:
from lucena_engine import build_fact_sheet
from lucena_engine.analysis import analyze_positional
from lucena_engine.reads import material, piece_list, eval_block, pv_san
analysis = engine.analyse(board.fen, multipv=2, movetime_ms=500)
material(board) # {"white": 10, "black": 10, "net": 0, "standing": "Material is equal"}
eval_block(analysis.best.score) # {"cp": 12, "win_pct": 52.1, ...}
pv_san(board.fen, analysis.best.pv) # ["Nxe4", "d5", "Bd3", ...] (UCI → SAN)
analyze_positional(board) # the five strategic terms, each with a "standing"
Structured facts (typed, with squares)
For tool-calling, board arrows, or your own routing, take the facts as records instead of prose:
from lucena_engine import build_fact_sheet
for f in build_fact_sheet(board, engine, movetime_ms=500):
print(f.kind, f.squares, f.salience, "—", f.text)
# hanging ['e4'] 0.9 — the pawn on e4 is undefended
# fork ['c7'] 0.8 — a knight on c7 would fork the king and rook
Fact = kind, squares, text, provenance, salience (0..1). The individual detectors are
public too:
from lucena_engine import (
detect_hanging, detect_fork, detect_pin, detect_defender_removed,
detect_combination, detect_null_move_threat,
)
The board core
A fast Rust board (cozy-chess + a custom SAN encoder and SEE) with a small Python surface — legal moves, SAN ⇄ UCI, static exchange evaluation, attackers/defenders, apply:
from lucena_engine.board import Board
b = Board(fen)
b.side_to_move # "white" | "black"
b.legal_moves() # ["e2e4", "g1f3", ...] (UCI)
b.san("f6e4") # "Nxe4"
b.uci("Nxe4") # "f6e4"
b.see("f6e4") # static exchange eval (centipawns) — is this capture safe?
b.attackers("e4", "white") # squares of white pieces hitting e4
b.apply("f6e4") # → a new Board after the move (immutable)
Nothing here calls the engine — it's pure, instant board logic.
Move evaluation, hints, and the rest
# Best move + a worded verdict for a candidate
from lucena_engine import classify, Glyph
before = engine.analyse(fen, multipv=2, movetime_ms=500)
after = engine.analyse(board.apply("f6e4").fen, movetime_ms=500)
glyph, _ = classify(before.best.score, after.best.score.negated()) # OK / dubious / mistake / blunder …
# A ladder of hints, vague → specific, none of which is the answer
from lucena_engine import derive_hints
# Brilliancy / sacrifice detection
from lucena_engine import is_brilliant, is_material_sacrifice
# Whole-game annotation
from lucena_engine import parse_pgn, run_pass
game = parse_pgn(pgn_text) # Game(plies=[Ply(san, uci, fen), ...])
Human-move prediction (Maia)
Optional. Predicts what a human of a given rating would actually play (not the engine's best) — useful for "this is a common trap at your level" style grounding. Runs as its own subprocess.
from lucena_engine.maia import MaiaEngine # requires LUCENA_MAIA to point at a maia UCI wrapper
maia = MaiaEngine()
maia.top_human_moves(fen, rating=1500, n=5) # [{"uci": "f6e4", "rank": 1, "policy": 0.42}, ...]
Over the wire (gRPC)
Prefer a language-agnostic or networked boundary? The same truth is exposed as a gRPC service — handy if your app isn't Python, or if you want the engine (and its GPL dependencies) in a separate process.
python -m lucena_engine.server.serve # listens on :50051 (LUCENA_ENGINE_PORT)
Two services (proto in proto/lucena/engine/v1/engine.proto, SAN on the wire):
- Truth —
Analyze(FEN → fact sheet),Evaluate,Hints,LegalMoves,Apply,ValidateFen,DetectFens,ParsePgn,GetInfo. - Behaviour —
TopHumanMoves,PoisonedLine(Maia-backed; optional).
Design principles
- Calculate vs. interpret. The engine and board calculate the truth; your LLM only interprets it. Anything the model could get wrong is grounded (handed over) or guarded (made structurally impossible) — never left to the model.
- Deterministic. Same position + same engine limit → same facts. Production uses
movetime_ms; tests usenodeswiththreads=1for bit-reproducibility. - SAN on the wire. UCI is engine-internal; everything you see and send is human-readable SAN.
- No eval leakage into prose. The natural-language layer says "you're winning", never "‑347 cp".
- Copyleft hygiene. GPL engines (Stockfish, Maia) are invoked only as subprocesses over UCI — arm's-length, never linked. A CI gate keeps GPL libraries out of the shipped code.
Requirements
- Python ≥ 3.9, Rust toolchain (for the board core via maturin).
- Stockfish (18 recommended; the NNUE eval parser is version-aware). Point
LUCENA_STOCKFISHat it. - Optional: Maia (
maia3) in its own environment, viaLUCENA_MAIA.
Contributing & license
Licensed under AGPL-3.0-or-later (see LICENSE, NOTICE, and THIRD-PARTY-NOTICES.md). If you run
a modified version as a network service, AGPL §13 requires you to offer users its source.
Contributions are welcome under the CLA in CONTRIBUTING.md. Note the copyleft-hygiene rule: the
shipped library never imports python-chess (GPL) — the board core replaces it, and a test enforces it.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lucena_engine-0.1.1.tar.gz.
File metadata
- Download URL: lucena_engine-0.1.1.tar.gz
- Upload date:
- Size: 163.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9f35074b50bd3150a729be607769bde9d8ae805bb94ddf245562b2e59ecad08
|
|
| MD5 |
ec2849c0497ac9aad65e93a6230066d9
|
|
| BLAKE2b-256 |
d5d09e79311188a2cb98c4a8a96dc99e37722c1ad73927ba443bdf4c0a49c7c7
|
Provenance
The following attestation bundles were made for lucena_engine-0.1.1.tar.gz:
Publisher:
release.yml on distressedrook/lucena-engine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lucena_engine-0.1.1.tar.gz -
Subject digest:
c9f35074b50bd3150a729be607769bde9d8ae805bb94ddf245562b2e59ecad08 - Sigstore transparency entry: 2172686793
- Sigstore integration time:
-
Permalink:
distressedrook/lucena-engine@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/distressedrook
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Trigger Event:
push
-
Statement type:
File details
Details for the file lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 467.6 kB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f94d9ba5854b37273b131101a0cbbefa54ca277c4f7ad874d6b41811ca54b46c
|
|
| MD5 |
bc2c69f66f3349c74b040a682e0c3771
|
|
| BLAKE2b-256 |
9a306b3fb0ee9f593ff016f1447b1302738ae8848c7f93975e9618648b4e836f
|
Provenance
The following attestation bundles were made for lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on distressedrook/lucena-engine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
f94d9ba5854b37273b131101a0cbbefa54ca277c4f7ad874d6b41811ca54b46c - Sigstore transparency entry: 2172686804
- Sigstore integration time:
-
Permalink:
distressedrook/lucena-engine@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/distressedrook
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Trigger Event:
push
-
Statement type:
File details
Details for the file lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 467.2 kB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
978dc0c2e91bcda88f1870d452e9b11a5fd700a85e4a310fbd6f951f1ad05099
|
|
| MD5 |
f8bb42dab28a5f438887fee417e4fdf6
|
|
| BLAKE2b-256 |
c05cb6724bd00716bafaf5e51acb0ce224de477ccee875d004325be062fa6762
|
Provenance
The following attestation bundles were made for lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on distressedrook/lucena-engine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lucena_engine-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
978dc0c2e91bcda88f1870d452e9b11a5fd700a85e4a310fbd6f951f1ad05099 - Sigstore transparency entry: 2172686810
- Sigstore integration time:
-
Permalink:
distressedrook/lucena-engine@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/distressedrook
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Trigger Event:
push
-
Statement type:
File details
Details for the file lucena_engine-0.1.1-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: lucena_engine-0.1.1-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 459.4 kB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f8c02fe7a5b0d2f62eb62af6bac683bafa3cdf141369f6fe3e8a776bf16c7c6
|
|
| MD5 |
685c5660a8d4118bf637c89cf388e03e
|
|
| BLAKE2b-256 |
290ddf80c732b7348a5937b8dd90d6227c411ba7345cc79883aeaf2243046f48
|
Provenance
The following attestation bundles were made for lucena_engine-0.1.1-cp39-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on distressedrook/lucena-engine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lucena_engine-0.1.1-cp39-abi3-macosx_11_0_arm64.whl -
Subject digest:
3f8c02fe7a5b0d2f62eb62af6bac683bafa3cdf141369f6fe3e8a776bf16c7c6 - Sigstore transparency entry: 2172686800
- Sigstore integration time:
-
Permalink:
distressedrook/lucena-engine@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/distressedrook
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d3c858cfc159c3e4385eb7146cd29fd5b1ca31aa -
Trigger Event:
push
-
Statement type: