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
11 changes: 7 additions & 4 deletions catanatron/catanatron/models/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
FastResource,
NodeRef,
)
from catanatron.models.spiral import spiral_land_coordinates
from catanatron.models.spiral import (
get_starting_spiral_coordinates,
spiral_land_coordinates,
)
from catanatron.models.tiles import EdgeId, LandTile, NodeId, Port, Tile, Water

NUM_NODES = 54
Expand Down Expand Up @@ -255,7 +258,7 @@ def init_port_nodes_cache(
if not isinstance(tile, Port):
continue

(a_noderef, b_noderef) = PORT_DIRECTION_TO_NODEREFS[tile.direction]
a_noderef, b_noderef = PORT_DIRECTION_TO_NODEREFS[tile.direction]
port_nodes[tile.resource].add(tile.nodes[a_noderef])
port_nodes[tile.resource].add(tile.nodes[b_noderef])
return port_nodes
Expand Down Expand Up @@ -351,7 +354,7 @@ def initialize_tiles(

# create and save tile
if isinstance(tile_type, tuple): # is port
(_, direction) = tile_type
_, direction = tile_type
port = Port(
port_autoinc, shuffled_port_resources.pop(), direction, nodes, edges
)
Expand Down Expand Up @@ -380,7 +383,7 @@ def initialize_tiles(
)

# iterate in order of official spiral and assign numbers, skipping desert tile
start = (2, -2, 0) if map_template == BASE_MAP_TEMPLATE else (1, -1, 0)
start = get_starting_spiral_coordinates(all_tiles)
i = 0
for coordinate in spiral_land_coordinates(all_tiles, start):
tile = all_tiles[coordinate]
Expand Down
51 changes: 50 additions & 1 deletion catanatron/catanatron/models/spiral.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
from collections.abc import Generator
from typing import Mapping
from typing import Dict, Mapping
import random

from catanatron.models.coordinate_system import Direction, UNIT_VECTORS, add, Coordinate
from catanatron.models.tiles import LandTile, Tile

COUNTERCLOCKWISE_RING_DIRECTIONS = (
Direction.NORTHWEST,
Direction.WEST,
Direction.SOUTHWEST,
Direction.SOUTHEAST,
Direction.EAST,
Direction.NORTHEAST,
)


def spiral_land_coordinates(
all_tiles: Mapping[Coordinate, Tile], start: Coordinate
Expand Down Expand Up @@ -65,3 +75,42 @@ def is_land(coord: Coordinate) -> bool:
continue

direction = directions[(directions.index(direction) + 1) % len(directions)]


def cube_radius(coord: Coordinate) -> int:
return max(abs(component) for component in coord)


def ring_coordinates(radius: int) -> tuple[Coordinate, ...]:
if radius == 0:
return ((0, 0, 0),)

coord = (radius, -radius, 0)
ring = []
for direction in COUNTERCLOCKWISE_RING_DIRECTIONS:
for _ in range(radius):
ring.append(coord)
coord = add(coord, UNIT_VECTORS[direction])
return tuple(ring)


def outer_land_coordinates(
all_tiles: Mapping[Coordinate, Tile],
) -> tuple[Coordinate, ...]:
"""Return outer-ring land coordinates in deterministic coast-following order."""
land_coords = {
coord for coord, tile in all_tiles.items() if isinstance(tile, LandTile)
}
if not land_coords:
return tuple()

radius = max(cube_radius(coord) for coord in land_coords)
return tuple(coord for coord in ring_coordinates(radius) if coord in land_coords)


def get_starting_spiral_coordinates(all_tiles: Dict[Coordinate, Tile]) -> Coordinate:
"""Return a randomly chosen corner coordinate from the outer ring of land coordinates."""
outer_ring = outer_land_coordinates(all_tiles)

corners = [coord for coord in outer_ring if 0 in coord]
return random.choice(corners)
21 changes: 21 additions & 0 deletions tests/models/test_spiral.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from catanatron.models.spiral import get_starting_spiral_coordinates
import pytest

from catanatron.models.map import (
Expand Down Expand Up @@ -51,3 +52,23 @@ def test_spiral_land_coordinates_mini_map_order_from_tile_one():
ids = [all_tiles[coord].id for coord in spiral_land_coordinates(all_tiles, start)]

assert ids == [1, 6, 5, 4, 3, 2, 0]


def test_spiral_starts_at_random_corners():
"""Prove that the spiral starting coordinate is a randomly chosen corner."""

# We only need to generate the board layout once to test the coordinate picker
all_tiles = initialize_tiles(BASE_MAP_TEMPLATE, number_placement="random")
starts = set()

for _ in range(50):
# Directly call the function we modified to see what it picks
start_coord = get_starting_spiral_coordinates(all_tiles)

# 1. Prove it is a corner (cube coordinates for corners always contain a 0)
assert 0 in start_coord

starts.add(start_coord)

# 2. Prove it picked more than one unique corner over 50 calls
assert len(starts) > 1