Inspiration

The idea started as a simple question: what separates a snake that survives from one that wins?

Most beginner Battlesnake implementations are reactive — avoid walls, avoid bodies, chase food. That produces a bot that lasts a while but inevitably loses to anything with even a hint of positional awareness. I wanted to build something that thought ahead about space, territory, and opponent behavior rather than just reacting to immediate collisions.

The name Thunga came first. The snake needed a personality — aggressive, deliberate, a hunter rather than a survivor. That framing shaped every design decision that followed: Thunga shouldn't just avoid dying, it should close traps, read enemies, and control the board.


How It Was Built

Thunga is a Python-based Battlesnake bot built on a multi-layered evaluation architecture. Every turn, a single move is selected by scoring all legal candidates across six dimensions and choosing the highest.

Layer 1 — Hard Constraints

Before any scoring happens, get_safe_moves() filters moves into two buckets:

  • Safe: in-bounds, not a body collision, not a head-to-head zone against an equal or larger snake
  • Risky: only dangerous due to head-to-head exposure (used as fallback if all moves are risky)

Layer 2 — Heuristic Scoring

Each candidate move is passed to evaluate_move(), which scores across six dimensions:

Dimension Range Purpose
Flood Fill $[0, 100]$ Space reachable from next head position
Food $[0, 80]$ Urgency-scaled, A*-distance, competition-aware
Center Control $[0, 20]$ Positional dominance
Wall Penalty $[-50, 0]$ Discourages hugging perimeter
Threat $[-120, +25]$ Graduated by distance to enemy heads
Voronoi $[0, 30]$ Territory owned via multi-source Dijkstra race

Food urgency is not a flat weight — it scales with health:

$$f_{\text{urgency}} = 0.60 + 0.40 \cdot \left(\frac{100 - h}{100}\right)$$

where $h$ is current health. At full health, $f_{\text{urgency}} = 0.60$, keeping food relevant even when healthy. At $h = 0$, $f_{\text{urgency}} = 1.00$, making food the dominant signal.

Flood fill uses a hard cutoff for truly trapped positions, then a graduated trap severity penalty for near-trapped positions:

$$\text{score}_{\text{trap}} = -500 - 500 \cdot \left(1 - \frac{\text{space}}{L}\right), \quad \text{space} < L$$

where $L$ is the snake's current length. This ensures more space is always strictly better even in losing positions, producing stable gradient descent toward the least-bad move.

Layer 3 — Tactical Overlays

On top of the base score, three independent overlays fire when their conditions are met:

Corridor Trap Detection — counts passable exits from next_head after the move. A one-way corridor gets a penalty that scales inversely with available space:

$$p_{\text{corridor}} = 10 + 40 \cdot (1 - \rho_{\text{space}})$$

where $\rho_{\text{space}} = \text{space} / (W \times H)$ is the space ratio. On a cramped board this reaches $-50$; on an open board it stays mild (~$-10$) since corridors are survivable with room to maneuver.

Double-Cut Detection — counts how many of next_head's 4 neighbours are covered by enemy head zones or walls. Two enemies on opposite sides create a combined enclosure that neither triggers individually:

$$p_{\text{double-cut}} = \begin{cases} -160 & \text{covered} = 4 \ -80 & \text{covered} = 3 \ 0 & \text{otherwise} \end{cases}$$

Walls count as covered neighbours — a snake pressed into a corner only needs two enemies to be fully boxed.

Box-Out (Wall-Pin) — a $O(1)$ geometric detector that identifies when an enemy is hugging the perimeter moving parallel to it, and Thunga's candidate move places it in the inner lane. When triggered (and Thunga is strictly longer), a $+150$ bonus fires, overriding defensive hesitancy and committing to the kill.

Layer 4 — Opponent Profiler

Every enemy snake accumulates a behavioral profile across four counters updated each turn:

  • vacuum_count — moved toward nearest food
  • wall_count — head on perimeter
  • aggro_count — moved toward Thunga while ignoring food (and strictly larger)
  • center_count — head inside central $5 \times 5$ region

After 15 turns, ratios are computed and the snake is classified into one of five archetypes: vacuum, camper, aggressor, area_control, or erratic. Each archetype unlocks a specific tactical overlay within interaction radius $r = 6$ manhattan tiles — for example, tripling threat penalties against aggressors, or cancelling wall penalties when shadowing a camper.


What I Learned

Heuristic weights are fragile. The hardest part wasn't writing any single algorithm — it was tuning the relative weights between dimensions. Food scoring at $[0, 80]$ and threat penalties at $[-120, 0]$ weren't derived mathematically; they emerged from watching replays and noticing failure modes. A formal approach would use something like a linear weight vector $\mathbf{w} \in \mathbb{R}^d$ trained via self-play, but hand-tuning taught me far more about why each dimension matters.

Flood fill lies. A region with 40 reachable squares sounds safe, but if the entry point is a single corridor tile, those 40 squares can become 0 reachable squares in one turn. This is what motivated the corridor trap check — flood fill counts volume, not accessibility.

Single-turn evaluation has a ceiling. Every heuristic in Thunga reasons about the current board state. A lookahead search would render most of the manual trap logic unnecessary, because the position would simply evaluate as losing N turns from now without needing special cases. This is the clearest direction for future improvement.

The profiler is only as good as its classification threshold. Setting _PROFILE_MIN_TURNS = 15 means Thunga plays blind for the opening. In a 4-snake standard game, the opening turns are often the most food-dense and positionally critical. A Bayesian update model that assigns soft archetype probabilities from turn 1 would be strictly better.


Challenges

The >= vs > bug was the most impactful single bug in the codebase. The food competition check used snake['length'] >= my_length to zero out food scores when a "bigger" enemy was contesting the same food. Because all snakes start at the same length, this fired on every food tile in the early game — Thunga effectively stopped chasing food after turn 2 without any visible error or crash. It took a full replay analysis to trace the passive board behavior back to a one-character condition.

Balancing aggression against self-preservation was an ongoing tension. The box-out overlay adds $+150$ to inner-lane moves, which is large enough to override threat penalties. Early versions would commit to a wall-pin even when Thunga was already cramped, trading its own survival for an offensive opportunity that never closed. The space_ratio <= 0.30 safety gate resolved this, but finding the right threshold required testing failure cases manually.

Corner behavior was consistently the weakest part of the bot. Corners reduce the passable exit count to 2 before any enemies are involved, which means the corridor and double-cut checks both become more sensitive near corners. Thunga's wall penalty partially addresses this, but the interaction between wall penalties, corridor penalties, and threat penalties in corner positions remains the most complex region of the scoring space.


Built with Python · Flask · Deployed on Render

Built With

Share this project:

Updates