Ruby developer. CTO. Swimmer. Always trying to write more

One of my favourite board games is Tantrix. I can be played as a puzzle like Solitaire or as a competitive game. We picked it up in New Zealand over 10 years ago (it was invented there) and we played it in the evenings as we travelled around. The rules are pretty simple but the game can be frustrating and very tactical.

Anyway, I reckon that Tantrix (or Minesweeper, for that matter) would make a great compsci project instead of the usual projects: there’s a data structure representation problem, gameplay rules to be enforced and non-obvious rules of thumb to apply.

The first step in a creating a game is representing the game state. For Tantrix we need to represent an arbitrary sized collection of hexagonal hexs. We can’t work off a traditional 2D square grid, instead we need a *hexagonal grid*. That’s when I discovered the amazing collection of work by Red Blob Games on HexGrids. The credit for all the maths and C++ implementations below goes to them; I wasn’t figuring it out from first principles.

Coordinates in hex-space are much like those in 3D space — because they are 3D coordinates. I’m using the ‘Cube Coordinate’ system which uses a constrained 3D space to represent hexagons. Seriously: go and check out the brilliant animation here.

http://www.redblobgames.com/grids/hexagons/#coordinates

Once you get your head around the basic coordinate concepts, the implementation guide provides some C code illustrating the basic concepts.

They use a `Hex`

struct to represent coordinates in hex-space and perform operations on it. I decided that it made more sense to separate the location of something and the actual thing itself. So I have a `Point`

class which is responsible for the location in hex-space.

Here’s the original C++ implementation

```
struct Hex { // Axial storage, cube constructor
const int q_, r_;
Hex(int q, int r, int s): q_(q), r_(r) {
assert (q + r + s == 0);
}
inline int q() { return q_; }
inline int r() { return r_; }
inline int s() { return - q_ - r_; }
};
```

and my Ruby version

```
class Point
attr_reader :q, :r, :s
def initialize q, r, s = -q-r
@q = q
@r = r
@s = s
raise InvalidPointError unless valid?
end
```

`q`

, `r`

, and `s`

are our Hex coordinates (like, x,y,z in 3D space). The difference is that the `s`

coordinate is technically unnecessary since it can be calculate from `q`

and `r`

. In Ruby we can easily provide an optional `s`

argument which is calculated from the other constructor args.

It seemed easier for the moment to store `s`

instead of calculating each time it’s required. I might still change my mind (and its easy to define a `s`

method as `-q-r`

but I generally prefer to store the variable as it makes debugging a little simpler.

Next we need to define equality for `Point`

. The C++ implementation looks like

```
bool operator == (Hex a, Hex b) {
return a.q == b.q && a.r == b.r && a.s == b.s;
}
bool operator != (Hex a, Hex b) {
return !(a == b);
}
```

and in Ruby we can do

```
def == other
@q == other.q && @r == other.r && @s == other.s
end
alias :eql? :==
def hash
[@q, @r, @s].hash
end
```

We implement equality as simply comparing the 3 coordinate values. We alias `eql?`

to `==`

so that we can use `Point`

as keys in a `Hash`

. Unlike C++, we don’t need to define `!=`

because that shit is *crazy*.

Since we want to use `Point`

in a `Hash`

, we also implement the `hash`

method which use the fairly common idiom of combining the values into an array and using that `hash`

value.

One last “dumb” thing I do is provide a comparison operator for sorting points. There isn’t really a sensible way to sort points so we’ll just sort them based on the `hash`

value — it has no meaning but it means we can consistently sort points which makes comparing arrays of points much easier!

```
include Comparable
def <=> other
hash <=> other.hash
end
```

Now we need to do coordinate arithmetic which looks like this in C++

```
Hex hex_add(Hex a, Hex b) {
return Hex(a.q + b.q, a.r + b.r, a.s + b.s);
}
Hex hex_subtract(Hex a, Hex b) {
return Hex(a.q - b.q, a.r - b.r, a.s - b.s);
}
Hex hex_multiply(Hex a, int k) {
return Hex(a.q * k, a.r * k, a.s * k);
}
```

and like this in Ruby

```
def + point
Point.new(@q + point.q, @r + point.r, @s + point.s)
end
def - point
Point.new(@q - point.q, @r - point.r, @s - point.s)
end
def * n
Point.new(@q * n, @r * n, @s * n)
end
```

Not exactly rocket science. Implementing the operator methods means we can do things like this

```
a = Point.new 1, 2, -3
b = Point.new 0, -1, 1
a+b #=> Point.new(1, 1, -2)
```

Implementing length and is pretty simple too

```
int hex_length(Hex hex) {
return int((abs(hex.q) + abs(hex.r) + abs(hex.s)) / 2);
}
int hex_distance(Hex a, Hex b) {
return hex_length(hex_subtract(a, b));
}
```

and there’s not much difference in Ruby except the `distance`

method reads a bit better

```
def length
(@q.abs + @r.abs + @s.abs).to_f / 2.0
end
def distance point
(self - point).length
end
```

The next important functionality is to calculate the neighbouring points. The idea is we have a number of fixed directions corresponding to each of the six hexagon edges. In C++ we might do…

```
const vector<Hex> hex_directions = {
Hex(1, 0, -1), Hex(1, -1, 0), Hex(0, -1, 1),
Hex(-1, 0, 1), Hex(-1, 1, 0), Hex(0, 1, -1)
};
Hex hex_direction(int direction /* 0 to 5 */) {
assert (0 <= direction && direction < 6);
return hex_directions[direction];
}
Hex hex_neighbor(Hex hex, int direction) {
return hex_add(hex, hex_direction(direction));
}
```

I’m not much of a fan of passing magic numbers like `int direction`

into functions so I’d rather name these directions. Let’s use the compass points. I’m assuming our hexagons are “pointy-topped” so we’ll have East and West as usual, and use North-West/North-East/South-West/South-East to represent the other faces. I know it’s not technically correct according to the compass points but it’s instantly understandable.

```
EAST = Point.new(-1, 1, 0)
WEST = Point.new(1, -1, 0)
SOUTH_WEST = Point.new(0, -1, 1)
SOUTH_EAST = Point.new(-1, 0, 1)
NORTH_EAST = Point.new(0, 1, -1)
NORTH_WEST = Point.new(1, 0, -1)
VALID_DIRECTIONS = [
Point::EAST,
Point::SOUTH_EAST,
Point::SOUTH_WEST,
Point::WEST,
Point::NORTH_WEST,
Point::NORTH_EAST
]
def neighbor direction
raise InvalidDirectionError unless Point::VALID_DIRECTIONS.include? direction
self + direction
end
alias :neighbour :neighbor
def neighbors
Point::VALID_DIRECTIONS.map { |direction| neighbor direction }
end
alias :neighbours :neighbors
```

I also alias the `neighbor`

method to the British English equivalent because. Just because. Grrr…

This lets us do things like this, which is pretty neat

```
a = Point.new(1, 2, -3)
a.neighbour(Point::EAST) #=> Point.new(0, 3, -3)
```

And fetching all the neighbouring points is as simple as

```
def neighbors
Point::VALID_DIRECTIONS.map { |direction| neighbor direction }
end
```

Now that we have our coordinate structure, we need something to place at those coordinates. Since I’m thinking about Tantrix, I’m going to call this a `Hex`

. Clearly a hex is located somewhere in our hex space so it will need a `Point`

attribute:

```
class Hex
attr_accessor :point
def initialize *args
if args.length == 1
@point = args[0]
elsif args.length > 1
@point = Point.new *args
end
end
```

As I was implementing the tests for this class I realised that writing `Hex.new(Point.new(1,2,-3))`

was pretty crufty. So I allow multiple arguments to the initialiser: if we pass one argument, we can assume it’s an instance of `Point`

; otherwise, we’ll use those arguments to make a `Point`

.

A hex doesn’t just belong somewhere in coordinate space, it belongs on a particular instance of a grid (or “board”, but I’m trying not to make this *too* game-specific)

A `Grid`

is just a collection of hexagons

```
class Grid
def initialize
@hexs = {}
end
```

We use a Hash of `Point`

-> `Hex`

to store the hexs so we can easily look them up based on their location. Actually storing the hexs works like this:

```
def add_hex hex
@hexs[hex.point] = hex
hex.grid = self
end
```

Obviously enough, we’re storing the hexagons in the `hexs`

hash using its location. We also store the grid in the hex instance — I’ll come back to that later.

```
def << hex
if hex.respond_to? :each
hex.each { |t| add_hex t }
else
add_hex hex
end
end
```

We use `<<`

in a similar way to the `Array`

although we can also pass in `Enumerable`

objects like `Array`

and incrementally add those hexagons to the grid.

```
hex = Hex.new(1, 2, -3)
@grid << hex
hexs = [Hex.new(1,2,-3), Hex.new(1, 0, -1)]
@grid << hexs
```

We use the `[]`

method to retrieve a hex from the grid. No surprise, this just does a hash lookup

```
def [] point
@hexs[point]
end
```

We also implement the `each`

method to make our grid work as an Enumerable:

```
include Enumerable
def each
if block_given?
hexs.each { |hex| yield hex }
end
end
```

This means we can iterate over all the hexagons in the grid and perform operations on them. In fact, we’ve already use this functionality in `<<`

which lets us move all the pieces from one grid to another.

I’ll be coming back to this topic to implement route finding, loop detection and the beginnings of a Tantrix game. The current hexgrid implementation is available on Github if you’d like to play around with it.