Skip to content

grid

stable

2D grid and hexagonal grid utilities for game development: cell access, pathfinding (A*), flood fill, Bresenham lines, and distance metrics.

use plugin grid::{create_grid, get_cell, set_cell, …}
20 functions Game
/ filter jk navigate Esc clear
Functions (20)
  1. create_grid Create a 2D grid table with a default value
  2. get_cell Read a cell value by x, y coordinates
  3. set_cell Write a value to a cell, returns updated grid
  4. neighbors_4 List 4-directional neighbors of a cell
  5. neighbors_8 List 8-directional neighbors of a cell
  6. manhattan_distance Manhattan distance between two grid points
  7. chebyshev_distance Chebyshev (king-move) distance between two points
  8. line_bresenham Cells along a Bresenham line
  9. flood_fill Fill connected region with a new value
  10. resize Resize a grid, preserving existing cells
  11. clear Reset all cells to a default value
  12. dimensions Return width and height of a grid
  13. astar A* pathfinding avoiding wall cells
  14. HexGrid Create a hexagonal grid handle
  15. HexGrid.get Read a hex cell by axial (q, r) coordinates
  16. HexGrid.set Write a value to a hex cell
  17. HexGrid.hex_neighbors List the 6 axial neighbors of a hex
  18. HexGrid.hex_distance Axial distance between two hexes
  19. HexGrid.hex_to_pixel Convert axial coordinates to pixel x, y
  20. HexGrid.pixel_to_hex Convert pixel x, y back to axial q, r

Overview

grid is a toolkit for the spatial logic that powers tile-based games and procedural worlds. The square-grid functions are stateless and value-based: a grid is just a plain table with width, height, and cells fields, so you pass it in and (for mutating helpers) get a fresh grid back rather than editing in place. This makes grids easy to snapshot, compare, and reason about, at the cost of treating each transform as a new value (set_cell, flood_fill, resize, and clear all return an updated grid).

Alongside the square grid is a HexGrid class for hexagonal maps. Unlike the table-based square grid, a HexGrid is a stateful handle that uses axial (q, r) coordinates and mutates in place via set. Reach for grid whenever you need cell storage, neighbor enumeration, distance metrics, line drawing, flood fill, or A* pathfinding on a rectangular or hexagonal board.

Common patterns

Build a grid, carve a wall, and find a path around it with A*:

use plugin grid::{create_grid, set_cell, astar}

let g = create_grid(6, 6, 0)
let g2 = set_cell(g, 3, 0, 1)
let g3 = set_cell(g2, 3, 1, 1)
let path = astar(g3, 0, 0, 5, 5, 1)
print("path length: {path.len()}")
print("first step: ({path[1]["x"]}, {path[1]["y"]})")

Enumerate a cell's orthogonal neighbors and rank them by distance to a target:

use plugin grid::{neighbors_4, manhattan_distance}

let ns = neighbors_4(2, 2, 5, 5)
for n in ns {
  let d = manhattan_distance(n["x"], n["y"], 4, 4)
  print("({n["x"]}, {n["y"]}) -> {d}")
}

Drive a stateful hex map with axial coordinates:

use plugin grid::{HexGrid}

let hex = HexGrid(8, 8)
hex.set(2, 3, "forest")
print(hex.get(2, 3))
print("distance: {hex.hex_distance(0, 0, 2, 3)}")

Create a 2D grid table with a default value

Creates a flat grid table with width, height, and cells fields. Every cell is initialized to default_val.

use plugin grid::{create_grid, dimensions}

let g = create_grid(10, 10, 0)
let d = dimensions(g)
print("{d["width"]}x{d["height"]}")

The default value can be any Zolo value, including a string sentinel for an empty tile:

use plugin grid::{create_grid, get_cell}

let board = create_grid(8, 8, ".")
print(get_cell(board, 0, 0))

Read a cell value by x, y coordinates

Reads the value at column x, row y. Returns nil if coordinates are out of bounds.

use plugin grid::{create_grid, get_cell, set_cell}

let g = create_grid(5, 5, 0)
let g2 = set_cell(g, 2, 3, 99)
print(get_cell(g2, 2, 3))

Out-of-bounds reads are safe and yield nil rather than erroring:

use plugin grid::{create_grid, get_cell}

let g = create_grid(3, 3, 0)
print(get_cell(g, 10, 10))

Write a value to a cell, returns updated grid

Returns a new grid with the cell at x, y replaced by val. The original grid is not modified.

use plugin grid::{create_grid, set_cell, get_cell}

let g = create_grid(3, 3, ".")
let g2 = set_cell(g, 1, 1, "X")
print(get_cell(g2, 1, 1))

Because each call returns a fresh grid, chain them to place several tiles:

use plugin grid::{create_grid, set_cell, get_cell}

let g = create_grid(4, 4, 0)
let g2 = set_cell(g, 0, 0, 1)
let g3 = set_cell(g2, 3, 3, 2)
print("{get_cell(g3, 0, 0)} {get_cell(g3, 3, 3)}")

List 4-directional neighbors of a cell

Returns up to 4 adjacent cell coordinates (no diagonals) for position x, y within a grid of size w x h. Boundary cells are excluded.

use plugin grid::{neighbors_4}

let ns = neighbors_4(2, 2, 5, 5)
print(ns[1]["x"])
print(ns[1]["y"])

A corner cell has only two in-bounds orthogonal neighbors:

use plugin grid::{neighbors_4}

let corner = neighbors_4(0, 0, 5, 5)
print("corner neighbors: {corner.len()}")

List 8-directional neighbors of a cell

Returns up to 8 adjacent cell coordinates (including diagonals) for position x, y within a grid of size w x h. Cells outside the board are dropped.

use plugin grid::{neighbors_8}

let ns = neighbors_8(2, 2, 5, 5)
print(ns.len())

Walk every neighbor of an interior cell:

use plugin grid::{neighbors_8}

for n in neighbors_8(3, 3, 8, 8) {
  print("({n["x"]}, {n["y"]})")
}

Manhattan distance between two grid points

Returns the Manhattan distance (sum of absolute coordinate differences) between two grid cells. This matches the cost of moving in 4-directional (orthogonal-only) movement.

use plugin grid::{manhattan_distance}

print(manhattan_distance(0, 0, 3, 4))

Chebyshev (king-move) distance between two points

Returns the Chebyshev distance (the larger of the absolute x and y differences) between two cells. This is the number of moves for a king-style piece that can step diagonally, matching 8-directional movement.

use plugin grid::{chebyshev_distance}

print(chebyshev_distance(0, 0, 3, 4))

Compare the two metrics for the same pair of points to see how diagonals shorten the path:

use plugin grid::{manhattan_distance, chebyshev_distance}

print("manhattan: {manhattan_distance(0, 0, 3, 4)}")
print("chebyshev: {chebyshev_distance(0, 0, 3, 4)}")

Cells along a Bresenham line

Returns all grid cells along the Bresenham line from (x1, y1) to (x2, y2) as a list of {x, y} tables, useful for line-of-sight checks or drawing.

use plugin grid::{line_bresenham}

let pts = line_bresenham(0, 0, 4, 2)
print(pts.len())

Trace each cell the line passes through:

use plugin grid::{line_bresenham}

for p in line_bresenham(0, 0, 3, 3) {
  print("({p["x"]}, {p["y"]})")
}

Fill connected region with a new value

Replaces all cells connected to (x, y) that share the starting cell's value with new_val, spreading orthogonally. Returns the updated grid.

use plugin grid::{create_grid, flood_fill, get_cell}

let g = create_grid(4, 4, 0)
let g2 = flood_fill(g, 0, 0, 5)
print(get_cell(g2, 3, 3))

Walls of a different value stop the fill, so a region bounded by them stays isolated:

use plugin grid::{create_grid, set_cell, flood_fill, get_cell}

let g = create_grid(3, 3, 0)
let walled = set_cell(g, 1, 0, 1)
let filled = flood_fill(walled, 0, 0, 9)
print("origin: {get_cell(filled, 0, 0)}")
print("wall:   {get_cell(filled, 1, 0)}")

Resize a grid, preserving existing cells

Returns a grid with new dimensions, copying over the cells that fall within both the old and new bounds. Newly exposed cells are filled with default_val.

use plugin grid::{create_grid, set_cell, resize, get_cell}

let g = create_grid(3, 3, 0)
let g2 = set_cell(g, 1, 1, 7)
let bigger = resize(g2, 5, 5, 0)
print(get_cell(bigger, 1, 1))

Shrinking a grid discards cells outside the new bounds:

use plugin grid::{create_grid, resize, dimensions}

let g = create_grid(8, 8, 0)
let small = resize(g, 4, 4, 0)
let d = dimensions(small)
print("{d["width"]}x{d["height"]}")

Reset all cells to a default value

Returns a grid of the same dimensions with every cell reset to default_val.

use plugin grid::{create_grid, set_cell, clear, get_cell}

let g = create_grid(4, 4, 0)
let g2 = set_cell(g, 2, 2, 5)
let wiped = clear(g2, 0)
print(get_cell(wiped, 2, 2))

Return width and height of a grid

Returns a table with the width and height of the grid.

use plugin grid::{create_grid, dimensions}

let g = create_grid(12, 9, 0)
let d = dimensions(g)
print("{d["width"]} x {d["height"]}")

A* pathfinding avoiding wall cells

Runs A* pathfinding on a grid from (start_x, start_y) to (end_x, end_y), treating any cell equal to wall_val as impassable. Movement is 4-directional and uses a Manhattan heuristic. Returns a list of {x, y} tables from start to end, or an empty table if no path exists.

use plugin grid::{create_grid, set_cell, astar}

let g = create_grid(5, 5, 0)
let g2 = set_cell(g, 2, 0, 1)
let g3 = set_cell(g2, 2, 1, 1)
let path = astar(g3, 0, 0, 4, 4, 1)
print("path length: {path.len()}")

When walls completely seal off the goal, the result is an empty table:

use plugin grid::{create_grid, set_cell, astar}

let g = create_grid(3, 3, 0)
let g2 = set_cell(g, 0, 1, 1)
let g3 = set_cell(g2, 1, 1, 1)
let g4 = set_cell(g3, 2, 1, 1)
let path = astar(g4, 0, 0, 0, 2, 1)
print("reachable: {path.len() > 0}")

Create a hexagonal grid handle

Creates a stateful hexagonal grid handle of width x height cells, addressed by axial (q, r) coordinates. Unlike the square-grid table functions, a HexGrid mutates in place through its methods: get, set, hex_neighbors, hex_distance, hex_to_pixel, and pixel_to_hex.

use plugin grid::{HexGrid}

let hex = HexGrid(10, 10)
hex.set(2, 3, "forest")
print(hex.get(2, 3))

Read a hex cell by axial (q, r) coordinates

Reads the value stored at axial coordinates (q, r). Returns nil if the coordinates are outside the grid.

use plugin grid::{HexGrid}

let hex = HexGrid(5, 5)
hex.set(1, 1, "water")
print(hex.get(1, 1))
print(hex.get(9, 9))

Write a value to a hex cell

Stores val at axial coordinates (q, r), mutating the grid in place. Out-of-bounds writes are ignored. Returns nil.

use plugin grid::{HexGrid}

let hex = HexGrid(6, 6)
hex.set(0, 0, "start")
hex.set(2, 1, "wall")
print(hex.get(2, 1))

List the 6 axial neighbors of a hex

Returns the six axial neighbors of the hex at (q, r) as a list of {q, r} tables. The offsets are returned regardless of grid bounds.

use plugin grid::{HexGrid}

let hex = HexGrid(8, 8)
let ns = hex.hex_neighbors(2, 2)
print("neighbor count: {ns.len()}")
print("first: ({ns[1]["q"]}, {ns[1]["r"]})")

Axial distance between two hexes

Returns the hex distance between two axial coordinates, computed via cube-coordinate conversion. This is the minimum number of single-step moves between the two hexes.

use plugin grid::{HexGrid}

let hex = HexGrid(10, 10)
print(hex.hex_distance(0, 0, 2, 3))

Convert axial coordinates to pixel x, y

Converts axial coordinates to a pixel position using a pointy-top hex layout, where size is the hex radius. Returns a table with x and y numbers, suitable for rendering.

use plugin grid::{HexGrid}

let hex = HexGrid(10, 10)
let p = hex.hex_to_pixel(2, 1, 16.0)
print("({p["x"]}, {p["y"]})")

Convert pixel x, y back to axial q, r

Converts a pixel position back to the nearest axial hex (the inverse of hex_to_pixel), rounding via cube coordinates. Returns a table with integer q and r fields, handy for translating mouse clicks into hex selections.

use plugin grid::{HexGrid}

let hex = HexGrid(10, 10)
let cell = hex.pixel_to_hex(40.0, 24.0, 16.0)
print("clicked ({cell["q"]}, {cell["r"]})")

Round-trip a hex through pixel space to confirm the conversion is stable:

use plugin grid::{HexGrid}

let hex = HexGrid(10, 10)
let p = hex.hex_to_pixel(3, 2, 20.0)
let back = hex.pixel_to_hex(p["x"], p["y"], 20.0)
print("({back["q"]}, {back["r"]})")
enespt-br