Skip to content
On this page

Traversing grids

The order in which hexes in a grid are iterated is the same as the order in which they were added to the grid. And because a grid represents a 2D map of hexes in contrast with a 1D list of things (like an array), it makes sense to iterate over a portion of the grid and in a specific order. This is why Honeycomb has traversers.

What is a traverser?

A traverser is a function that produces hexes in a specific order. When passed to a grid's traverse() method, it's a powerful way to iterate over a subset of hexes in a grid in a specific order.

Details

More specifically, a traverser is a function that accepts a hex factory and an optional cursor and returns an iterable of hexes:

typescript
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>

An example:

typescript
const Hex = defineHex({ dimensions: 30 })
const grid = new Grid(Hex, rectangle({ width: 5, height: 5 }))
const spiralTraverser = spiral({ start: [0, 2], radius: 1 })

grid.traverse(spiralTraverser)
  • Line 2: create a rectangular grid of 5⨉5 hexes.
  • Line 3: create a traverser with the built-in spiral() function (spiral() returns a traverser). The spiral starts at the hex with coordinates [0, 2] and runs outward until its radius of 1 hex (excluding the center hex) is reached for a total of 7 hexes.
  • Line 5: grid.traverse() internally calls spiralTraverser and loops over the hexes it produces. Only the hexes that are present in grid are returned in a new grid. In this example all hexes produced by the traverser are also present in grid.

This can be visualized like so:

0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

When the radius is increased from 1 to 2 it becomes apparent that only the hexes present in grid are traversed. You see that the traversal "jumps" from [-2, 4] to [0, 0]:

0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

After [-2, 4] the traverser would've wanted to go to [-2, 3] (North West of [-2, 4]) and then to [-2, 2] (another step NW), but these hexes don't exist in grid and are skipped. That's until [0, 0] is traversed, which is present and that also happens to finish the spiral.

Bail a traversal

grid.traverse() accepts a second parameter that stops traversing when the traverser produces a hex that's not present in the grid.

typescript
grid.traverse(spiralTraverser, { bail: true })
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

Note that it didn't "jump" from [-2, 4] to [0, 0] this time. After [-2, 4] the traverser would've wanted to go to [-2, 3] (which is North West of [-2, 4] and not present in grid) and bailed.

Combining traversers

Any function or method that accepts a traverser, also accepts an array of traversers. This makes it possible to create more complex traversals. For example, you could traverse a 5⨉5 grid with the outline of a square:

typescript
const grid = new Grid(Hex, rectangle({ width: 5, height: 5 }))
const squareOutlineTraverser = [
  line({ direction: Direction.E, length: 4 }),
  line({ direction: Direction.S, length: 3 }),
  line({ direction: Direction.W, length: 3 }),
  line({ direction: Direction.N, length: 3 }),
]

grid.traverse(squareOutlineTraverser)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

Note that line() is never called with a start option. Most functions that create traversers, including line() and rectangle(), accept a start option. But it's optional. When it's not passed it either starts where the previous traverser left off (this happens on lines 4, 5, and 6 in the example above, that's why they have a length of 3). Or if it's the first traverser (line 3 in the example above), start defaults to [0, 0].

This is what happens when start: [1, 1] is passed to the first line():

typescript
const squareOutlineTraverser = [
  line({ start: [1, 1], direction: Direction.E, length: 4 }),
  line({ direction: Direction.S, length: 3 }),
  line({ direction: Direction.W, length: 3 }),
  line({ direction: Direction.N, length: 3 }),
]
grid.traverse(squareOutlineTraverser)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

Built-in traversers

Honeycomb has several functions to create traversers, they are: concat(), fromCoordinates(), line(), move(), rectangle(), repeat(), repeatWith(), ring() and spiral().

concat()

This is mostly used internally to combine (concatenate) traversers. Putting some traversers in an array doesn't magically tie them together, that's what concat() is for:

typescript
// this is just an array of traversers:
const traversers = [line({}), rectangle({})];
// [function lineTraverser() { … }, function rectangleTraverser() { … }]

// this composes them into a single traverser:
concat(traversers) // function concatTraverser() { … }

fromCoordinates()

Probably the simplest traverser. It accepts any number of hex coordinates and returns a traverser that produces hexes with those coordinates:

typescript
const someHexes = fromCoordinates(
  [1, 3],
  { q: 4, r: 0 },
  { col: 0, row: 2 }
)
grid.traverse(someHexes)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

line()

A line traverser can be created in two ways:

  1. With "vector options":
typescript
const vector = line({ start: [1, 0], direction: Direction.SE, length: 4 })
grid.traverse(vector)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

When the direction is ambiguous (North and South for pointy hexes, West and East for flat hexes), the next hex is chosen based on the offset setting.

  1. With "between hexes options":
typescript
const lineBetween = line({ start: [2, 0], stop: [1, 4] })
grid.traverse(lineBetween)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

This uses interpolation to determine which hexes are on the line.

move()

It only accepts a direction and can be used to "move" the cursor a single hex in that direction:

typescript
const firstHex = fromCoordinates([1, 1])
const moveSouth = move(Direction.S)
const moveEast = move(Direction.E)

grid.traverse([firstHex, moveSouth, moveEast])
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

It's equivalent to line({ direction, length: 1 }) and can be used to make more complex traversers.

When the direction is ambiguous (North and South for pointy hexes, West and East for flat hexes), the hex is chosen based on the offset setting.

rectangle()

A rectangle traverser can be created in two ways:

  1. With "rectangle options":
typescript
const square = rectangle({ start: [1, 1], width: 3, height: 3 })
grid.traverse(square)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

It's possible to change the direction of the "rows":

typescript
const square = rectangle({
  start: [0, 3],
  width: 3,
  height: 3,
  direction: Direction.N
})
grid.traverse(square)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

WARNING

When you pass an ordinal direction (NE, SE, SW or NW), you get a diamond shape:

typescript
const square = rectangle({
  start: [0, 2],
  width: 3,
  height: 3,
  direction: Direction.NE
})
grid.traverse(square)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4
  1. With opposing corners:
typescript
const rect = rectangle([-1, 4], [3, 1])
grid.traverse(rect)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

repeat()

Another "helper" traverser. It accepts a number and a traverser (or multiple traversers) to repeat those traverser(s) the given number of times:

typescript
const fiveStepsSE = repeat(3, move(Direction.SE))
grid.traverse([fromCoordinates([2, 0]), fiveStepsSE])
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

A more complex example:

typescript
const rightLeft = [
  line({ direction: Direction.E, length: 2 }),
  move(Direction.S),
  line({ direction: Direction.W, length: 2 }),
  move(Direction.S)
]
const zigZag = repeat(2, rightLeft)

grid.traverse([
  fromCoordinates([1, 0]),
  zigZag,
  line({ direction: Direction.E, length: 2 })
])
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

repeatWith()

Similar to repeat(), this traverser "multiplies" other traversers. However, repeatWith() takes a "source traverser" (or multiple "source traversers") and a "branch traverser" (or multiple "branch traversers"). It iterates over the hexes produced by the source traverser(s) and passes them as cursors to the branch traverser(s). The rectangle() function internally uses repeatWith() like so:

typescript
const width = 3
const height = 3
const square = repeatWith(
  line({ start: [1, 1], direction: Direction.S, length: height }),
  line({ direction: Direction.E, length: width - 1 }),
)

grid.traverse(square)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

The source traverser (line 4) is a line going South and for each hex it produces, a line in a perpendicular direction (East) is created. Because the hexes from the source traverser are included, the branch traverser can be one hex shorter. repeatWith() accepts a third argument to exclude the hexes created by the source traverser(s). Here's the same square, but with { includeSource: false }:

typescript
const width = 3
const height = 3
const square = repeatWith(
  line({ start: [0, 1], direction: Direction.S, length: height }),
  line({ direction: Direction.E, length: width }),
  { includeSource: false },
)

grid.traverse(square)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

Apart from the added third argument, two other things are different from the previous example. start on line 4 is shifted one hex to the West, because no hex from that line() is included in the result anymore. And length on line 5 can now just be width, again, because the first "column" of hexes from the source traverser is missing.

ring()

A ring traverser can be created in two ways:

  1. With "ring options":
typescript
const someRing = ring({ start: [1, 4], center: [1, 2] })
grid.traverse(someRing)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

The direction of rotation can be changed as well:

typescript
const ccwRing = ring({
  start: [1, 4],
  center: [1, 2],
  rotation: Rotation.COUNTERCLOCKWISE
})
grid.traverse(ccwRing)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4
  1. With "radius options":
typescript
const radiusRing = ring({ center: [1, 2], radius: 2 })
grid.traverse(radiusRing)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

Note that when passing a radius, it's not possible to control where the ring starts (it'll always start East of the center).

spiral()

A spiral has one required option: radius. But you may want to pass start as well. If you don't it'll start where the previous traverser left off, or at [0, 0] if there is no previous traverser.

typescript
const spiralFrom1_2 = spiral({ start: [1, 2], radius: 2 })
grid.traverse(spiralFrom1_2)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

Just as with ring(), the radius excludes the center. And also just as with ring(), the rotation can be counterclockwise:

typescript
const ccwSpiral = spiral({
  start: [1, 2],
  radius: 2
  rotation: Rotation.COUNTERCLOCKWISE
})
grid.traverse(ccwSpiral)
0,01,02,03,04,00,11,12,13,14,1-1,20,21,22,23,2-1,30,31,32,33,3-2,4-1,40,41,42,4

Released under the MIT License.