Building Spore Field: A Territory Control Game as an npm Package
Why I Built Spore Field
The other games in this series are real-time. I wanted one that was turn-based, giving players time to think strategically. Spore Field draws from territory control games like Go and Hex, but with a biological twist: you grow spore colonies that spread organically, and you can sacrifice your own cells to power up your next move.
The sacrifice mechanic is what makes the game interesting. You control territory by spreading, but you gain power by sacrificing. Every sacrifice weakens your current position but strengthens your next placement. Finding the right balance between growth and sacrifice is the core strategic challenge.
Hex Grid System
The game board is a hex-like grid where each cell has up to six neighbors. Hex grids create more interesting adjacency patterns than square grids: there are no diagonal ambiguities, and surrounded cells have more escape routes. The grid is stored as a flat array with axial coordinate helpers for neighbor lookups.
function getNeighbors(coord: Coord, gridSize: number): Coord[] {
const directions = [
{ q: 1, r: 0 }, { q: -1, r: 0 },
{ q: 0, r: 1 }, { q: 0, r: -1 },
{ q: 1, r: -1 }, { q: -1, r: 1 },
];
return directions
.map(d => ({ q: coord.q + d.q, r: coord.r + d.r }))
.filter(c => isInBounds(c, gridSize));
}Rendering converts axial coordinates to pixel positions using the standard hex-to-pixel transformation. Pointy-top hexagons are drawn with Canvas 2D paths, colored by owner (player, AI, or empty) with nutrient level indicated by opacity.
Spreading Mechanics
After both the player and AI place a spore, all existing spores spread to adjacent empty cells. Higher-nutrient spores spread first and win any conflicts. This creates a natural priority system: cells placed with sacrifice-boosted nutrients expand faster and claim contested territory.
The spreading algorithm processes cells in nutrient-descending order. Each cell attempts to spread to all empty neighbors. If two cells try to spread to the same empty cell in the same turn, the higher-nutrient cell wins. Ties go to the cell that has been alive longer.
AI Opponent
The AI evaluates each empty cell as a potential placement using a scoring function. Factors include: proximity to AI territory (to reinforce existing colonies), proximity to player territory (to contest borders), number of empty neighbors (expansion potential), and distance from board edges (central cells are more valuable).
The AI also decides whether to sacrifice. It compares its current territory percentage against the player's. If it is trailing, it sacrifices more aggressively to make power plays. If it is ahead, it plays conservatively, placing without sacrifice to maintain its lead. This creates an AI that feels adaptive without using complex tree search.
Win Conditions
Two win conditions keep games from stalling. The primary condition: when the board is full, whoever controls more territory wins. The domination condition: if either side reaches 70% control at any point, they win immediately. Domination prevents hopeless games from dragging on and rewards aggressive, decisive play.
The 70% threshold was chosen after testing. At 60%, domination triggers too early and feels unfair. At 80%, it almost never triggers. 70% hits the sweet spot where domination wins feel earned, happening only when one side has made a significant strategic error.
Testing the Game Engine
43 tests cover grid generation, neighbor lookups, spreading logic, sacrifice mechanics, AI decision-making, and win condition detection. The most important tests verify spreading fairness: that nutrient priority works correctly, that simultaneous spreading resolves deterministically, and that the AI does not have any information advantage over the player.
Testing the AI required a different approach than testing deterministic systems. Instead of checking exact moves, the tests verify properties: the AI always makes a valid move, it never places on occupied cells, its sacrifice decisions correlate with its territory deficit, and it completes in under 10ms per decision.
Lessons Learned
Turn-based games need clear feedback about what happened and why. After each turn, a brief spreading animation shows which cells expanded and which conflicts were resolved. Without this animation, players could not understand why the board changed the way it did.
The sacrifice mechanic needed visual communication. When you sacrifice cells, they visibly shrink and dissolve while your placement indicator grows brighter. This makes the trade-off tangible: you see your territory shrinking and your next move powering up simultaneously.
Get more posts like this
I write about system design, backend engineering, and building npm packages from scratch. Follow along on Substack for new posts.
Subscribe on Substack →