A Kotlin library providing a unified abstraction layer for tile-based game boards.
GridKit supports square, hexagonal, triangular, and diamond grid topologies
through a single topology-agnostic API. Built-in grids keep concise constructors
such as SquareGrid<D>, while custom grid implementations can specify their
own cell, edge, and vertex subclasses in the Grid type parameters.
// Square grid — chess / minesweeper style
val board = squareGrid<String>(width = 8, height = 8) {
place(SquareCoordinate(3, 3), data = "M")
}
// Hexagonal grid — Catan / hex-strategy style
val hexBoard = hexGrid<String>(rows = 5, cols = 6) {
place(HexCoordinate(row = 1, col = 2), data = "forest")
}
// Triangular grid — even col = UP /\, odd col = DOWN \/
val triBoard = triangleGrid<Nothing>(cols = 12, rows = 4)
// Diamond/diamond grid — isometric layout
val diamondBoard = diamondGrid<Nothing>(rows = 4, cols = 4)
// Pathfinding — works the same on all topologies
val path = board.findPath(SquareCoordinate(0, 0), SquareCoordinate(7, 7))
// Flood fill & connectivity
val reachable = board.flood(SquareCoordinate(0, 0))
println("Connected: ${board.isConnected()}")
// ASCII box-art renderer
println(board.toAsciiString())Every cell is a structural node in a shared topology graph. Adjacent cells hold
the same Vertex and Edge instances (referential equality ===):
Vertex (corner)
/ \
Edge ——— Cell ——— Edge
\ /
Vertex
val grid = SquareGrid<Nothing>(3, 3)
val a = grid.getCell(SquareCoordinate(1, 1))!!
val b = grid.getCell(SquareCoordinate(2, 1))!! // RIGHT neighbor of a
// Same Edge instance
val edge = a.getEdge(SquareEdgeDirection.RIGHT)!!
assert(edge === b.getEdge(SquareEdgeDirection.LEFT))
// Same Vertex instance at the shared corner
assert(a.getVertex(SquareVertexDirection.TOP_RIGHT) ===
b.getVertex(SquareVertexDirection.TOP_LEFT))
// Navigate via edges (no coordinate arithmetic)
val neighbor: Cell<…>? = a.getNeighbor(SquareEdgeDirection.RIGHT)
// Edge properties
edge.isBorder // true when one side has no cell
edge.getOpposite(a) // returns b (or null for border edges)
edge.vertices // Set<Vertex> of exactly 2, unordered
// Vertex properties
edge.vertices.first().cells // all cells sharing this corner
edge.vertices.first().edges // all edges meeting at this cornercol→ 0 1 2 3 4
row↓
0 (0,0) (1,0) (2,0) (3,0) (4,0)
1 (0,1) (1,1) (2,1) (3,1) (4,1)
2 (0,2) (1,2) (2,2) (3,2) (4,2)
SquareCoordinate(col, row) — col increases rightward, row increases downward.
diagonal = false→ 4-connected (cardinal only, vertex/edge topology)diagonal = true→ 8-connected neighbors viagetNeighbors()/getDirectedNeighbors()
Edge directions: SquareEdgeDirection — TOP, BOTTOM, LEFT, RIGHT
Vertex directions: SquareVertexDirection — TOP_LEFT, TOP_RIGHT, DOWN_LEFT, DOWN_RIGHT
val grid = SquareGrid<Nothing>(5, 5)
grid.getNeighbor(SquareCoordinate(2, 2), SquareEdgeDirection.TOP) // → (2, 1)
grid.getNeighbor(SquareCoordinate(2, 2), SquareEdgeDirection.RIGHT) // → (3, 2)GridKit uses even-r offset coordinates — NOT cube coordinates. Row increases downward; col increases rightward. Odd rows are visually shifted right by half a hex width.
col→ 0 1 2 3 4
row↓
0 [0,0] [0,1] [0,2] [0,3] [0,4]
1 [1,0] [1,1] [1,2] [1,3] [1,4] ← odd row offset
2 [2,0] [2,1] [2,2] [2,3] [2,4]
Edge directions: HexEdgeDirection — TOP_LEFT, TOP_RIGHT, LEFT, RIGHT, DOWN_LEFT, DOWN_RIGHT
Vertex directions: HexVertexDirection — TOP, TOP_LEFT, TOP_RIGHT, DOWN_LEFT, DOWN, DOWN_RIGHT
Neighbor offsets:
| Direction | Even row | Odd row |
|---|---|---|
| TOP_LEFT | (row-1, col-1) | (row-1, col) |
| TOP_RIGHT | (row-1, col) | (row-1, col+1) |
| LEFT | (row, col-1) | (row, col-1) |
| RIGHT | (row, col+1) | (row, col+1) |
| DOWN_LEFT | (row+1, col-1) | (row+1, col) |
| DOWN_RIGHT | (row+1, col) | (row+1, col+1) |
Physical coordinate mapping (hexWidth=1.0, hexHeight=1.0):
x = col * hexWidth + (if oddRow then hexWidth/2 else 0.0)
y = row * hexHeight * 0.75
Each cell is a TriangleCoordinate(col, row). The pointing direction is encoded
in the parity of col:
- even
col→ UP-pointing triangle∆(apex at top) - odd
col→ DOWN-pointing triangle∇(apex at bottom)
col: 0 1 2 3 4 5 6 7
/\ \/ /\ \/ /\ \/ /\ \/ row 0
/\ \/ /\ \/ /\ \/ /\ \/ row 1
Edge directions: TriangleEdgeDirection — LEFT, RIGHT, VERTICAL
Vertex directions: TriangleVertexDirection — LEFT, RIGHT, VERTICAL
(VERTICAL = apex: top for UP-pointing, bottom for DOWN-pointing)
Neighbor rules:
| Direction | Formula |
|---|---|
| LEFT | (col-1, row) |
| RIGHT | (col+1, row) |
| VERTICAL | (col+1, row-1) if even col (UP) |
| VERTICAL | (col-1, row+1) if odd col (DOWN) |
val grid = TriangleGrid<Nothing>(cols = 8, rows = 4)
TriangleCoordinate(4, 2).isUp // true — even col
TriangleCoordinate(5, 2).isDown // true — odd col
grid.getNeighbor(TriangleCoordinate(4, 2), TriangleEdgeDirection.VERTICAL) // → (5, 1)
grid.getNeighbor(TriangleCoordinate(5, 2), TriangleEdgeDirection.VERTICAL) // → (4, 3)Physical centroid mapping (slot = col / 2):
x = slot * 0.5 + (if UP then 0.166 else 0.333)
y = row * triHeight + (if UP then 0.333 else 0.666)
Diamond-shaped cells laid out in an isometric (rotated-square) pattern.
col→ 0 1 2 3
row↓
0 (0,0)
1 (1,0)(1,1)
2 (2,0)(2,1)(2,2)(2,3)
3 (3,0)(3,1)(3,2)(3,3)
Edge directions: DiamondEdgeDirection — TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT
Vertex directions: DiamondVertexDirection — TOP, LEFT, RIGHT, DOWN
Neighbor offsets:
| Direction | Offset |
|---|---|
| TOP_LEFT | (row, col-1) |
| TOP_RIGHT | (row-1, col) |
| BOTTOM_LEFT | (row+1, col) |
| BOTTOM_RIGHT | (row, col+1) |
Physical coordinate mapping (diamondWidth=1.0, diamondHeight=1.0):
x = (col - row) * diamondWidth * 0.5
y = (col + row) * diamondHeight * 0.5
toAsciiString() renders diamond grids as shared-border diamond box art with
odd rows staggered horizontally and connector spans scaled to label width.
The grid is sparse by default — only explicitly placed cells exist. Negative coordinates are fully valid in all directions.
// Sparse square grid — only named cells are placed
val board = squareGrid<String> {
place(SquareCoordinate(0, 0), data = "start")
place(SquareCoordinate(0, 2), data = "end")
// (0,1) does NOT exist
}
// Grow organically with placeNext
val hexBoard = hexGrid<Int> {
place(HexCoordinate(0, 0), data = 1)
placeNext(HexCoordinate(0, 0), HexEdgeDirection.RIGHT)
placeNext(HexCoordinate(0, 0), HexEdgeDirection.DOWN_RIGHT)
}
// Negative coordinates work everywhere
val grid = SquareGrid<Nothing>(1, 1)
grid.placeNext(SquareCoordinate(0, 0), SquareEdgeDirection.TOP) // → SquareCoordinate(0, -1)placeNext is idempotent — if the target cell already exists it is returned unchanged.
When a new cell is placed next to existing neighbors, all shared Vertex and Edge instances
are automatically wired up.
Every cell carries an optional typed payload D (null when not set):
data class Terrain(val elevation: Int, val biome: String)
val map = hexGrid<Terrain>(rows = 5, cols = 6) {
place(HexCoordinate(2, 3), data = Terrain(elevation = 500, biome = "forest"))
}
map.getCell(HexCoordinate(2, 3))?.data // Terrain(500, "forest")Use <Nothing> when you don't need per-cell data.
Cell, Edge, and Vertex are open base classes. Custom Grid
implementations can expose subclasses through the Grid<C, Dir, D, CellT, EdgeT, VertexT>
contract so callers keep typed access to additional fields or methods:
class TerrainCell(
coordinate: SquareCoordinate,
data: Terrain? = null
) : Cell<SquareCoordinate, SquareDirection, Terrain>(coordinate, data) {
var movementCost: Int = 1
}The built-in SquareGrid, HexGrid, TriangleGrid, and DiamondGrid continue
to use the base Cell, Edge, and Vertex types.
Boards can grow at runtime — useful for Carcassonne-style games:
val grid = hexGrid<Nothing>(rows = 1, cols = 1)
val newCell = grid.placeNext(HexCoordinate(0, 0), HexEdgeDirection.RIGHT)
// → creates HexCoordinate(0, 1); shared edges/vertices wired; bounding box expands
val same = grid.placeNext(HexCoordinate(0, 0), HexEdgeDirection.RIGHT)
// → returns the existing cell (idempotent)Returns the coordinate closest to the geometric middle of the bounding box (floor-based for even-sized or negative-range grids):
SquareGrid<Nothing>(5, 5).arithmeticCenter() // SquareCoordinate(col=2, row=2)
SquareGrid<Nothing>(4, 4).arithmeticCenter() // SquareCoordinate(col=1, row=1)physicalCenter() returns a GridCenter<C> combining both the raw physical
position and the nearest grid coordinate:
val center = hexGrid<Nothing>(rows = 4, cols = 6).physicalCenter()
center.physical.x // average x of all cells
center.physical.y // average y of all cells
center.coordinate // nearest HexCoordinate to that positionA* with the grid's own distance() as heuristic — works on all topologies:
val path = grid.findPath(from, to) // default: all cells passable
val path = grid.findPath(from, to) { cell -> // custom predicate
cell.data?.passable == true
}
// Returns null when no path existsimport io.gridkit.core.extensions.*
// Flood fill — BFS from start, optional predicate
val reachable: Set<C> = grid.flood(start)
val reachable = grid.flood(start) { cell -> cell.data?.passable == true }
// Connectivity — true if all cells are mutually reachable
val ok: Boolean = grid.isConnected()
// Single-character ASCII debug map
println(grid.toAsciiMap())| Method | Description |
|---|---|
cells: Map<C, CellT> |
All cells |
edges: Set<EdgeT> |
All shared edges |
vertices: Set<VertexT> |
All shared vertices |
getCell(C): CellT? |
Cell at coordinate |
getNeighbors(C): List<CellT> |
Adjacent cells (unordered) |
getDirectedNeighbors(C): Map<Dir, CellT> |
Neighbors keyed by direction |
getNeighbor(C, Dir): C? |
Single neighbour coordinate |
isValidCoordinate(C): Boolean |
Presence check |
findPath(C, C, predicate): List<C>? |
A* pathfinding |
getRange(C, Int): List<CellT> |
All cells within radius |
getRing(C, Int): List<CellT> |
Cells at exact radius |
getLine(C, C): List<CellT> |
Straight line |
distance(C, C): Int |
Step distance |
placeNext(C, Dir): CellT |
Add adjacent cell, expand grid |
arithmeticCenter(): C |
Bounding-box centre coordinate |
physicalCenter(): GridCenter<C> |
Physical centre + nearest coordinate |
toAsciiString(): String |
ASCII box-art renderer |
| Property / Method | Description |
|---|---|
coordinate: C |
Grid position |
data: D? |
Optional payload |
edges: Map<Dir, Edge<D>> |
Direction → shared edge |
vertices: Map<Dir, Vertex<D>> |
Direction → shared vertex |
getEdge(Dir): Edge<D>? |
Edge in a direction |
getVertex(Dir): Vertex<D>? |
Vertex at a corner |
getNeighbors(): Map<Dir, Cell> |
Neighbors via edges |
getNeighborList(): List<Cell> |
Flat neighbor list |
getNeighbor(Dir): Cell? |
Single neighbor via edge |
| Property / Method | Description |
|---|---|
vertices: Set<Vertex<D>> |
Exactly 2 vertices (unordered) |
cellA: Cell? / cellB: Cell? |
The two cells separated by this edge |
isBorder: Boolean |
True when one cell slot is null |
getOpposite(Cell): Cell? |
Cell on the other side |
| Property | Description |
|---|---|
data: D? |
Optional payload |
cells: Set<Cell> |
All cells sharing this corner |
edges: Set<Edge<D>> |
All edges connected here |
| Parameter | Bound | Role |
|---|---|---|
C |
GridCoordinate |
SquareCoordinate, HexCoordinate, TriangleCoordinate, DiamondCoordinate |
Dir |
GridDirection |
SquareDirection, HexDirection, TriangleDirection, DiamondDirection |
D |
— | Optional per-cell data; use Nothing for topology-only grids |
CellT |
Cell<C, Dir, D> |
Concrete cell implementation exposed by a grid |
EdgeT |
Edge<D> |
Concrete edge implementation used by a grid |
VertexT |
Vertex<D> |
Concrete vertex implementation used by a grid |
gridkit/
├── gridkit-core/ ← Pure grid logic, zero UI dependencies
│ └── io.gridkit.core
│ ├── core/ GridCoordinate, GridDirection, Cell, Grid,
│ │ GridCenter, PhysicalCenter
│ ├── topology/ Vertex, Edge (shared structural objects)
│ ├── direction/ SquareDirection, HexDirection, TriangleDirection,
│ │ DiamondDirection (sealed interfaces + enums)
│ ├── grid/ SquareGrid, HexGrid, TriangleGrid, DiamondGrid
│ ├── pathfinding/ PathfindingStrategy, AStarPathfinder
│ ├── dsl/ squareGrid{}, hexGrid{}, triangleGrid{}, diamondGrid{}
│ └── extensions/ flood(), isConnected(), toAsciiMap()
│
└── gridkit-visualization/ ← Placeholder for future renderers
./gradlew :gridkit-core:test # run core tests
./gradlew build # compile + test all modulesRequires JDK 21+. No external runtime dependencies (stdlib only).
