Battleship Monte-Carlo Solver
An AI that plays Battleship by sampling millions of possible board configurations and firing at the most statistically likely cell, every single turn.
# The problem
There are 30 billion+ possible ship arrangements on a 10×10 grid (source). Computing exact per-cell probabilities across all of them, conditioned on every hit and miss, is NP-hard — far too expensive for real-time play.
So we sample: draw thousands of random legal configurations, tally which cells appear most often, and fire at the highest-frequency target. This frequentist approach assumes all valid boards are equally likely — a simplifying assumption, since real players have placement biases — but uniform sampling is the most principled starting point without a bias model, and in practice it plays surprisingly well.
# How it works
-
Encode legal placements as bitboards
Every position each ship could occupy is stored as an integer, one bit per cell. A 10x10 board fits in 128 bits.
-
Sample, count, fire, prune
Each turn: draw thousands of random boards consistent with all hits and misses, tally cell frequencies, and fire at the highest-probability unseen cell. After each shot, impossible placements are pruned and the search space shrinks.
-
Converge
As ships sink the solver's confidence increases and shots become near-deterministic.
# The tricks
Trick 01
Bitboard representation
All data (placements, hits, misses, boards) is encoded as Python integers. Overlap tests, unions, and checks compile to C-level bigint operations, eliminating set and dictionary overhead entirely.
overlap = (ship_a & ship_b) != 0
Trick 02
Carry-chain bit counting
Instead of iterating through set bits one by one, a carry-chain counter tracks per-tile counts across all positions simultaneously using bitwise AND/XOR on full-width integers. This replaces thousands of method calls with a handful of bigint ops.
counter[j] = (counter[j] ^ board) & carry
Trick 03
Incremental board filtering
Previously sampled boards are tested with a single bitwise check each. Only the deficit is regenerated, avoiding redundant sampling when the game state changes minimally.
valid = [b for b in pool if (b & new_miss) == 0]
Trick 04
Constraint propagation
After every shot, placements contradicting the known state are permanently discarded. Misses remove all placements touching the missed cell; hits remove those that don't cover confirmed cells. The rejection-sampling search space shrinks with every turn.
poss = [p for p in poss if not (p & miss_bit)]
Trick 05
Placement ordering
Ships are sorted by constraint count (fewest legal positions first) before board generation. The most constrained ship is placed first, maximizing early rejection and cutting wasted sampling attempts.
ships.sort(key=lambda s: len(s.placements))
Trick 06
Cython & xorshift64* PRNG
The rejection-sampling inner loop is compiled to C via Cython using 128-bit bitboards (two uint64_t fields) and a xorshift64* PRNG. The GIL is released for the entire sampling phase, yielding roughly 8-11x speedup over pure Python.
with nogil: # rejection loop in typed C