Table of Contents
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.
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.
The library can be installed the usual way with go modules:
go get -u github.com/legendary-code/hexe
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
- pointy-top
- flat-top
- Cube coordinate math functions
- Coordinate functions
- Neighbors
- Movement Range
- Set Operations
- Lines
- Rings
- Tracing
- Field of View
- Path Finding
- Grid with load/save functionality
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:
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)
}
This is the most common usage of this library, working directly with coordinates and sets of coordinates.
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())
}
Some functions return a set of coordinates, which you can easily work with
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
})
}
To help visualize hex grids generated in code, simple plotting functionality is provided for drawing hex grid coordinates and styling the cells.
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")
}
You can calculate neighbors of a coordinate
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")
}
This library also supports diagonal neighbors of a coordinate
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")
}
Using the movement range on a coord returns all the coordinates that can be reached into a given number of steps
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")
}
Drawing lines is supported as well
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")
}
Trace draws a line but with collision detection
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")
}
Flood fill tries to fill an area up to a maximum radius, taking into account blocked areas
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")
}
You can rotate single coordinates or a set of coordinates around a center in 60-degree increments
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")
}
You can reflect a coordinate or set of coordinates across the Q, R, or S axis
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")
}
You can generate rings of various radii
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")
}
Field of view casts out rays in all directions from a given coordinate to generate the cells visible from the location
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")
}
You can perform basic pathfinding with the breadth first search functionality
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")
}
The library also provides a basic Grid[C coords.Coord]
collection type for storing and querying values by coordinates.
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)
}
}
You can also persist and load grids to any io.Writer
/io.Reader
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())
}
}
For more examples of various features, check out ./examples
This library uses the MIT License.
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