Source code for haive.games.monopoly.utils

"""Monopoly game utilities and board logic.

This module provides utility functions for the monopoly game, including:
    - Board setup and property definitions
    - Game logic calculations
    - Card definitions and handling
    - Rent calculations

"""

import random
from typing import TYPE_CHECKING, Any

from haive.games.monopoly.models import (
    DiceRoll,
    Player,
    Property,
    PropertyColor,
    PropertyType,
)

if TYPE_CHECKING:
    from haive.games.monopoly.state import MonopolyState

# Board positions and property definitions
BOARD_PROPERTIES = {
    0: {"name": "GO", "type": PropertyType.SPECIAL, "color": PropertyColor.SPECIAL},
    1: {
        "name": "Mediterranean Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.BROWN,
        "price": 60,
        "rent": [2, 10, 30, 90, 160, 250],
        "house_cost": 50,
        "mortgage_value": 30,
    },
    2: {
        "name": "Community Chest",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    3: {
        "name": "Baltic Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.BROWN,
        "price": 60,
        "rent": [4, 20, 60, 180, 320, 450],
        "house_cost": 50,
        "mortgage_value": 30,
    },
    4: {
        "name": "Income Tax",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    5: {
        "name": "Reading Railroad",
        "type": PropertyType.RAILROAD,
        "color": PropertyColor.RAILROAD,
        "price": 200,
        "rent": [25, 50, 100, 200],
        "mortgage_value": 100,
    },
    6: {
        "name": "Oriental Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.LIGHT_BLUE,
        "price": 100,
        "rent": [6, 30, 90, 270, 400, 550],
        "house_cost": 50,
        "mortgage_value": 50,
    },
    7: {"name": "Chance", "type": PropertyType.SPECIAL, "color": PropertyColor.SPECIAL},
    8: {
        "name": "Vermont Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.LIGHT_BLUE,
        "price": 100,
        "rent": [6, 30, 90, 270, 400, 550],
        "house_cost": 50,
        "mortgage_value": 50,
    },
    9: {
        "name": "Connecticut Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.LIGHT_BLUE,
        "price": 120,
        "rent": [8, 40, 100, 300, 450, 600],
        "house_cost": 50,
        "mortgage_value": 60,
    },
    10: {"name": "Jail", "type": PropertyType.SPECIAL, "color": PropertyColor.SPECIAL},
    11: {
        "name": "St. Charles Place",
        "type": PropertyType.STREET,
        "color": PropertyColor.PINK,
        "price": 140,
        "rent": [10, 50, 150, 450, 625, 750],
        "house_cost": 100,
        "mortgage_value": 70,
    },
    12: {
        "name": "Electric Company",
        "type": PropertyType.UTILITY,
        "color": PropertyColor.UTILITY,
        "price": 150,
        "rent": [4, 10],  # 4x dice if own 1, 10x dice if own both
        "mortgage_value": 75,
    },
    13: {
        "name": "States Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.PINK,
        "price": 140,
        "rent": [10, 50, 150, 450, 625, 750],
        "house_cost": 100,
        "mortgage_value": 70,
    },
    14: {
        "name": "Virginia Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.PINK,
        "price": 160,
        "rent": [12, 60, 180, 500, 700, 900],
        "house_cost": 100,
        "mortgage_value": 80,
    },
    15: {
        "name": "Pennsylvania Railroad",
        "type": PropertyType.RAILROAD,
        "color": PropertyColor.RAILROAD,
        "price": 200,
        "rent": [25, 50, 100, 200],
        "mortgage_value": 100,
    },
    16: {
        "name": "St. James Place",
        "type": PropertyType.STREET,
        "color": PropertyColor.ORANGE,
        "price": 180,
        "rent": [14, 70, 200, 550, 750, 950],
        "house_cost": 100,
        "mortgage_value": 90,
    },
    17: {
        "name": "Community Chest",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    18: {
        "name": "Tennessee Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.ORANGE,
        "price": 180,
        "rent": [14, 70, 200, 550, 750, 950],
        "house_cost": 100,
        "mortgage_value": 90,
    },
    19: {
        "name": "New York Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.ORANGE,
        "price": 200,
        "rent": [16, 80, 220, 600, 800, 1000],
        "house_cost": 100,
        "mortgage_value": 100,
    },
    20: {
        "name": "Free Parking",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    21: {
        "name": "Kentucky Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.RED,
        "price": 220,
        "rent": [18, 90, 250, 700, 875, 1050],
        "house_cost": 150,
        "mortgage_value": 110,
    },
    22: {
        "name": "Chance",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    23: {
        "name": "Indiana Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.RED,
        "price": 220,
        "rent": [18, 90, 250, 700, 875, 1050],
        "house_cost": 150,
        "mortgage_value": 110,
    },
    24: {
        "name": "Illinois Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.RED,
        "price": 240,
        "rent": [20, 100, 300, 750, 925, 1100],
        "house_cost": 150,
        "mortgage_value": 120,
    },
    25: {
        "name": "B&O Railroad",
        "type": PropertyType.RAILROAD,
        "color": PropertyColor.RAILROAD,
        "price": 200,
        "rent": [25, 50, 100, 200],
        "mortgage_value": 100,
    },
    26: {
        "name": "Atlantic Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.YELLOW,
        "price": 260,
        "rent": [22, 110, 330, 800, 975, 1150],
        "house_cost": 150,
        "mortgage_value": 130,
    },
    27: {
        "name": "Ventnor Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.YELLOW,
        "price": 260,
        "rent": [22, 110, 330, 800, 975, 1150],
        "house_cost": 150,
        "mortgage_value": 130,
    },
    28: {
        "name": "Water Works",
        "type": PropertyType.UTILITY,
        "color": PropertyColor.UTILITY,
        "price": 150,
        "rent": [4, 10],  # 4x dice if own 1, 10x dice if own both
        "mortgage_value": 75,
    },
    29: {
        "name": "Marvin Gardens",
        "type": PropertyType.STREET,
        "color": PropertyColor.YELLOW,
        "price": 280,
        "rent": [24, 120, 360, 850, 1025, 1200],
        "house_cost": 150,
        "mortgage_value": 140,
    },
    30: {
        "name": "Go To Jail",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    31: {
        "name": "Pacific Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.GREEN,
        "price": 300,
        "rent": [26, 130, 390, 900, 1100, 1275],
        "house_cost": 200,
        "mortgage_value": 150,
    },
    32: {
        "name": "North Carolina Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.GREEN,
        "price": 300,
        "rent": [26, 130, 390, 900, 1100, 1275],
        "house_cost": 200,
        "mortgage_value": 150,
    },
    33: {
        "name": "Community Chest",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    34: {
        "name": "Pennsylvania Avenue",
        "type": PropertyType.STREET,
        "color": PropertyColor.GREEN,
        "price": 320,
        "rent": [28, 150, 450, 1000, 1200, 1400],
        "house_cost": 200,
        "mortgage_value": 160,
    },
    35: {
        "name": "Short Line",
        "type": PropertyType.RAILROAD,
        "color": PropertyColor.RAILROAD,
        "price": 200,
        "rent": [25, 50, 100, 200],
        "mortgage_value": 100,
    },
    36: {
        "name": "Chance",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    37: {
        "name": "Park Place",
        "type": PropertyType.STREET,
        "color": PropertyColor.DARK_BLUE,
        "price": 350,
        "rent": [35, 175, 500, 1100, 1300, 1500],
        "house_cost": 200,
        "mortgage_value": 175,
    },
    38: {
        "name": "Luxury Tax",
        "type": PropertyType.SPECIAL,
        "color": PropertyColor.SPECIAL,
    },
    39: {
        "name": "Boardwalk",
        "type": PropertyType.STREET,
        "color": PropertyColor.DARK_BLUE,
        "price": 400,
        "rent": [50, 200, 600, 1400, 1700, 2000],
        "house_cost": 200,
        "mortgage_value": 200,
    },
}

# Color group mappings
COLOR_GROUPS = {
    PropertyColor.BROWN: ["Mediterranean Avenue", "Baltic Avenue"],
    PropertyColor.LIGHT_BLUE: [
        "Oriental Avenue",
        "Vermont Avenue",
        "Connecticut Avenue",
    ],
    PropertyColor.PINK: ["St. Charles Place", "States Avenue", "Virginia Avenue"],
    PropertyColor.ORANGE: ["St. James Place", "Tennessee Avenue", "New York Avenue"],
    PropertyColor.RED: ["Kentucky Avenue", "Indiana Avenue", "Illinois Avenue"],
    PropertyColor.YELLOW: ["Atlantic Avenue", "Ventnor Avenue", "Marvin Gardens"],
    PropertyColor.GREEN: [
        "Pacific Avenue",
        "North Carolina Avenue",
        "Pennsylvania Avenue",
    ],
    PropertyColor.DARK_BLUE: ["Park Place", "Boardwalk"],
    PropertyColor.RAILROAD: [
        "Reading Railroad",
        "Pennsylvania Railroad",
        "B&O Railroad",
        "Short Line",
    ],
    PropertyColor.UTILITY: ["Electric Company", "Water Works"],
}

# Chance cards
CHANCE_CARDS = [
    "Advance to GO (Collect $200)",
    "Advance to Illinois Avenue",
    "Advance to St. Charles Place",
    "Advance token to nearest Utility",
    "Advance token to nearest Railroad",
    "Bank pays you dividend of $50",
    "Get Out of Jail Free",
    "Go Back 3 Spaces",
    "Go to Jail",
    "Make general repairs on all your property",
    "Pay poor tax of $15",
    "Take a trip to Reading Railroad",
    "Take a walk on the Boardwalk",
    "You have been elected Chairman of the Board",
    "Your building and loan matures",
    "You have won a crossword competition",
]

# Community Chest cards
COMMUNITY_CHEST_CARDS = [
    "Advance to GO (Collect $200)",
    "Bank error in your favor",
    "Doctor's fee",
    "From sale of stock you get $50",
    "Get Out of Jail Free",
    "Go to Jail",
    "Holiday fund matures",
    "Income tax refund",
    "It is your birthday",
    "Life insurance matures",
    "Pay hospital fees of $100",
    "Pay school fees of $50",
    "Receive $25 consultancy fee",
    "You are assessed for street repairs",
    "You have won second prize in a beauty contest",
    "You inherit $100",
]


[docs] def create_board() -> dict[str, Property]: """Create the initial board with all properties.""" properties = {} for position, prop_data in BOARD_PROPERTIES.items(): # Create ALL properties, including special ones # This ensures positions like Vermont Avenue are properly mapped property_type = PropertyType(prop_data["type"]) # For regular properties, include all details if property_type != PropertyType.SPECIAL: properties[prop_data["name"]] = Property( name=prop_data["name"], position=position, property_type=property_type, color=PropertyColor(prop_data["color"]), price=prop_data.get("price", 0), rent=prop_data.get("rent", [0, 0, 0, 0, 0, 0]), house_cost=prop_data.get("house_cost", 0), mortgage_value=prop_data.get("mortgage_value", 0), ) # For special properties, create minimal Property objects else: properties[prop_data["name"]] = Property( name=prop_data["name"], position=position, property_type=property_type, color=PropertyColor(prop_data["color"]), price=0, rent=[0, 0, 0, 0, 0, 0], house_cost=0, mortgage_value=0, ) # Debug print to verify all properties were created return properties
[docs] def create_players(player_names: list[str]) -> list[Player]: """Create initial players.""" return [Player(name=name) for name in player_names]
[docs] def roll_dice() -> DiceRoll: """Roll two dice.""" return DiceRoll(die1=random.randint(1, 6), die2=random.randint(1, 6))
[docs] def move_player(player: Player, dice_roll: DiceRoll) -> tuple[int, bool]: """Move a player based on dice roll.""" old_position = player.position new_position = (old_position + dice_roll.total) % 40 passed_go = new_position < old_position player.position = new_position return new_position, passed_go
[docs] def get_properties_by_color(color: PropertyColor) -> list[str]: """Get all property names for a color group.""" return COLOR_GROUPS.get(color, [])
[docs] def calculate_rent( property: Property, state: "MonopolyState", dice_roll: int | None = None ) -> int: """Calculate rent for a property.""" if not property.owner or property.mortgaged: return 0 if property.property_type == PropertyType.RAILROAD: # Count railroads owned by the same player railroads_owned = sum( 1 for prop in state.properties.values() if ( prop.property_type == PropertyType.RAILROAD and prop.owner == property.owner and not prop.mortgaged ) ) return property.rent[min(railroads_owned - 1, 3)] if property.property_type == PropertyType.UTILITY: if dice_roll is None: return 0 # Count utilities owned by the same player utilities_owned = sum( 1 for prop in state.properties.values() if ( prop.property_type == PropertyType.UTILITY and prop.owner == property.owner and not prop.mortgaged ) ) multiplier = property.rent[0] if utilities_owned == 1 else property.rent[1] return dice_roll * multiplier if property.property_type == PropertyType.STREET: # Check if owner has monopoly has_monopoly = state.player_owns_monopoly(property.owner, property.color.value) if property.hotel: return property.rent[5] if property.houses > 0: return property.rent[property.houses] if has_monopoly: # Double rent if monopoly but no houses return property.rent[0] * 2 return property.rent[0] return 0
[docs] def get_property_at_position(position: int) -> dict[str, Any] | None: """Get property information at a board position.""" return BOARD_PROPERTIES.get(position)
[docs] def shuffle_cards() -> tuple[list[str], list[str]]: """Shuffle and return chance and community chest cards.""" chance = CHANCE_CARDS.copy() community_chest = COMMUNITY_CHEST_CARDS.copy() random.shuffle(chance) random.shuffle(community_chest) return chance, community_chest
[docs] def handle_special_position( position: int, player: Player, state: "MonopolyState" ) -> str: """Handle special board positions like GO, Jail, etc.""" position_data = get_property_at_position(position) if not position_data: return "unknown_position" position_name = position_data["name"] if position_name == "GO": return "collect_go_money" if position_name == "Jail": return "visiting_jail" if position_name == "Go To Jail": return "go_to_jail" if position_name == "Free Parking": return "free_parking" if position_name == "Income Tax": return "pay_income_tax" if position_name == "Luxury Tax": return "pay_luxury_tax" if position_name == "Chance": return "draw_chance" if position_name == "Community Chest": return "draw_community_chest" return "no_action"
[docs] def check_game_end(state: "MonopolyState") -> tuple[bool, str | None]: """Check if the game should end.""" active_players = state.active_players # Game ends if only one player remains if len(active_players) <= 1: winner = active_players[0].name if active_players else None return True, winner # Could add other end conditions (time limit, etc.) return False, None
[docs] def get_building_cost( property: Property, buildings: int, is_hotel: bool = False ) -> int: """Calculate cost to build houses or hotel.""" if is_hotel: return property.house_cost # Hotel costs same as one house return property.house_cost * buildings
[docs] def can_trade_properties( prop1: Property, prop2: Property, state: "MonopolyState" ) -> bool: """Check if two properties can be traded.""" # Can't trade mortgaged properties if prop1.mortgaged or prop2.mortgaged: return False # Can't trade properties with buildings return not (prop1.houses > 0 or prop1.hotel or prop2.houses > 0 or prop2.hotel)