Skip to content
Merged
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
12 changes: 6 additions & 6 deletions .github/workflows/run-pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
python-version: ['3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v4
Expand All @@ -24,9 +24,9 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --no-cache-dir -r test_requirements.txt
pip install -e .
curl -LsSf https://astral.sh/uv/install.sh | sh
uv sync
- name: Run tests
run: |
pytest tests.py
run: uv run pytest --cov=hexometry --cov-fail-under=100 tests.py
- name: Lint
run: uv run ruff check
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
__pycache__/
.pytest_cache/
.ruff_cache/

.coverage
uv.lock

dist/
83 changes: 40 additions & 43 deletions hexometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import random
import functools

from typing import TypeAlias, Iterator
from typing import TypeAlias, Iterator, Callable


__version__ = '0.0.1'
Expand All @@ -15,8 +15,9 @@
_FLOAT_PRECISION = 4


class Direction(enum.Enum):
class Direction(enum.Enum):
"""Hexagonal directions"""

NE = '↗'
E = '→'
SE = '↘'
Expand All @@ -34,17 +35,17 @@ def __repr__(self) -> str:
def __invert__(self) -> 'Direction':
"""turn 180 degrees"""
return ---self

def __neg__(self) -> 'Direction':
"""turn counter-clockwise"""
index = self._all.index(self)
return self._all[index - 1]

def __pos__(self) -> 'Direction':
"""turn clockwise"""
index = self._all.index(self)
return self._all[(index + 1) % len(self._all)]

def __mul__(self, n: int) -> list['Direction']:
return [self] * n

Expand All @@ -62,32 +63,33 @@ class Coord(collections.namedtuple('Coord', ['x', 'y'])):
`z` coordinate can be calculated as -x-y and is useful in some calculations.
Refer to this article to get the idea: https://catlikecoding.com/unity/tutorials/hex-map/part-1/
"""

def __repr__(self) -> str:
return f'[{self.x}:{self.y}]'

@property
def __invert__(self) -> DecartCoord:
return functools.partial(hex_to_decart, self, scale_factor=1)
def __invert__(self) -> Callable[..., DecartCoord]:
return lambda: hex_to_decart(self, scale_factor=1)

@classmethod
def from_decart(cls, x: float, y: float, scale_factor: float = 1) -> 'Coord':
return decart_to_hex(x, y, scale_factor=scale_factor)

def __rshift__(self, other: 'Coord') -> Route:
return get_route(self, other)

def __lshift__(self, other: 'Coord') -> Route:
return get_route(other, self)

def __mul__(self, route: Route) -> 'Coord':
def __mul__(self, route: Route) -> 'Coord': # type: ignore
return traverse_route(self, route)
def __add__(self, direction: Direction) -> 'Coord':

def __add__(self, direction: Direction | str) -> 'Coord': # type: ignore
return get_neighbour(self, direction)

def __sub__(self, other: 'Coord') -> int:
return get_distance(self, other)

def __round__(self, scale_factor: float = 1) -> list[DecartCoord]:
return hex_to_decart_corners(self, scale_factor=scale_factor)

Expand All @@ -97,7 +99,7 @@ def get_route(start: Coord, end: Coord, shuffle: bool = False) -> Route:
if shuffle is True, directions in a route will be shuffled
"""
# grad_x: {grad_y: direction} — coordinates gradients for each direction
gradients = {
gradients = {
0: {1: Direction.NE, -1: Direction.SW},
1: {0: Direction.E, -1: Direction.SE},
-1: {0: Direction.W, 1: Direction.NW},
Expand All @@ -108,16 +110,16 @@ def get_route(start: Coord, end: Coord, shuffle: bool = False) -> Route:

route = []
while dx != 0 or dy != 0:
grad_x = math.copysign(1, dx) if dx != 0 else 0
grad_y = math.copysign(1, dy) if dy != 0 else 0
grad_x = int(math.copysign(1, dx)) if dx != 0 else 0
grad_y = int(math.copysign(1, dy)) if dy != 0 else 0

grad_y = grad_y if grad_y in gradients[grad_x] else next(iter(gradients[grad_x]))

route.append(gradients[grad_x][grad_y])

dx -= grad_x
dy -= grad_y

if shuffle:
random.shuffle(route)

Expand All @@ -126,32 +128,27 @@ def get_route(start: Coord, end: Coord, shuffle: bool = False) -> Route:

# lambda functions for getting coordinates of a neighbour hex in a given direction
NEIGHBOUR_GETTERS = {
Direction.NE: lambda x, y: (x, y+1), # z-1
Direction.NE.value: lambda x, y: (x, y+1), # z-1

Direction.E: lambda x, y: (x+1, y), # z-1
Direction.E.value: lambda x, y: (x+1, y), # z-1

Direction.SE: lambda x, y: (x+1, y-1), # z
Direction.SE.value: lambda x, y: (x+1, y-1), # z

Direction.SW: lambda x, y: (x, y-1), # z+1
Direction.SW.value: lambda x, y: (x, y-1), # z+1

Direction.W: lambda x, y: (x-1, y), # z+1
Direction.W.value: lambda x, y: (x-1, y), # z+1

Direction.NW: lambda x, y: (x-1, y+1), # z
Direction.NW.value: lambda x, y: (x-1, y+1), # z
Direction.NE: lambda x, y: (x, y + 1), # z-1
Direction.NE.value: lambda x, y: (x, y + 1), # z-1
Direction.E: lambda x, y: (x + 1, y), # z-1
Direction.E.value: lambda x, y: (x + 1, y), # z-1
Direction.SE: lambda x, y: (x + 1, y - 1), # z
Direction.SE.value: lambda x, y: (x + 1, y - 1), # z
Direction.SW: lambda x, y: (x, y - 1), # z+1
Direction.SW.value: lambda x, y: (x, y - 1), # z+1
Direction.W: lambda x, y: (x - 1, y), # z+1
Direction.W.value: lambda x, y: (x - 1, y), # z+1
Direction.NW: lambda x, y: (x - 1, y + 1), # z
Direction.NW.value: lambda x, y: (x - 1, y + 1), # z
}


def get_neighbour(coord: Coord, direction: Direction) -> Coord:
def get_neighbour(coord: Coord, direction: Direction | str) -> Coord:
"""Returns the coordinate of the neighbour in the given direction"""
return Coord(*NEIGHBOUR_GETTERS[direction](*coord))


def get_neighbours(coord: Coord, distance: int = 1, within: bool = False) -> Iterator[Coord] | None:
def get_neighbours(coord: Coord, distance: int = 1, within: bool = False) -> Iterator[Coord]:
"""Generator, yields neighbouring coordinates within a given distance

If within is True, yields all coordinates within the distance,
Expand Down Expand Up @@ -218,20 +215,20 @@ def hex_to_decart(coord: Coord, scale_factor: float) -> DecartCoord:
if scale_factor == 0:
return (0.0, 0.0)

decart_x = 3/2 * hex_x * scale_factor
decart_x = 3 / 2 * hex_x * scale_factor
decart_y = (3**0.5 / 2 * hex_x + 3**0.5 * hex_y) * scale_factor

return round(decart_x, _FLOAT_PRECISION), round(decart_y, _FLOAT_PRECISION)


def decart_to_hex(x: float, y: float, scale_factor: float) -> Coord:
"""Converts a decart coordinates to a hex coordinate object"""
if scale_factor == 0:
return (0, 0)
return Coord(0, 0)

hx = 2/3 * x
hy = (-1/3 * x + 3**0.5/3 * y)
return Coord(round(hx/scale_factor), round(hy/scale_factor))
hx = 2 / 3 * x
hy = -1 / 3 * x + 3**0.5 / 3 * y
return Coord(round(hx / scale_factor), round(hy / scale_factor))


def hex_to_decart_corners(coord: Coord, scale_factor: float) -> list[DecartCoord]:
Expand Down
27 changes: 25 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,34 @@ build-backend = "flit_core.buildapi"

[project]
name = "hexometry"
authors = [{name = "wlame", email = "wlarne@gmail.com"}]
requires-python = ">=3.10"
authors = [{ name = "wlame", email = "wlarne@gmail.com" }]
readme = "README.md"
license = {file = "LICENSE"}
license = { file = "LICENSE" }
classifiers = ["License :: OSI Approved :: MIT License"]
dynamic = ["version", "description"]
dependencies = []

[project.urls]
Home = "https://github.com/wlame/hexometry"


[dependency-groups]
dev = [
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
"ruff>=0.8.6",
]

[tool.ruff]
# Allow lines to be as long as 120.
line-length = 120


[tool.ruff.lint]
ignore = ["E501"]
# Avoid enforcing line-length violations (`E501`)

[tool.ruff.format]
quote-style = "single"
skip-magic-trailing-comma = false
2 changes: 0 additions & 2 deletions test_requirements.txt

This file was deleted.

27 changes: 16 additions & 11 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ def test_direction_turns():
assert ~Direction.W == Direction.E

assert ~Direction.SE == Direction.NW
assert Direction.NW == ~Direction.SE
assert ~Direction.NW == Direction.SE

assert ~Direction.NE == Direction.SW
assert ~Direction.SW == Direction.NE



def test_direction_multiplication():
Expand Down Expand Up @@ -95,10 +99,10 @@ def test_get_neighbor_by_direction(coordinates, nearest_neighbours):
assert get_neighbour(c, direction.value) == Coord(*expected_neighbour)
assert get_neighbour(coordinates, direction) == Coord(*expected_neighbour)
assert get_neighbour(coordinates, direction.value) == Coord(*expected_neighbour)


@pytest.mark.parametrize('coordinates, neighbours_by_distance', neighbours_by_coordinates.items())
def test_get_neighbors_by_distance(coordinates, neighbours_by_distance):
def test_get_neighbors_by_distance(coordinates, neighbours_by_distance):
c = Coord(*coordinates)

for distance, expected_neighbours in neighbours_by_distance.items():
Expand Down Expand Up @@ -129,8 +133,8 @@ def test_coord_repr():


def test_get_neighbor_for_big_distance_works():
assert len(list(get_neighbours((123, 234), distance=3000))) == 18000
assert len(list(get_neighbours((123, 234), distance=300, within=True))) == 270900
assert len(list(get_neighbours((123, 234), distance=3000))) == 18000 # type: ignore
assert len(list(get_neighbours((123, 234), distance=300, within=True))) == 270900 # type: ignore


def test_get_directed_neighbours():
Expand Down Expand Up @@ -200,7 +204,6 @@ def test_get_distance():
assert c1 - c2 == c2 - c1



coordinates_test_cases = [
# hex_xy, scale_factor, dec_xy
((0, 0), 1, (0.0, 0.0)),
Expand Down Expand Up @@ -233,21 +236,23 @@ def test_decart_to_hex(hex_xy, scale_factor, dec_xy):
assert decart_to_hex(*hex_to_decart(hex_xy, scale_factor=scale_factor), scale_factor=scale_factor) == hex_xy



def test_coord_from_decart():
assert Coord.from_decart(*~Coord(23, 45)) == Coord(23, 45)

assert Coord.from_decart(184.5, -683.294) == Coord(123, -456)
assert Coord.from_decart(184.9, -683.3) == Coord(123, -456)
assert Coord.from_decart(185, -683) == Coord(123, -456)

assert Coord.from_decart(-579.60675, 2146.568238, scale_factor=3.14) == Coord(-123, 456)
assert Coord.from_decart(-579.60675, 2146.568238,
scale_factor=3.14) == Coord(-123, 456)


hex_corners_coordinates_test_cases = [
# hex_xy, scale_factor, expected_xy
((0, 0), 1, [(0.5, 0.866), (1.0, 0.0), (0.5, -0.866), (-0.5, -0.866), (-1.0, 0.0), (-0.5, 0.866)]),
((0, 0), 2, [(1.0, 1.7321), (2.0, 0.0), (1.0, -1.7321), (-1.0, -1.7321), (-2.0, 0.0), (-1.0, 1.7321)]),
((0, 0), 1, [(0.5, 0.866), (1.0, 0.0), (0.5, -0.866),
(-0.5, -0.866), (-1.0, 0.0), (-0.5, 0.866)]),
((0, 0), 2, [(1.0, 1.7321), (2.0, 0.0), (1.0, -1.7321),
(-1.0, -1.7321), (-2.0, 0.0), (-1.0, 1.7321)]),
(
(-4, 3),
1,
Expand All @@ -270,7 +275,7 @@ def test_coord_from_decart():
(21.255, -2.8319),
(22.89, -5.6638),
(21.255, -8.4957),
]
],
),
]

Expand Down
2 changes: 0 additions & 2 deletions tests_requirements.txt

This file was deleted.

Loading