A small tactics game I wrote to learn two things: hex grid pathfinding, and how to keep a game loop honest.
Hex grids are not that hard
The only genuinely fiddly part is converting between the two coordinate systems — axial coordinates for storage, cube coordinates for distance maths — and then remembering that each cube coordinate sums to zero.
// Cube distance: the only hex distance formula you actually need
function cubeDistance(a: Cube, b: Cube): number {
return (
Math.abs(a.q - b.q) +
Math.abs(a.r - b.r) +
Math.abs(a.s - b.s)
) / 2
}
// Neighbours are six fixed offsets — no lookup table needed
const HEX_DIRECTIONS: Cube[] = [
{ q: +1, r: 0, s: -1 },
{ q: +1, r: -1, s: 0 },
{ q: 0, r: -1, s: +1 },
{ q: -1, r: 0, s: +1 },
{ q: -1, r: +1, s: 0 },
{ q: 0, r: +1, s: -1 },
]divmod is the other function worth memorising, because flat-to-axial conversion appears
in every hex library you'll ever write:
function divMod(x: number, y: number) {
const q = Math.floor(x / y)
return [q, x - q * y] as const
}Pathfinding is A* with cubeDistance as the heuristic. Hex grids are unweighted enough
that this converges almost immediately at the scale a tactics game needs.
The Worker was the point
Turn resolution — AI decisions, damage, status effects, win conditions — runs entirely in a Web Worker. The main thread receives a resolved turn and only ever does rendering.
This is the right split for a turn-based game specifically. Nothing about the simulation is frame-rate-sensitive, so it has no business occupying the thread that also owns paint and input. The main thread never runs game logic; it draws a state.
// main thread
worker.postMessage({ type: "resolveTurn", state, order: unitId })
// worker — no DOM access at all, no imports from the renderer
self.onmessage = ({ data }) => {
self.postMessage({ type: "turnResolved", state: resolve(data.state) })
}It also meant the entire simulation was unit-testable without a DOM, which is not a small nicety when you're writing AI that decides who dies.
Status
Archived, and I'm at peace with that. It taught me hex maths, worker architecture, and that a game's AI is mostly just a priority queue. I'd like to return to it with actual pathfinding through terrain, but the lessons were worth the time spent.