
Table of Contents
-
About The Project
-
Getting Started
-
Features
-
Math Functions
-
Coordinates
-
Grid
-
More Examples
-
License
-
Contributing
-
Contact
-
Acknowledgements
About The Project
As part of a side-project, I needed a robust library for working with hexagonal grids in Go, but didn't find
anything great, so, I decided to implement my own. I stumbled on this great guide
that covers just about everything you could want to know about hexagonal grids and algorithms for them. This project is
an implementation of that guide as an easy-to-use Go library.
(back to top)
Getting Started
Prerequisites
This library was written with heavy use of generics and some experimental go modules. Your project will require at
least Go 1.22.3 installed.
Installation
The library can be installed the usual way with go modules:
go get -u github.com/legendary-code/hexe
(back to top)
Features
These are the features currently supported by this library:
- Coordinate systems
- axial
- cube
- double-height
- double-width
- even-q
- even-r
- odd-q
- odd-r
- Orientations
- Cube coordinate math functions
- Coordinate functions
- Neighbors
- Movement Range
- Set Operations
- Lines
- Rings
- Tracing
- Field of View
- Path Finding
- Grid with load/save functionality
(back to top)
Math Functions
This library provides basic math functions for cubic coordinates, which are then used by the rest of the library. This
is a less common use-case, but, is available if needed.
Example:
math_functions.go
package main
import (
"fmt"
"github.com/legendary-code/hexe/pkg/hexe/math"
)
func mathFunctionsExample() {
distance := math.CubeDistance(0, 1, -1, 0, 2, -2)
fmt.Printf("The distance from (0, 1, -1) to (0, 2, -2) is %d\n", distance)
}
(back to top)
Coordinates
This is the most common usage of this library, working directly with coordinates and sets of coordinates.
Instantiation
instantiation.go
package main
import (
"fmt"
"github.com/legendary-code/hexe/pkg/hexe/coord"
)
func instantiationExample() {
// new axial coordinate (0, 1)
a := coord.NewAxial(0, 1)
// convert to cube coordinates (0, 1, -1)
c := a.Cube()
fmt.Println(c.Q(), c.R(), c.S())
// zero value
c = coord.ZeroCube()
// accessing components
fmt.Println(c.Q(), c.R(), c.S())
}
Sets
Some functions return a set of coordinates, which you can easily work with
sets.go
package main
import (
"fmt"
"github.com/legendary-code/hexe/pkg/hexe/coord"
)
func setsExample() {
// Create a set of axial coordinates
a := coord.NewAxials(
coord.NewAxial(0, 0),
coord.NewAxial(0, 1),
coord.NewAxial(1, 0),
coord.NewAxial(1, 1),
)
// Convert them to cube coordinates
c := a.Cubes()
// You can iterate over them
for iter := c.Iterator(); iter.Next(); {
fmt.Println(iter.Item())
}
// Another way to iterate
c.ForEach(func(v coord.Cube) bool {
fmt.Println(v)
return true
})
}
(back to top)
Visualization
To help visualize hex grids generated in code, simple plotting functionality
is provided for drawing hex grid coordinates and styling the cells.
plot.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func plotExample() {
fig := plot.NewFigure()
center := coord.NewAxial(0, 0)
grid := center.MovementRange(3)
waterStyle := style.Color(colornames.Lightblue).FontSize(40).Name("🌊")
landStyle := style.Color(colornames.Sandybrown).FontSize(40).Name("🏝️")
fig.AddStyledCoords(
grid,
waterStyle,
)
fig.AddStyledCoords(
coord.NewAxials(
coord.NewAxial(0, 0),
coord.NewAxial(1, 0),
coord.NewAxial(1, -1),
coord.NewAxial(0, -1),
coord.NewAxial(-1, 0),
),
landStyle,
)
fig.AddStyledCoord(
coord.NewAxial(1, 1),
landStyle.Name("🏖️"),
)
_ = fig.RenderFile("images/plot.svg")
}
Output:

Neighbors
You can calculate neighbors of a coordinate
neighbors.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func neighborsExample() {
fig := plot.NewFigure()
center := coord.ZeroAxial()
neighbors := center.Neighbors()
fig.AddStyledCoords(neighbors, style.Color(colornames.Lightblue))
fig.AddCoord(center)
_ = fig.RenderFile("images/neighbors.svg")
}
Output:

Diagonal Neighbors
This library also supports diagonal neighbors of a coordinate
diagonal_neighbors.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func diagonalNeighborsExample() {
fig := plot.NewFigure()
center := coord.ZeroAxial()
neighbors := center.DiagonalNeighbors()
fig.AddStyledCoords(neighbors, style.Color(colornames.Lightblue))
fig.AddCoord(center)
_ = fig.RenderFile("images/diagonal_neighbors.svg")
}
Output:

Movement Range
Using the movement range on a coord returns all the coordinates that can be reached into a given number of steps
movement_range.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func movementRangeExample() {
fig := plot.NewFigure()
center := coord.ZeroAxial()
movementRange := center.MovementRange(2)
fig.AddStyledCoords(movementRange, style.Color(colornames.Lightblue))
fig.AddCoord(center)
_ = fig.RenderFile("images/movement_range.svg")
}
Output:

Line
Drawing lines is supported as well
line_to.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func lineToExample() {
fig := plot.NewFigure()
grid := coord.ZeroAxial().MovementRange(3)
from := coord.NewAxial(-1, -1)
to := coord.NewAxial(2, 0)
line := from.LineTo(to)
fig.AddCoords(grid)
fig.AddStyledCoords(line, style.Color(colornames.Lightgreen))
_ = fig.RenderFile("images/line_to.svg")
}
Output:

Trace
Trace draws a line but with collision detection
trace_to.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func traceToExample() {
fig := plot.NewFigure()
grid, walls := createArena()
from := coord.NewAxial(-1, -1)
to := coord.NewAxial(0, 2)
trace := from.TraceTo(to, walls.Contains)
fig.AddCoords(grid)
fig.AddStyledCoords(walls, style.Color(colornames.Bisque))
fig.AddStyledCoords(trace, style.Color(colornames.Lightgreen))
_ = fig.RenderFile("images/trace_to.svg")
}
Output:

Flood Fill
Flood fill tries to fill an area up to a maximum radius, taking into account blocked areas
flood_fill.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func floodFillExample() {
fig := plot.NewFigure()
grid, walls := createArena()
center := coord.NewAxial(-1, -1)
fill := center.FloodFill(3, walls.Contains)
fig.AddCoords(grid)
fig.AddStyledCoords(walls, style.Color(colornames.Bisque))
fig.AddStyledCoords(fill, style.Color(colornames.Lightgreen))
_ = fig.RenderFile("images/flood_fill.svg")
}
Output:

Rotate
You can rotate single coordinates or a set of coordinates around a center in 60-degree increments
rotate.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func rotateExample() {
fig := plot.NewFigure()
grid, walls := createArena()
walls = walls.Rotate(coord.ZeroAxial(), 2)
fig.AddCoords(grid)
fig.AddStyledCoords(walls, style.Color(colornames.Bisque))
_ = fig.RenderFile("images/rotate.svg")
}
Output:

Reflect
You can reflect a coordinate or set of coordinates across the Q, R, or S axis
reflect.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func reflectExample() {
fig := plot.NewFigure()
grid, walls := createArena()
walls = walls.ReflectR()
fig.AddCoords(grid)
fig.AddStyledCoords(walls, style.Color(colornames.Bisque))
_ = fig.RenderFile("images/reflect.svg")
}
Output:

Ring
You can generate rings of various radii
ring.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func ringExample() {
fig := plot.NewFigure()
grid, walls := createArena()
walls = walls.ReflectR()
ring := coord.ZeroAxial().Ring(1)
fig.AddCoords(grid)
fig.AddStyledCoords(walls, style.Color(colornames.Bisque))
fig.AddStyledCoords(ring, style.Color(colornames.Lightcoral))
_ = fig.RenderFile("images/ring.svg")
}
Output:

Field Of View
Field of view casts out rays in all directions from a given coordinate to generate the cells visible from the location
field_of_view.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
"image/color"
)
func fieldOfViewExample() {
fig := plot.NewFigure()
grid, walls := createArena()
person := coord.NewAxial(-1, 2)
fov := person.FieldOfView(3, walls.Contains)
wallStyle := style.Color(colornames.Bisque)
fovStyle := style.Color(color.RGBA{R: 0xdd, G: 0xff, B: 0xdd, A: 0xff})
personStyle := fovStyle.FontSize(40).Name("🧍")
fig.AddCoords(grid)
fig.AddStyledCoords(walls, wallStyle)
fig.AddStyledCoords(fov, fovStyle)
fig.AddStyledCoord(person, personStyle)
_ = fig.RenderFile("images/field_of_view.svg")
}
Output:

Find Path - Breadth First Search
You can perform basic pathfinding with the breadth first search functionality
find_path_bfs.go
package main
import (
"github.com/legendary-code/hexe/pkg/hexe/coord"
"github.com/legendary-code/hexe/pkg/hexe/plot"
"github.com/legendary-code/hexe/pkg/hexe/plot/style"
"golang.org/x/image/colornames"
)
func findPathBfsExample() {
fig := plot.NewFigure()
grid, walls := createArena()
person := coord.NewAxial(-2, 0)
target := coord.NewAxial(-2, 2)
path := person.FindPathBFS(target, 20, walls.Contains)
wallStyle := style.Color(colornames.Bisque)
pathStyle := style.Color(colornames.Lightblue).FontSize(40)
personStyle := pathStyle.Name("🧍")
targetStyle := pathStyle.Name("❌")
fig.AddCoords(grid)
fig.AddStyledCoords(walls, wallStyle)
fig.AddStyledCoords(path, pathStyle)
fig.AddStyledCoord(person, personStyle)
fig.AddStyledCoord(target, targetStyle)
_ = fig.RenderFile("images/find_path_bfs.svg")
}
Output:

(back to top)
Grid
The library also provides a basic Grid[C coords.Coord] collection type for storing and querying values by coordinates.
Grid Operations
grid.go
package main
import (
"fmt"
"github.com/legendary-code/hexe/pkg/hexe"
"github.com/legendary-code/hexe/pkg/hexe/coord"
)
func gridExample() {
grid := hexe.NewAxialGrid[string]()
// set some values
coords := coord.ZeroAxial().MovementRange(2)
for i := coords.Iterator(); i.Next(); {
c := i.Item()
grid.Set(c, fmt.Sprintf("%d", c.Q()+c.R()))
}
// remove the center value
grid.Delete(coord.ZeroAxial())
// get values for a line
line := coord.NewAxial(1, 1).LineTo(coord.NewAxial(-1, -1))
values := grid.GetAll(line)
// print it out
for c, value := range values {
fmt.Printf("%v => %s\n", c, value)
}
}
Grid Persistence
You can also persist and load grids to any io.Writer/io.Reader
grid_persistence.go
package main
import (
"fmt"
"github.com/legendary-code/hexe/pkg/hexe"
"github.com/legendary-code/hexe/pkg/hexe/coord"
"strings"
)
type StringEncoderDecoder struct {
}
func (s *StringEncoderDecoder) Encode(value string) ([]byte, error) {
return []byte(value), nil
}
func (s *StringEncoderDecoder) Decode(bytes []byte) (string, error) {
return string(bytes), nil
}
func gridPersistenceExample() {
codec := &StringEncoderDecoder{}
grid := hexe.NewAxialGrid[string](
hexe.WithEncoderDecoder[string](codec),
)
grid.Set(coord.NewAxial(0, 1), "foo")
grid.Set(coord.NewAxial(1, 0), "bar")
sb := strings.Builder{}
err := grid.Encode(&sb)
if err != nil {
panic(err)
}
grid.Clear()
r := strings.NewReader(sb.String())
err = grid.Decode(r)
if err != nil {
panic(err)
}
for i := grid.Iterator(); i.Next(); {
fmt.Printf("%v => %s\n", i.Index(), i.Item())
}
}
(back to top)
More Examples
For more examples of various features, check out ./examples
(back to top)
License
This library uses the MIT License.
(back to top)
Contributing
Any contributions you make are greatly appreciated.
If you have a suggestion that would make this library better, please fork the repo and create a pull request.
You can also file a feature request issue if you don't feel comfortable contributing the solution yourself.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature)
- Commit your Changes (
git commit -am 'Add some AmazingFeature')
- Push to the Branch (
git push origin feature/AmazingFeature)
- Open a Pull Request
(back to top)
Gene Heinrich

Acknowledgements
Hexagonal Grids
Best README.md Template