Skip to main content

Matilda: a free human-like UCI chess engine.

Project description

Matilda

paper/website/discord

Mikhail Botvinnik, 1962

Long before brute-force search won, world champion Mikhail Botvinnik argued that a chess program should work the way a master does: don't examine every move — build a small candidate set the way a human would, and spend your calculation only there. He devoted his post-championship career (the PIONEER project; Computers, Chess and Long-Range Planning, 1970) to formalizing that selective, human-feature-focused search. Hardware went the other way, and Botvinnik's programme was shelved.

Matilda's architecture is that idea, made practical by learning the human part instead of hand-coding it. A frozen human-move model (Maia-3) proposes the up-to-16 moves a human would actually consider in the position — the candidate set Botvinnik wanted — and the pluggable search controller decides how to spend engine effort across only those moves. Stockfish and Lc0 plug in today; the SearchController API (see developer.md) exists precisely so anyone can implement their own allocation policy — deeper on the moves that look most interesting for a human, shallower elsewhere. The re-ranker then blends what the engine found back into the human distribution. It should "just work" with any controller: that composability, not any single engine, is the point.

Matilda speaks the standard UCI protocol, so it plugs into any chess GUI (CuteChess, Arena, BanksiaGUI, En Croissant) or engine runner that can talk to Stockfish. The default backend architecture is a frozen Maia-3 (23M) re-ranked by the paper's trained set-transformer, with optional engine features from any plugged-in search controller (Stockfish, Lc0, or your own). By default we use Stockfish, but we encourage developers to make the engine search build off of Maia's attention layers to focus on more human-like engine searches and not always do max depth searches.

Photo: Harry Pot / Anefo, Dutch National Archives — licensed CC BY-SA 3.0 NL.

What the re-ranker buys over raw Maia-3

Move-prediction improvement (NLL on held-out human games) of the shipped re-ranker over the frozen Maia-3 prior, by rating band — from the paper's results; 95% player-bootstrap CIs and the regeneration command for every number are in the paper repo's RESULTS.md:

Elo band vs Maia-3
1000–2500 ~0% — no low-Elo collapse; plays as human as Maia-3 does
2500–2800 +0.5%
2800–2900 +4.3%
2900–3000 +12.0%
3000–4000 +23.0%

The gains concentrate exactly where a pure human prior runs out of training examples: the top of the rating pyramid, where the engine features carry real signal. Below 2500 the re-ranker gets no engine features and leaves Maia-3's human play intact.

Install & run

pip install matilda-uci
matilda-uci --elo 1500

That's the whole install: the Maia-3 runtime comes along as the maia3-runtime dependency (the paper's pinned CSSLab/maia3 revision, republished on PyPI), and everything else is fetched on first run — the re-ranker checkpoint (base_3k.pt, ~6 MB, sha256-verified) from this repo's release assets into ~/.cache/matilda-uci, and Maia-3's 23M weights (~88 MB) from HuggingFace. A git clone needs no downloads for the checkpoints at all — they're in checkpoints/ via git-lfs (git lfs pull).

Matilda drives a search engine and expects one at startup: by default it finds stockfish on your PATH (install Stockfish; macOS brew install stockfish, Debian/Ubuntu apt install stockfish), --engine-cmd points at any other UCI engine, and --no-engine opts out (raw human prior — much weaker at high Elo). The per-move engine time follows the game's time control: roughly 2s in bullet, 15s in blitz, 30s in rapid, 60s in classical, always bounded by the remaining clock.

The UCI handshake itself is instant — models load on the first move request.

The legacy backend needs neither: matilda-uci --backend maia2 --maia-type rapid.

To use it from a chess GUI, point the "add engine" dialog at the matilda-uci executable — walkthroughs for CuteChess, cutechess-cli, and En Croissant are in docs/gui-demo.md.

CLI flags

Flag Default Meaning
--backend matilda matilda (Maia-3 + re-ranker) or maia2 (legacy)
--elo 1500 Playing strength (the Elo the engine imitates)
--opp-elo follows --elo The opponent Elo the model conditions on
--temperature 0.0 0 = always the most-human move; higher = sample
--seed fresh each start Sampling seed; pass a value to reproduce a run
--device cpu matilda: cpu/mps/cuda; maia2: cpu/gpu
--name Matilda Engine name reported to the GUI
--log-level WARNING Logging goes to stderr (stdout is the UCI channel)

Matilda-backend flags:

Flag Default Meaning
--checkpoint base_3k.pt (auto-fetched) The trained re-ranker weights: a path, or a released name
--tc-base / --tc-inc 180 / 0 Time control fed to the model (blitz default)
--no-auto-tc off Don't latch the real TC from the first go clocks
--style-checkpoint Style transformation weights (pairs with --style-vector)
--style-vector A 32-d player embedding to imitate (demos/fit_style_vector.py)
--engine-cmd auto Search controller; auto = stockfish from PATH, or e.g. lc0 --weights=...
--no-engine off Play from the raw human prior alone (no search engine)
--engine-depth / --engine-nodes / --engine-movetime 22 / 0 / 0 Controller search budget; movetime takes the min with the TC-derived cap
--threads / --cache-size 0 / 4096 torch threads; prediction-cache entries
--maia3-model 23m Maia-3 variant (non-23m warns: untrained-against)

Legacy maia2-backend flag: --maia-type (rapid/blitz) — only valid with --backend maia2.

UCI options (matilda backend)

Set from any GUI's engine-options dialog: UCI_LimitStrength, UCI_Elo (1000–3200), OpponentElo, TimeControlBase/TimeControlInc/AutoLatchTC, Temperature, Checkpoint, StyleCheckpoint/StyleVector, EngineCmd/EngineDepth/EngineNodes/EngineMovetime, Device, Threads, and CacheSize.

The model is rating-conditioned, so UCI_Elo maps directly onto playing style and strength: the engine plays like a human of that rating, including human-characteristic mistakes. Time control is a real model input — the same position is played differently at 60+0 than at 900+10 — and is latched automatically from the clock at the first go of each game. The same time control also sets the search controller's per-move budget (bullet ~2s, blitz 15s, rapid 30s, classical 60s), bounded by the remaining clock and any EngineMovetime / go movetime.

Architecture

UciEngine (protocol loop) ──drives──> MovePolicy (swap seam)
                                        ├── MatildaPolicy (default)
                                        │     └── MatildaModel
                                        │           ├── Maia3Wrapper (frozen 23M prior)
                                        │           ├── TXTC re-ranker (base_3k.pt)
                                        │           └── SearchController (optional:
                                        │               Stockfish / Lc0 / custom)
                                        └── MaiaPolicy (Maia-2, legacy)

MovePolicy is a small protocol and the deliberate extension point. The SearchController protocol is the second seam: any engine that can score up to 16 candidate moves plugs in, and without one the model degrades gracefully to the pure human prior. go infinite runs on a worker thread and holds bestmove until stop, per the UCI spec.

Real games on lichess: Matilda at full strength vs the AI levels

Played live on lichess.org against the real server-side AI (via the Board API — demos/play_on_lichess.py): Matilda at Elo 3200 with the Stockfish search controller, 5+2 clock, one game each color per level.

Opponent Games Score
lichess level 6 win as White, mate · win as Black, mate 2 – 0
lichess level 7 win as White, mate · win as Black, mate 2 – 0
lichess level 8 draw as White · draw as Black 1 – 1

5/6 against the machine spectrum, all decided over the board (mates and a 193-ply stalemate grind) — checkmating levels 6–7 outright and holding level 8, essentially full-strength Stockfish, to two draws. For local engine-vs-engine testing without a lichess account, demos/play_vs_stockfish.py reproduces the same opponents offline; sample PGNs in demos/games/.

Demos and docs

  • developer.md — Python API, custom search controllers, style personalization, embedding in your own service.
  • CONTRIBUTING.md — getting your search engine natively supported by this repo.
  • docs/gui-demo.md — playing it from CuteChess & co.
  • docs/profiling.md — inference throughput by game phase (~100 predictions/s on an Apple M3 Pro CPU), rliable bootstrap CIs.
  • demos/games/ — the local Elo × level matrix: Matilda {1500, 2000, 2800, 3200} vs lichess levels 6–8, two games per cell. The scores form the staircase an Elo-conditioned engine should produce (0–6 at 1500, 5–1 at 3200); regenerate with demos/play_vs_stockfish.py.
  • demos/style_demo.py — measure how player-style embeddings condition the policy.

Development

git lfs install && git lfs pull   # the vendored checkpoints are git-lfs objects
pip install -e ".[dev]"
python -m pytest
python -m ruff check .
PYTHONPATH=src python scripts/verify_checkpoint.py checkpoints  # model parity

The test-suite injects fake policies/models/wrappers, so it runs without the model runtimes or downloads. scripts/verify_checkpoint.py proves the ported model matches the paper implementation bitwise (requires the checkpoints and, for the parity checks, a clone of the paper repo).

License

GPL-3.0 — see LICENSE.

Citation

If you use Matilda in your research, please cite:

@article{carlson2026matilda,
  title  = {Matilda: Engine-Agnostic Search with Human Policy Guidance},
  author = {Carlson, Jason},
  year   = {2026},
  note   = {Preprint},
}

To cite this UCI engine / its documentation specifically:

@software{matilda_uci_2026,
  title   = {matilda-uci: a human-like UCI chess engine},
  author  = {Carlson, Jason and Hartfield, Justin},
  year    = {2026},
  url     = {https://github.com/GarryChess/matilda-uci},
}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

matilda_uci-0.2.0.tar.gz (98.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

matilda_uci-0.2.0-py3-none-any.whl (76.0 kB view details)

Uploaded Python 3

File details

Details for the file matilda_uci-0.2.0.tar.gz.

File metadata

  • Download URL: matilda_uci-0.2.0.tar.gz
  • Upload date:
  • Size: 98.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for matilda_uci-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4fa924377339a20d05d59b1be0536c0a55a4efe13449676257465318d6382f8b
MD5 88ad9790acebaa65fb0cf01f99a2d38c
BLAKE2b-256 d094290653d588780898b8d3337db87ee7cd6847489abcce9abe318585fd04be

See more details on using hashes here.

Provenance

The following attestation bundles were made for matilda_uci-0.2.0.tar.gz:

Publisher: release.yml on GarryChess/matilda-uci

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file matilda_uci-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: matilda_uci-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 76.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for matilda_uci-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e082b4d61a3b15e7cb440feef460d4541fd915fa3899c9ae9df969f53769c09d
MD5 8fb19dbd0b81399b016945dcd6db7940
BLAKE2b-256 f32d53653d527ffb31a245666c4dd49c1377e6cb557e2bb1f6f2a51bfdc01554

See more details on using hashes here.

Provenance

The following attestation bundles were made for matilda_uci-0.2.0-py3-none-any.whl:

Publisher: release.yml on GarryChess/matilda-uci

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page