Creating your own traverser
TIP
If you haven't already, please read Traversing grids first.
A traverser is a function that accepts a hex factory (a function that creates hexes) and an optional cursor hex. That cursor is passed by the previous traverser when traversers are combined. A traverser must return an iterable of hexes. An array is a fine choice in nearly all cases, but a generator is valid too (albeit less performant in most cases).
type Traverser = (
// hex factory: a function that creates a hex
createHex: (coordinates?: HexCoordinates) => Hex,
// cursor: so that the next traverser knows where to continue traversing
cursor?: HexCoordinates,
) => Iterable<Hex>
As an example, we're going to pretend the spiral()
traverser doesn't exist yet and we're going to create it ourselves.
Recreating spiral()
I think it's good practice to use an options object once a function accepts three or more arguments. The function we're going to make accepts a (optional) start
, radius
and (optional) rotation
. start
will be the center of the spiral, unless it's not passed, then cursor
will be the center, unless that doesn't exist either, then the center will default to [0, 0]
.
This is the "empty" version of our spiral()
:
import { Hex, HexCoordinates, Rotation, Traverser } from "honeycomb-grid";
interface SpiralOptions {
start?: HexCoordinates
radius: number
rotation?: Rotation
}
function spiral<T extends Hex>(options: SpiralOptions): Traverser<T> {
return function spiralTraverser(createHex, cursor) {
return []
}
}
Let's review the code:
- Lines 3-7: the options are defined in the
SpiralOptions
interface - Line 9: the generic
T
is needed for the return typeTraverser<T>
. It's the subtype ofHex
this traverser will return and it enables custom hexes. - Line 10:
spiral()
is a function that returns a traverser, that's what's happening here. - Line 11: for now, we're returning an empty array, but here's where we should make the traverser return a spiral of hexes.
Before implementing the core of the traverser it's wise to think about how to best solve the issue. The center hex coordinates are either start
or cursor
or [0, 0]
. Then the algorithm should produce a ring around the center (in the desired rotation
) and continue producing concentric rings outward until the desired radius
is reached. Since we need rings, it would be nice if the ring()
traverser can be used (spoiler: it can!). But how do we make concentric rings? We need a line starting at the center with length radius
and each hex in the line is the start for a ring. We need to repeat rings with a line.
Let's update the code to use ring()
, line()
and repeatWith()
.
function spiral<T extends Hex>(options: SpiralOptions): Traverser<T> {
return function spiralTraverser(createHex, cursor) {
const center = start ?? cursor ?? [0, 0]
const lineTraverser = line({ start, direction: Direction.N, length: radius + 1 })
const ringTraverser = ring({ center, rotation })
const repeatRingWithLine = repeatWith<T>(lineTraverser, ringTraverser)
return repeatRingWithLine(createHex, cursor)
}
}
- Line 3: the
center
coordinates are eitherstart
,cursor
or[0, 0]
. - Line 4: create a line traverser that starts at
start
, has an arbitrarydirection
and a length ofradius + 1
(because the spiral's radius is without its center). - Line 5: create a ring traverser with
center
androtation
.repeatWith()
is going to make sure each starts at the right coordinates (by passing a cursor internally). - Line 6:
repeatWith()
ties the other two traversers together just as we want to: for each hex in the line start a ring at that hex's position. - Line 8: return the hexes by calling the final traverser, passing
createHex
andcursor
.
And that's it: we recreated spiral()
. There's actually one edge-case left that needs fixing: when spiral()
is called without start
but with a cursor
, line()
will be one hex too long (see Combining traversers). So in that specific case, length
should just be radius
:
function spiral<T extends Hex>(options: SpiralOptions): Traverser<T> {
return function spiralTraverser(createHex, cursor) {
// ...
const length = !start && cursor ? radius : radius + 1
const lineTraverser = line({ start, direction: Direction.N, length })
// ...
}
}
Most traversers that accept a start
have these kinds of "exceptions". Because when there's no start
but there is a cursor
(!start && cursor
), the first hex needs to be skipped to prevent duplicate hexes when traversers are combined.