All projects ~/laksh/projects/peg-solitaire
Graduate-level ML class project, Jan – May 2026

ML Library for Peg Solitaire

My graduate-level machine learning class said to write an ML library without using anyone else's, then point it at a problem. I picked peg solitaire.

View on GitHub ↗
RoleSolo, scope set by the class
Timeline5 months
Size~4,700 lines, MIT licensed
Stack
PythonNumPyJupyterGitHub Actions
420,000boards solved
less search, from symmetry
14algorithms written from scratch

Why I built it

Peg solitaire is the board with pegs in every hole but one, where you jump a peg over its neighbour and take the neighbour off. You want to finish with as few pegs as possible, and one peg left is a perfect game.

I picked it because the question is short and the answer is expensive to compute: for a given board, what is the best outcome anybody could reach?

What it does

Two packages, about 4,700 lines of Python, public under MIT.

rice_ml is the library the class asked for: ten supervised techniques (linear and logistic regression, gradient descent, k-nearest neighbours, perceptron, multilayer perceptron, decision trees, regression trees, random forests, ensembles) and four unsupervised ones (k-means, DBSCAN, PCA, graph community detection). I derived backpropagation by hand before writing it.

brainvita is the game half: board representation, visualization, and a solver that computes the fewest and the most pegs a board can finish with, plus the best and worst opening moves you could pick.

How it works

How the dataset was built

  1. Generate random boards.

    Random connected board shapes, in three size tiers.

    Python
  2. Solve every board.

    Try every legal jump, and skip boards already seen, counting rotations and mirrors as the same board.

    Pythonmultiprocessing
  3. Describe each board with 19 numbers.

    Each board is labelled with the solver's answer.

    NumPy
  4. Train models on the result.

    Using rice_ml, the 14 algorithms I wrote from scratch.

    NumPyJupyter
  5. Test everything on every push.

    The unit tests run and every notebook is executed.

    GitHub Actions

About 420,000 boards solved, and the whole set regenerates identically from the same seeds.

The solver walks every legal sequence of jumps depth first, memoizing on board state so a position you can reach two different ways only gets explored once.

The code is in Jupyter notebooks, so the code and its output are on the same page. Running every notebook in CI has caught commits that broke other notebooks.

The hard parts

The search space is the entire project

A position has about a dozen legal jumps, and each of those leads to another dozen. Run that across hundreds of thousands of boards and it does not finish.

Symmetry cuts it down. Rotating or mirroring a board gives the same puzzle, so folding all eight orientations into one canonical form cuts the work by a factor of eight.

The canonical form has to collapse every true duplicate and nothing else. If it over-collapses, the solver returns a wrong answer and no error, which is much harder to find.

Large boards do not terminate

Past a certain size the exhaustive search stops terminating in any useful amount of time, and those are exactly the boards worth asking about.

Those fall back to a state-budgeted search using greedy rollouts. It explores until the budget runs out, and every rollout runs to a terminal position, so the number it reports is an outcome a real player could reach.

Finding sign errors in hand-derived gradients

With a sign error the loss still falls, more slowly, and converges somewhere worse. Nothing in the output flags it, which matters most when you are deriving backpropagation by hand.

So all fourteen are checked against scikit-learn on identical inputs and have to agree to within 1e-6, backed by 187 unit tests running in CI on every push. 1e-6 is loose enough for floating point noise and tight enough to catch a sign error.

Back to all projects Next: OwlUCanEat →