hexgrid-algorithms
SKILL.md
Hexgrid Algorithms
Complete reference for hexagonal grid mathematics and algorithms optimized for game development.
Coordinate Systems
Cube Coordinates (Recommended)
Three-axis system where q + r + s = 0 always.
interface CubeCoord {
q: number; // Column
r: number; // Row
s: number; // Derived: s = -q - r
}
// Constraint: q + r + s must equal 0
function isValidCube(coord: CubeCoord): boolean {
return coord.q + coord.r + coord.s === 0;
}
Axial Coordinates (Storage-Efficient)
Two-axis system derived from cube (drop s).
interface AxialCoord {
q: number;
r: number;
}
// Convert to cube
function axialToCube(axial: AxialCoord): CubeCoord {
return { q: axial.q, r: axial.r, s: -axial.q - axial.r };
}
// Convert from cube
function cubeToAxial(cube: CubeCoord): AxialCoord {
return { q: cube.q, r: cube.r };
}
Offset Coordinates (Display)
For rendering in standard grid layouts.
// Odd-q offset (pointy-top hexes)
function axialToOffset(axial: AxialCoord): { col: number; row: number } {
const col = axial.q;
const row = axial.r + Math.floor((axial.q - (axial.q & 1)) / 2);
return { col, row };
}
function offsetToAxial(col: number, row: number): AxialCoord {
const q = col;
const r = row - Math.floor((col - (col & 1)) / 2);
return { q, r };
}
Core Operations
Distance
function hexDistance(a: CubeCoord, b: CubeCoord): number {
return Math.max(
Math.abs(a.q - b.q),
Math.abs(a.r - b.r),
Math.abs(a.s - b.s)
);
}
// Or equivalently:
function hexDistanceAlt(a: CubeCoord, b: CubeCoord): number {
return (Math.abs(a.q - b.q) + Math.abs(a.r - b.r) + Math.abs(a.s - b.s)) / 2;
}
Neighbors
const CUBE_DIRECTIONS: CubeCoord[] = [
{ q: 1, r: -1, s: 0 }, // East
{ q: 1, r: 0, s: -1 }, // Southeast
{ q: 0, r: 1, s: -1 }, // Southwest
{ q: -1, r: 1, s: 0 }, // West
{ q: -1, r: 0, s: 1 }, // Northwest
{ q: 0, r: -1, s: 1 }, // Northeast
];
function getNeighbors(hex: CubeCoord): CubeCoord[] {
return CUBE_DIRECTIONS.map(dir => ({
q: hex.q + dir.q,
r: hex.r + dir.r,
s: hex.s + dir.s
}));
}
function getNeighbor(hex: CubeCoord, direction: number): CubeCoord {
const dir = CUBE_DIRECTIONS[direction];
return {
q: hex.q + dir.q,
r: hex.r + dir.r,
s: hex.s + dir.s
};
}
Ring
Get all hexes at exact distance from center.
function hexRing(center: CubeCoord, radius: number): CubeCoord[] {
if (radius === 0) return [center];
const results: CubeCoord[] = [];
let hex: CubeCoord = {
q: center.q + CUBE_DIRECTIONS[4].q * radius,
r: center.r + CUBE_DIRECTIONS[4].r * radius,
s: center.s + CUBE_DIRECTIONS[4].s * radius
};
for (let i = 0; i < 6; i++) {
for (let j = 0; j < radius; j++) {
results.push({ ...hex });
hex = getNeighbor(hex, i);
}
}
return results;
}
Spiral (All Hexes Within Radius)
function hexSpiral(center: CubeCoord, radius: number): CubeCoord[] {
const results: CubeCoord[] = [center];
for (let r = 1; r <= radius; r++) {
results.push(...hexRing(center, r));
}
return results;
}
Pathfinding
A* for Hex Grids
interface PathNode {
coord: CubeCoord;
g: number; // Cost from start
h: number; // Heuristic to goal
f: number; // Total: g + h
parent: PathNode | null;
}
function hexKey(coord: CubeCoord): string {
return `${coord.q},${coord.r}`;
}
function findPath(
start: CubeCoord,
goal: CubeCoord,
isPassable: (coord: CubeCoord) => boolean,
getCost: (coord: CubeCoord) => number = () => 1
): CubeCoord[] | null {
const openSet = new Map<string, PathNode>();
const closedSet = new Set<string>();
const startNode: PathNode = {
coord: start,
g: 0,
h: hexDistance(start, goal),
f: hexDistance(start, goal),
parent: null
};
openSet.set(hexKey(start), startNode);
while (openSet.size > 0) {
// Get node with lowest f
let current: PathNode | null = null;
for (const node of openSet.values()) {
if (!current || node.f < current.f) {
current = node;
}
}
if (!current) break;
// Check if reached goal
if (hexKey(current.coord) === hexKey(goal)) {
const path: CubeCoord[] = [];
let node: PathNode | null = current;
while (node) {
path.unshift(node.coord);
node = node.parent;
}
return path;
}
// Move current to closed set
openSet.delete(hexKey(current.coord));
closedSet.add(hexKey(current.coord));
// Check neighbors
for (const neighbor of getNeighbors(current.coord)) {
const key = hexKey(neighbor);
if (closedSet.has(key) || !isPassable(neighbor)) {
continue;
}
const g = current.g + getCost(neighbor);
const existing = openSet.get(key);
if (!existing || g < existing.g) {
const node: PathNode = {
coord: neighbor,
g,
h: hexDistance(neighbor, goal),
f: g + hexDistance(neighbor, goal),
parent: current
};
openSet.set(key, node);
}
}
}
return null; // No path found
}
Spreading Algorithms
Trouble Spread (Farming in Purria)
function spreadTrouble(
cluster: TroubleCluster,
gridRadius: number,
occupiedHexes: Set<string>
): CubeCoord[] {
const newHexes: CubeCoord[] = [];
const spreadChance = 0.3 + (cluster.severity * 0.1); // 30-80%
for (const hex of cluster.hexCoords) {
for (const neighbor of getNeighbors(hex)) {
const key = hexKey(neighbor);
// Skip if already occupied or out of bounds
if (occupiedHexes.has(key)) continue;
if (hexDistance({ q: 0, r: 0, s: 0 }, neighbor) > gridRadius) continue;
// Random spread chance
if (Math.random() < spreadChance) {
newHexes.push(neighbor);
occupiedHexes.add(key);
}
}
}
return newHexes;
}
Flood Fill
function floodFill(
start: CubeCoord,
maxDistance: number,
isValid: (coord: CubeCoord) => boolean
): CubeCoord[] {
const visited = new Set<string>();
const result: CubeCoord[] = [];
const queue: { coord: CubeCoord; dist: number }[] = [{ coord: start, dist: 0 }];
while (queue.length > 0) {
const { coord, dist } = queue.shift()!;
const key = hexKey(coord);
if (visited.has(key) || dist > maxDistance || !isValid(coord)) {
continue;
}
visited.add(key);
result.push(coord);
for (const neighbor of getNeighbors(coord)) {
queue.push({ coord: neighbor, dist: dist + 1 });
}
}
return result;
}
Rendering
Pixel Coordinates
const HEX_SIZE = 50; // Radius in pixels
// Pointy-top hex
function hexToPixel(hex: AxialCoord, size: number = HEX_SIZE): { x: number; y: number } {
const x = size * (Math.sqrt(3) * hex.q + Math.sqrt(3) / 2 * hex.r);
const y = size * (3 / 2 * hex.r);
return { x, y };
}
function pixelToHex(x: number, y: number, size: number = HEX_SIZE): AxialCoord {
const q = (Math.sqrt(3) / 3 * x - 1 / 3 * y) / size;
const r = (2 / 3 * y) / size;
return hexRound({ q, r });
}
// Round fractional hex to nearest hex
function hexRound(hex: AxialCoord): AxialCoord {
const cube = axialToCube(hex);
let rq = Math.round(cube.q);
let rr = Math.round(cube.r);
let rs = Math.round(cube.s);
const qDiff = Math.abs(rq - cube.q);
const rDiff = Math.abs(rr - cube.r);
const sDiff = Math.abs(rs - cube.s);
if (qDiff > rDiff && qDiff > sDiff) {
rq = -rr - rs;
} else if (rDiff > sDiff) {
rr = -rq - rs;
}
return { q: rq, r: rr };
}
SVG Hex Path
function hexCorners(center: { x: number; y: number }, size: number): { x: number; y: number }[] {
const corners: { x: number; y: number }[] = [];
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 180) * (60 * i - 30); // Pointy-top
corners.push({
x: center.x + size * Math.cos(angle),
y: center.y + size * Math.sin(angle)
});
}
return corners;
}
function hexPath(center: { x: number; y: number }, size: number): string {
const corners = hexCorners(center, size);
return corners.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x} ${c.y}`).join(' ') + ' Z';
}
Performance Tips
- Use string keys for Map/Set operations:
${q},${r} - Pre-calculate neighbors for static grids
- Limit pathfinding with max iterations
- Batch render updates instead of per-hex
- Use spatial hashing for large grids (chunk into regions)
Weekly Installs
16
Repository
ccalebcarter/pu…a-skillsFirst Seen
Jan 24, 2026
Security Audits
Installed on
gemini-cli11
claude-code11
antigravity10
codex10
opencode10
cursor10