Chess engine with C++ bindings and MCTS
AlphaZero's architecture applied to chess, built mainly to learn how to put a C++ core underneath Python numerics: move generation and board state in C++, search and network in Python, pybind11 across the boundary.
What I set out to check
Less about chess than about the boundary between two languages. Python is where the network and the search logic want to live; move generation is a tight loop that Python is a poor choice for. The question was whether a C++ core behind pybind11 gives back enough throughput to justify the build complexity, and how much friction the boundary adds in day-to-day work.
It was a proof of concept, and worth saying so plainly. The goal was familiarity with exposing C++ to Python — a pattern common enough in numerical work to be worth learning deliberately rather than encountering by accident — with chess as a demanding enough problem to make the exercise real.
What I built
A chess engine in C++17 holding board state and move generation, compiled into a Python module through `bindings.cpp` and a `setup.py`, with a Makefile wrapping the build. On the Python side, a wrapper class presenting the C++ board in the shape the search expects, a Monte Carlo tree search, and a policy-value convolutional network in PyTorch. A Pygame interface on top, so the result is playable rather than merely runnable.
What went well is the interop, which was the actual goal. The engine has a `time_test` target for measuring raw C++ performance independently of Python, and a separate smoke test for the bindings — being able to time the two sides separately is what makes it possible to say where time is going at all, rather than guessing.
Where it diverged
The important gap, and the one to be direct about: there is no self-play training loop. AlphaZero is defined by that loop — self-play generates games, games train the network, the better network plays better games. This project has the pieces around the loop, the tree search and the policy-value network, but not the loop itself. So it is AlphaZero's architecture without AlphaZero's training, and the network was never improved by its own play. Any claim about playing strength would be unearned.
The C++ speedup is also narrower than its presence suggests. Move generation and board state are in C++; the search is still Python, and search is where the time actually goes. Crossing the pybind11 boundary once per node evaluation caps how much the fast core can help, which is close to the opposite of the intended arrangement.
Two rules are unfinished. Promotions are not wired through the policy head, and there is no repetition or loop detection, so a game can fail to terminate.
The search has a defect I only found by reading it back carefully. PUCT weights an unvisited action by its prior, and the code has a branch meant to do exactly that — but the branch assigns into `U[action]` and the line below immediately overwrites it unconditionally. So the intended handling is dead code. The consequence is not cosmetic: on the first visit to any node the total visit count is zero, the exploration term carries a `sqrt(0)`, every Q starts at zero, and so every action scores identically. The first move explored at every node is whichever the dictionary happens to yield, and the policy network's priors have no influence at all until a second visit. For a search that is supposed to be guided by the network, that is close to the whole point going missing.
The measurement that would settle the original question was never recorded either. `time_test` exists, but no timing is written down anywhere in the repository, so the engine's throughput against a pure-Python baseline — the entire point of the exercise — remains unquantified.
What I would do next
Run `time_test` and write the number down, with a Python baseline beside it. It is the smallest piece of work here and the only one that answers the question the project was built to ask.
After that the ordering is forced. Moving the tree search into C++ comes before the self-play loop, because self-play is only affordable at a search throughput the current Python implementation cannot reach. And the two unfinished rules block it in turn: a game that does not terminate cannot produce usable training data, so promotion handling and repetition detection have to land before self-play generates anything worth learning from.
Absent that work, the honest framing is the one this entry takes — an interop exercise that answered its own question and stopped there, which is a perfectly good reason to stop.
- Stack
- C++pybind11PyTorchPygame
- Code
- github.com/AndrewGilbert2027/ChessAlphaZero ↗