from __future__ import annotations from typing import List, Tuple import pytest from catan.data import Resource from catan.game import Game, GameConfig, Phase def _prepare_main_phase(game: Game) -> None: game.phase = Phase.MAIN game.has_rolled = True game.pending_discards.clear() game.robber_move_required = False def _assign_settlement(game: Game, player, hex_id: int, corner_index: int = 0) -> None: corner_id = game.board.hexes[hex_id].corners[corner_index] corner = game.board.corners[corner_id] corner.owner = player.name corner.building = "settlement" player.settlements.add(corner_id) def _find_resource_hex(game: Game) -> Tuple[int, Resource, int]: for tile in game.board.hexes.values(): if tile.resource != Resource.DESERT and tile.number_token: return tile.id, tile.resource, tile.number_token raise RuntimeError("No producible hex found") def _find_edge_path(game: Game, length: int) -> Tuple[List[Tuple[int, int, int]], List[Tuple[int, int, int]]]: board = game.board def dfs(corner_id, path_edges, path_corners, visited_edges): if len(path_edges) == length: return list(path_edges), list(path_corners) for edge_id in board.corners[corner_id].edges: if edge_id in visited_edges: continue visited_edges.add(edge_id) edge = board.edges[edge_id] next_corner = next(c for c in edge.corners if c != corner_id) path_edges.append(edge_id) path_corners.append(next_corner) result = dfs(next_corner, path_edges, path_corners, visited_edges) if result: return result path_edges.pop() path_corners.pop() visited_edges.remove(edge_id) return None for start in board.corners: result = dfs(start, [], [start], set()) if result: return result raise RuntimeError("Unable to find connected edge path") def test_resource_shortage_blocks_multiple_players() -> None: game = Game(GameConfig(player_names=["A", "B", "C"])) hex_id, resource, token = _find_resource_hex(game) _assign_settlement(game, game.players[0], hex_id, 0) _assign_settlement(game, game.players[1], hex_id, 1) game.bank[resource] = 1 game._distribute_resources(token) # noqa: SLF001 - internal rule helper assert game.players[0].resources[resource] == 0 assert game.players[1].resources[resource] == 0 assert game.bank[resource] == 1 def test_resource_shortage_single_player_gets_remaining() -> None: game = Game(GameConfig(player_names=["A", "B", "C"])) hex_id, resource, token = _find_resource_hex(game) _assign_settlement(game, game.players[0], hex_id, 0) game.bank[resource] = 1 game._distribute_resources(token) # noqa: SLF001 assert game.players[0].resources[resource] == 1 assert game.bank[resource] == 0 def test_robber_requires_valid_victim() -> None: game = Game(GameConfig(player_names=["A", "B", "C"])) _prepare_main_phase(game) other = game.players[1] target_hex = next(h for h in game.board.hexes if h != game.board.robber_hex) _assign_settlement(game, other, target_hex, 0) other.resources[Resource.BRICK] = 1 game.robber_move_required = True with pytest.raises(ValueError, match="victim"): game.move_robber(target_hex) with pytest.raises(ValueError, match="adjacent"): game.move_robber(target_hex, victim=game.players[2].name) empty_hex = next( hid for hid in game.board.hexes if hid not in {target_hex, game.board.robber_hex} and all(game.board.corners[c].owner is None for c in game.board.hexes[hid].corners) ) game.robber_move_required = True with pytest.raises(ValueError, match="No valid victim"): game.move_robber(empty_hex, victim=other.name) game.robber_move_required = True game.move_robber(target_hex, victim=other.name) assert not game.robber_move_required def test_trade_restrictions_enforced() -> None: game = Game(GameConfig(player_names=["A", "B", "C"])) _prepare_main_phase(game) player = game.current_player opponent = game.players[1] player.resources[Resource.BRICK] = 2 opponent.resources[Resource.WOOL] = 2 with pytest.raises(ValueError, match="yourself"): game.trade_with_player(player.name, {Resource.BRICK: 1}, {Resource.WOOL: 1}) with pytest.raises(ValueError, match="both ways"): game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {}) with pytest.raises(ValueError, match="positive"): game.trade_with_player(opponent.name, {Resource.BRICK: 0}, {Resource.WOOL: 1}) with pytest.raises(ValueError, match="matching resources"): game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {Resource.BRICK: 1}) game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {Resource.WOOL: 1}) assert player.resources[Resource.WOOL] == 1 assert opponent.resources[Resource.BRICK] == 1 def test_settlement_breaks_longest_road() -> None: game = Game(GameConfig(player_names=["A", "B", "C"])) player = game.players[0] rival = game.players[1] edges, corners = _find_edge_path(game, 5) for edge_id in edges: game.board.edges[edge_id].owner = player.name player.roads.add(edge_id) game._update_longest_road() # noqa: SLF001 assert player.longest_road block_corner = corners[2] game._build_settlement(rival, block_corner, require_connection=False) # noqa: SLF001 assert not player.longest_road