Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Grid Co-ordinate Reference Calculations

This repo contains sample code for working with regular hexagon and triangle grids.
This repo contains sample code for working with regular hexagon and triangle grids.

It's been written as clearly and compactly as possible, to serve as a tutorial and reference for how to work with these grids.
There's many different co-ordinate systems for working with grids - this guide only covers the systems that are easiest to work with, and convert between.
There's many different coordinate systems for working with grids - this guide only covers the systems that are easiest to work with, and convert between.

## Available Grids

Expand All @@ -12,7 +12,7 @@ There's many different co-ordinate systems for working with grids - this guide o
[![](img/square.png)](src/square.py)

### [flat_topped_hex.py](src/flat_topped_hex.py)

Reference guide can be found here: https://www.redblobgames.com/grids/hexagons/
[![](img/flat_topped_hex.png)](src/flat_topped_hex.py)

### [updown_tri.py](src/updown_tri.py)
Expand All @@ -25,11 +25,11 @@ There's many different co-ordinate systems for working with grids - this guide o

## Usage

The code here focuses on keeping the methods as readable as possible. In real life usage, I'd recommend using classes to represent a cell and the grid as a whole.
The code here focuses on keeping the methods as readable as possible. You may want to add abstraction on top of them.

The code uses a three-coordinate system for referring to hexes and triangles. This is simplest for working with in memory, but it's not a good format for storage. You can use methods like `hex_rect_index`/`hex_rect_deindex` to convert from co-ordinates to/from a single integer that addresses the cells starting at 0 and counting upwards without gaps, allowing you to store cell values efficiently in a 1d array or file.

Note that some important grid methods, like path finding, are not included. These methods are the same for any type of grid, you can find good references elsewhere.
Note that some important grid methods, like path finding, are not included. You can find implementations for those in NetworkX.

## Ports

Expand Down
1 change: 1 addition & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .src import flat_topped_hex, flat_topped_trihex
128 changes: 80 additions & 48 deletions src/flat_topped_hex.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,44 @@


from __future__ import division
from math import floor, ceil, sqrt
from settings import edge_length
from common import mod
from updown_tri import pick_tri, tri_line_intersect, tri_rect_intersect
from math import sqrt

from .updown_tri import pick_tri, tri_line_intersect, tri_rect_intersect, tri_rotate_60

sqrt3 = sqrt(3)


# Basics #######################################################################

def hex_center(x, y, z):
def hex_center(x, y, z, edge_length=1.0):
"""Returns the center of a given hex in cartesian co-ordinates"""
# Each unit of x, y, z moves you in the direction of one of the corners of
# the hex, in linear combination.
#
# NB: This function has the nice property that if you pass in x,y,z values that
# sum to 1 or -1 (not a valid hex), it'll return co-ordinates for the vertices of the
# hexes.
return ((1 * x - 0.5 * y - 0.5 * z) * edge_length,
( sqrt3 / 2 * y - sqrt3 / 2 * z) * edge_length)
return ((1 * x - 0.5 * y - 0.5 * z) * edge_length,
(sqrt3 / 2 * y - sqrt3 / 2 * z) * edge_length)

def hex_corners(x, y, z):

def hex_corners(x, y, z, edge_length=1.0):
"""Returns the six corners of a given hex in cartesian co-ordinates"""
return [
hex_center(x , y , z - 1),
hex_center(x , y + 1, z ),
hex_center(x - 1, y , z ),
hex_center(x , y , z + 1),
hex_center(x , y - 1, z ),
hex_center(x + 1, y , z ),
hex_center(x, y, z - 1, edge_length=edge_length),
hex_center(x, y + 1, z, edge_length=edge_length),
hex_center(x - 1, y, z, edge_length=edge_length),
hex_center(x, y, z + 1, edge_length=edge_length),
hex_center(x, y - 1, z, edge_length=edge_length),
hex_center(x + 1, y, z, edge_length=edge_length),
]


def hex_shift(x, y, z, dx, dy, dz):
"""Given hex, shift it by a given offset (one can use hexes around origin for offsets)"""
return x + dx, y + dy, z + dz


def tri_to_hex(x, y, z):
"""Given a triangle co-ordinate as specified in updown_tri, finds the hex that contains it"""
# Rotate the co-ordinate system by 30 degrees, and discretize.
Expand All @@ -63,90 +70,101 @@ def tri_to_hex(x, y, z):
round((z - y) / 3),
)


def hex_to_tris(x, y, z):
"""Given a hex, returns the co-ordinates of the 6 triangles it contains, using co-ordinates as described in updown_tri"""
""" Given a hex, returns the co-ordinates of the 6 triangles it contains, using co-ordinates as
described in updown_tri"""
a = x - y
b = y - z
c = z - x
return [
(a + 1, b , c ),
(a + 1, b + 1, c ),
(a , b + 1, c ),
(a , b + 1, c + 1),
(a , b , c + 1),
(a + 1, b , c + 1),
(a + 1, b, c),
(a + 1, b + 1, c),
(a, b + 1, c),
(a, b + 1, c + 1),
(a, b, c + 1),
(a + 1, b, c + 1),
]


def pick_hex(x, y):
"""Returns the hex that contains a given cartesian co-ordinate point"""
(a, b, c) = pick_tri(x, y)
return tri_to_hex(a, b, c)


def hex_neighbours(x, y, z):
"""Returns the hexes that share an edge with the given hex"""
return [
(x + 1, y , z - 1),
(x , y + 1, z - 1),
(x - 1, y + 1, z ),
(x - 1, y , z + 1),
(x , y - 1, z + 1),
(x + 1, y - 1, z ),
(x + 1, y, z - 1),
(x, y + 1, z - 1),
(x - 1, y + 1, z),
(x - 1, y, z + 1),
(x, y - 1, z + 1),
(x + 1, y - 1, z),
]


def hex_dist(x1, y1, z1, x2, y2, z2):
"""Returns how many steps one hex is from another"""
return (abs(x1 - x2) + abs(y1 - y2) + abs(z1 - z2)) // 2


# Symmetry #####################################################################

def hex_rotate_60(x, y, z, n = 1):
def hex_rotate_60(x, y, z, n=1):
"""Rotates the given hex n * 60 degrees counter clockwise around the origin,
and returns the co-ordinates of the new hex."""
n = mod(n, 6)
n = n % 6
if n == 0:
return (x, y, z)
return x, y, z
if n == 1:
return (-y, -z, -x)
return -y, -z, -x
if n == 2:
return (z, x, y)
return z, x, y
if n == 3:
return (-x, -y, -z)
return -x, -y, -z
if n == 4:
return (y, z, x)
return y, z, x
if n == 5:
return (-z, -x, -y)
return -z, -x, -y


def hex_rotate_about_60(x, y, z, about_x, about_y, about_z, n = 1):
def hex_rotate_about_60(x, y, z, about_x, about_y, about_z, n=1):
"""Rotates the given hex n* 60 degress counter clockwise about the given hex
and return the co-ordinates of the new hex."""
(a, b, c) = tri_rotate_60(x - about_x, y - about_y, z - about_z)
return (a + about_x, y + about_y, z + about_z)
return a + about_x, y + about_y, z + about_z


def hex_reflect_y(x, y, z):
"""Reflects the given hex through the x-axis
and returns the co-ordinates of the new hex"""
return (x, z, y)
return x, z, y


def hex_reflect_x(x, y, z):
"""Reflects the given hex through the y-axis
and returns the co-ordinates of the new hex"""
return (-x, -z, -y)
return -x, -z, -y


def hex_reflect_by(x, y, z, n = 0):
def hex_reflect_by(x, y, z, n=0):
"""Reflects the given hex through the x-axis rotated counter clockwise by n * 30 degrees
and returns the co-ordinates of the new hex"""
(a, b, c) = hex_reflect_y(x, y, z)
return hex_rotate_60(a, b, c, n)


# Shapes #######################################################################

def hex_disc(x, y, z, r):
"""Returns the hexes that are at most distance r from the given hex"""
for dx in range(-r, r + 1):
for dy in range(max(-r, -dx - r), min(r, -dx + r) + 1):
dz = -dx - dy
yield (x + dx, y + dy, z + dz)
yield x + dx, y + dy, z + dz


def hex_line_intersect(x1, y1, x2, y2):
"""Returns hexes that intersect the line specified in cartesian co-ordinates"""
Expand All @@ -157,8 +175,10 @@ def hex_line_intersect(x1, y1, x2, y2):
yield hex
prev = hex


def hex_line(x1, y1, z1, x2, y2, z2):
"""Returns the hexes in a shortest path from one hex to another, staying as close to the straight line as possible"""
"""Returns the hexes in a shortest path from one hex to another, staying as close to the straight line as
possible """
# Note that drawing a straight line from one hex to another can touch hexes not returned by this method.
n = hex_dist(x1, y1, z1, x2, y2, z2)
c1 = hex_center(x1, y1, z1)
Expand All @@ -169,6 +189,7 @@ def hex_line(x1, y1, z1, x2, y2, z2):
py = c1[1] + (c2[1] - c1[1]) * t
yield pick_hex(px, py)


def hex_rect_intersect(x, y, width, height):
"""Returns the hexes that intersect the rectangle specified in cartesian co-ordinates"""
prev = None
Expand All @@ -183,6 +204,7 @@ def hex_rect_intersect(x, y, width, height):
yield hex
prev = hex


def hex_rect(rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_top=False):
"""Returns the hexes in a rectangle that includes the given hex in the bottom left,
that extends `height` hexes upwards, and `width` hexes to the right.
Expand All @@ -192,16 +214,18 @@ def hex_rect(rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_top=Fa
for dx in range(width):
# yield a vertical column
for dy in range(height + (dx % 2) * odd_height):
yield (x, y + dy, z - dy)
yield x, y + dy, z - dy
# Move one column along, staying at the bottom of the rect
x += 1
if dx % 2 == int(inc_bottom):
z -= 1
else:
y -= 1


def hex_rect_knoll(x, y, z, rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_top=False):
"""Given a hex and a rectangle, gives a pair of integer cartesian co-ordinates that identify the square in the rectangle"""
"""Given a hex and a rectangle, gives a pair of integer cartesian co-ordinates that identify the
square in the rectangle"""
dx = x - rect_x
if dx < 0 or dx >= width:
return None
Expand All @@ -211,10 +235,12 @@ def hex_rect_knoll(x, y, z, rect_x, rect_y, rect_z, width, height, inc_bottom=Fa
dy = y - base_dy
return (dx, dy)


def hex_rect_unknoll(dx, dy, rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_top=False):
"""Given a co-ordinate pair and a rectangle, reverses hex_rect_knoll"""
oy = - (dx // 2) - (dx % 2) * int(inc_bottom)
return (rect_x + dx, rect_y + oy + dy, rect_z - dx - oy - dy)
return rect_x + dx, rect_y + oy + dy, rect_z - dx - oy - dy


def hex_rect_index(x, y, z, rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_top=False):
"""Given a hex and a rectangle, gives a linear position of the hex.
Expand All @@ -232,6 +258,7 @@ def hex_rect_index(x, y, z, rect_x, rect_y, rect_z, width, height, inc_bottom=Fa
return None
return left_count + dy


def hex_rect_deindex(index, rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_top=False):
"""Performs the inverse of hex_rect_index
Equivalent to list(hex_rect(...))[index]"""
Expand All @@ -247,14 +274,16 @@ def hex_rect_deindex(index, rect_x, rect_y, rect_z, width, height, inc_bottom=Fa
dy = index
return hex_rect_unknoll(dx, dy, rect_x, rect_y, rect_z, width, height, inc_bottom, inc_top)


def hex_rect_size(rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_top=False):
"""Returns the number of hexes in a given rectangle.
Equivalent to len(list(hex_rect(...)))"""
odd_height = int(inc_bottom) + int(inc_top) - 1
return height * width + odd_height * (width // 2)


# Nesting ## ###################################################################

# Based on work in https://observablehq.com/@sanderevers/hexagon-tiling-of-an-hexagonal-grid
# Groups hexes into larger shapes that is each a disc around a given center hex
# These parent discs are themselves roughly hexagon shaped and roughly laid out like a pointy topped hexagon grid.
Expand All @@ -263,6 +292,7 @@ def hex_rect_size(rect_x, rect_y, rect_z, width, height, inc_bottom=False, inc_t
parent_area = 3 * parent_radius * parent_radius + 3 * parent_radius + 1
parent_shift = 3 * parent_radius + 2


def hex_parent(x, y, z):
"""Returns the parent hex containing the given hex."""
a = (z + y * parent_shift) // parent_area
Expand All @@ -274,6 +304,7 @@ def hex_parent(x, y, z):
(1 + b - a) // 3,
)


def hex_parent_center_child(x, y, z):
"""Returns the central hex of a given parent hex."""
a = y - z
Expand All @@ -285,7 +316,8 @@ def hex_parent_center_child(x, y, z):
(parent_shift * b + a) // 3,
)


def hex_parent_children(x, y, z):
"""Returns all children hex of a given parent hex"""
cx, cy, cz = hex_parent_center_child(x, y, z)
return hex_disc(cx, cy, cz, parent_radius)
return hex_disc(cx, cy, cz, parent_radius)
8 changes: 4 additions & 4 deletions src/flat_topped_trihex.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@

from __future__ import division
from math import floor, ceil, sqrt
from settings import edge_length
from common import mod
from updown_tri import pick_tri, tri_line_intersect, tri_rect_intersect


from .updown_tri import pick_tri, tri_line_intersect, tri_rect_intersect

sqrt3 = sqrt(3)

Expand All @@ -44,7 +44,7 @@ def trihex_cell_type(a, b, c):
if n == -1:
return "tri_down"

def trihex_center(a, b, c):
def trihex_center(a, b, c, edge_length=1.0):
"""Returns the center of a given trihex in cartesian co-ordinates"""
return (( a + -c) * edge_length,
(-sqrt3 / 3 * a + sqrt3 * 2 / 3 * b - sqrt3 / 3 * c) * edge_length)
Expand Down
2 changes: 0 additions & 2 deletions src/settings.py

This file was deleted.

Loading