From b9c5776e207d8e55315a9eeceedfdd36fc568ff1 Mon Sep 17 00:00:00 2001 From: samyaksingh2004 Date: Wed, 25 Mar 2026 23:04:59 +0530 Subject: [PATCH 1/3] Making the start for inital placement dynamic --- catanatron/catanatron/models/map.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/catanatron/catanatron/models/map.py b/catanatron/catanatron/models/map.py index cdb30a59..0beb5dde 100644 --- a/catanatron/catanatron/models/map.py +++ b/catanatron/catanatron/models/map.py @@ -380,7 +380,19 @@ 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) + if map_template == BASE_MAP_TEMPLATE: + possible_starts = [ + (2, -2, 0), (0, -2, 2), (-2, 0, 2), + (-2, 2, 0), (0, 2, -2), (2, 0, -2) + ] + else: # MINI_MAP_TEMPLATE + possible_starts = [ + (1, -1, 0), (0, -1, 1), (-1, 0, 1), + (-1, 1, 0), (0, 1, -1), (1, 0, -1) + ] + + start = random.choice(possible_starts) + i = 0 for coordinate in spiral_land_coordinates(all_tiles, start): tile = all_tiles[coordinate] From d2531c23a3d154a08fccfe05b0ba4d18ba75ac9e Mon Sep 17 00:00:00 2001 From: samyaksingh2004 Date: Thu, 26 Mar 2026 19:05:31 +0530 Subject: [PATCH 2/3] Code cleanup and adding tests --- catanatron/catanatron/models/map.py | 17 ++-------- catanatron/catanatron/models/spiral.py | 47 +++++++++++++++++++++++++- tests/models/test_spiral.py | 21 +++++++++++- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/catanatron/catanatron/models/map.py b/catanatron/catanatron/models/map.py index 0beb5dde..9e6717fd 100644 --- a/catanatron/catanatron/models/map.py +++ b/catanatron/catanatron/models/map.py @@ -15,7 +15,7 @@ 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 @@ -179,7 +179,6 @@ class MapTemplate: }, ) - class CatanMap: """Represents a randomly initialized map.""" @@ -380,19 +379,7 @@ def initialize_tiles( ) # iterate in order of official spiral and assign numbers, skipping desert tile - if map_template == BASE_MAP_TEMPLATE: - possible_starts = [ - (2, -2, 0), (0, -2, 2), (-2, 0, 2), - (-2, 2, 0), (0, 2, -2), (2, 0, -2) - ] - else: # MINI_MAP_TEMPLATE - possible_starts = [ - (1, -1, 0), (0, -1, 1), (-1, 0, 1), - (-1, 1, 0), (0, 1, -1), (1, 0, -1) - ] - - start = random.choice(possible_starts) - + start = get_starting_spiral_coordinates(all_tiles) i = 0 for coordinate in spiral_land_coordinates(all_tiles, start): tile = all_tiles[coordinate] diff --git a/catanatron/catanatron/models/spiral.py b/catanatron/catanatron/models/spiral.py index b44eed2d..a0769ace 100644 --- a/catanatron/catanatron/models/spiral.py +++ b/catanatron/catanatron/models/spiral.py @@ -1,9 +1,18 @@ 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 @@ -65,3 +74,39 @@ 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) \ No newline at end of file diff --git a/tests/models/test_spiral.py b/tests/models/test_spiral.py index 347425e1..4d553844 100644 --- a/tests/models/test_spiral.py +++ b/tests/models/test_spiral.py @@ -1,3 +1,4 @@ +from catanatron.models.spiral import get_starting_spiral_coordinates import pytest from catanatron.models.map import ( @@ -8,7 +9,6 @@ ) from catanatron.models.spiral import spiral_land_coordinates - def test_spiral_land_coordinates_rejects_non_land_start(): all_tiles = initialize_tiles(BASE_MAP_TEMPLATE, number_placement="random") @@ -51,3 +51,22 @@ 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 \ No newline at end of file From 569aa343ca111875f97dc1fb8367b5f80fcb7562 Mon Sep 17 00:00:00 2001 From: samyaksingh2004 Date: Fri, 27 Mar 2026 01:57:49 +0530 Subject: [PATCH 3/3] Run black formatter to pass CI checks --- catanatron/catanatron/models/map.py | 12 ++++++++---- catanatron/catanatron/models/spiral.py | 8 ++++++-- tests/models/test_spiral.py | 14 ++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/catanatron/catanatron/models/map.py b/catanatron/catanatron/models/map.py index 9e6717fd..1b1f827e 100644 --- a/catanatron/catanatron/models/map.py +++ b/catanatron/catanatron/models/map.py @@ -15,7 +15,10 @@ FastResource, NodeRef, ) -from catanatron.models.spiral import get_starting_spiral_coordinates, 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 @@ -179,6 +182,7 @@ class MapTemplate: }, ) + class CatanMap: """Represents a randomly initialized map.""" @@ -254,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 @@ -350,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 ) @@ -379,7 +383,7 @@ def initialize_tiles( ) # iterate in order of official spiral and assign numbers, skipping desert tile - start = get_starting_spiral_coordinates(all_tiles) + start = get_starting_spiral_coordinates(all_tiles) i = 0 for coordinate in spiral_land_coordinates(all_tiles, start): tile = all_tiles[coordinate] diff --git a/catanatron/catanatron/models/spiral.py b/catanatron/catanatron/models/spiral.py index a0769ace..2fb9df55 100644 --- a/catanatron/catanatron/models/spiral.py +++ b/catanatron/catanatron/models/spiral.py @@ -14,6 +14,7 @@ Direction.NORTHEAST, ) + def spiral_land_coordinates( all_tiles: Mapping[Coordinate, Tile], start: Coordinate ) -> Generator[Coordinate, None, None]: @@ -75,9 +76,11 @@ def is_land(coord: Coordinate) -> bool: 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),) @@ -90,7 +93,8 @@ def ring_coordinates(radius: int) -> tuple[Coordinate, ...]: coord = add(coord, UNIT_VECTORS[direction]) return tuple(ring) -def outer_land_coordinates( + +def outer_land_coordinates( all_tiles: Mapping[Coordinate, Tile], ) -> tuple[Coordinate, ...]: """Return outer-ring land coordinates in deterministic coast-following order.""" @@ -109,4 +113,4 @@ def get_starting_spiral_coordinates(all_tiles: Dict[Coordinate, Tile]) -> Coordi outer_ring = outer_land_coordinates(all_tiles) corners = [coord for coord in outer_ring if 0 in coord] - return random.choice(corners) \ No newline at end of file + return random.choice(corners) diff --git a/tests/models/test_spiral.py b/tests/models/test_spiral.py index 4d553844..fc091e7a 100644 --- a/tests/models/test_spiral.py +++ b/tests/models/test_spiral.py @@ -9,6 +9,7 @@ ) from catanatron.models.spiral import spiral_land_coordinates + def test_spiral_land_coordinates_rejects_non_land_start(): all_tiles = initialize_tiles(BASE_MAP_TEMPLATE, number_placement="random") @@ -52,21 +53,22 @@ def test_spiral_land_coordinates_mini_map_order_from_tile_one(): 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 \ No newline at end of file + assert len(starts) > 1