Source code for haive.games.monopoly.standalone_demo

"""Standalone Monopoly demo with minimal dependencies.

This script provides a self-contained demonstration of the Monopoly game
without relying on external dependencies like langchain.

Usage:
    python standalone_demo.py

"""

import random
import time
import traceback
from dataclasses import dataclass, field
from enum import Enum


# Define enums for property types and colors
[docs] class PropertyType(str, Enum): STREET = "street" RAILROAD = "railroad" UTILITY = "utility" SPECIAL = "special"
[docs] class PropertyColor(str, Enum): BROWN = "brown" LIGHT_BLUE = "light_blue" PINK = "pink" ORANGE = "orange" RED = "red" YELLOW = "yellow" GREEN = "green" DARK_BLUE = "dark_blue" RAILROAD = "railroad" UTILITY = "utility" SPECIAL = "special"
# Define basic data models @dataclass class Property: name: str position: int property_type: PropertyType color: PropertyColor price: int rent: list[int] house_cost: int = 0 mortgage_value: int = 0 owner: str | None = None houses: int = 0 hotel: bool = False mortgaged: bool = False @dataclass class Player: name: str money: int = 1500 position: int = 0 properties: list[str] = field(default_factory=list) jail_cards: int = 0 in_jail: bool = False jail_turns: int = 0 doubles_count: int = 0 bankrupt: bool = False def can_afford(self, amount: int) -> bool: """Can Afford. Args: amount: [TODO: Add description] Returns: [TODO: Add return description] """ return self.money >= amount @dataclass class DiceRoll: die1: int die2: int @property def total(self) -> int: """Total. Returns: [TODO: Add return description] """ return self.die1 + self.die2 @property def is_doubles(self) -> bool: """Is Doubles. Returns: [TODO: Add return description] """ return self.die1 == self.die2 @dataclass class GameEvent: event_type: str player: str description: str money_change: int = 0 property_involved: str | None = None details: dict = field(default_factory=dict) @dataclass class GameState: players: list[Player] properties: dict[str, Property] current_player_index: int = 0 turn_number: int = 1 game_events: list[GameEvent] = field(default_factory=list) last_roll: DiceRoll | None = None @property def current_player(self) -> Player: """Current Player. Returns: [TODO: Add return description] """ if not self.players: return Player(name="Unknown") if 0 <= self.current_player_index < len(self.players): return self.players[self.current_player_index] return self.players[0] if self.players else Player(name="Unknown") @property def active_players(self) -> list[Player]: """Active Players. Returns: [TODO: Add return description] """ return [p for p in self.players if not p.bankrupt] def next_player(self) -> None: """Next Player. Returns: [TODO: Add return description] """ if len(self.active_players) <= 1: return if not self.players: return # Find next active player start_index = self.current_player_index for i in range(len(self.players)): next_index = (start_index + 1 + i) % len(self.players) if next_index < len(self.players) and not self.players[next_index].bankrupt: self.current_player_index = next_index break self.turn_number += 1 # Board properties 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], "mortgage_value": 75, }, # Rest of the board properties... # (Adding key properties for brevity, full board would have positions 0-39) 20: { "name": "Free Parking", "type": PropertyType.SPECIAL, "color": PropertyColor.SPECIAL, }, 30: { "name": "Go To Jail", "type": PropertyType.SPECIAL, "color": PropertyColor.SPECIAL, }, 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, }, } # Helper functions
[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 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, ) 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 and new_position != 0 player.position = new_position return new_position, passed_go
[docs] def get_property_at_position(position: int) -> dict | None: """Get property information at a board position.""" return BOARD_PROPERTIES.get(position)
[docs] def calculate_rent( property_obj: Property, state: GameState, dice_roll: int | None = None ) -> int: """Calculate rent for a property.""" if not property_obj.owner or property_obj.mortgaged: return 0 if property_obj.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_obj.owner and not prop.mortgaged ) ) return property_obj.rent[min(railroads_owned - 1, 3)] if property_obj.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_obj.owner and not prop.mortgaged ) ) multiplier = ( property_obj.rent[0] if utilities_owned == 1 else property_obj.rent[1] ) return dice_roll * multiplier if property_obj.property_type == PropertyType.STREET: if property_obj.hotel: return property_obj.rent[5] if property_obj.houses > 0: return property_obj.rent[property_obj.houses] return property_obj.rent[0] return 0
# ANSI color codes for terminal output class Color: RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" BLUE = "\033[94m" MAGENTA = "\033[95m" CYAN = "\033[96m" BOLD = "\033[1m" RESET = "\033[0m" # UI helper functions
[docs] def handle_property_landing(state: GameState, position: int) -> list[GameEvent]: """Handle a player landing on a property.""" events = [] current_player = state.current_player # Get position data position_data = get_property_at_position(position) if not position_data: return events property_name = position_data["name"] # Handle special positions if position_data["type"] == PropertyType.SPECIAL: if property_name == "Income Tax": tax = 200 current_player.money -= tax events.append( GameEvent( event_type="tax_payment", player=current_player.name, description=f"Paid ${tax} Income Tax", money_change=-tax, ) ) elif property_name == "Luxury Tax": tax = 75 current_player.money -= tax events.append( GameEvent( event_type="tax_payment", player=current_player.name, description=f"Paid ${tax} Luxury Tax", money_change=-tax, ) ) elif property_name == "Go To Jail": current_player.position = 10 # Jail position current_player.in_jail = True events.append( GameEvent( event_type="go_to_jail", player=current_player.name, description="Sent to Jail", ) ) elif property_name in ["Chance", "Community Chest"]: pass else: pass return events # Handle regular property property_obj = state.properties.get(property_name) if not property_obj: # Create property from board data property_obj = Property( name=property_name, position=position, property_type=PropertyType(position_data["type"]), color=PropertyColor(position_data["color"]), price=position_data.get("price", 0), rent=position_data.get("rent", [0, 0, 0, 0, 0, 0]), house_cost=position_data.get("house_cost", 0), mortgage_value=position_data.get("mortgage_value", 0), ) # Add to state state.properties[property_name] = property_obj events.append( GameEvent( event_type="property_added", player=current_player.name, description=f"Property {property_name} added to game", property_involved=property_name, ) ) print_property(property_obj) # Check if property is owned if property_obj.owner is None: # Property is unowned - offer to buy if property_obj.price <= current_player.money: # In this demo, always buy if can afford current_player.money -= property_obj.price current_player.properties.append(property_name) property_obj.owner = current_player.name events.append( GameEvent( event_type="property_purchase", player=current_player.name, description=f"Purchased {property_name}", money_change=-property_obj.price, property_involved=property_name, ) ) else: events.append( GameEvent( event_type="property_pass", player=current_player.name, description=f"Could not afford {property_name}", property_involved=property_name, ) ) elif property_obj.owner == current_player.name: pass else: # Pay rent to owner owner = None for player in state.players: if player.name == property_obj.owner: owner = player break if not owner: return events # Calculate rent dice_total = state.last_roll.total if state.last_roll else 0 rent = calculate_rent(property_obj, state, dice_total) if rent <= 0: return events # Pay rent if current_player.money >= rent: current_player.money -= rent owner.money += rent events.append( GameEvent( event_type="rent_payment", player=current_player.name, description=f"Paid ${rent} rent to {owner.name}", money_change=-rent, property_involved=property_name, ) ) else: # Simplified bankruptcy current_player.bankrupt = True events.append( GameEvent( event_type="bankruptcy", player=current_player.name, description=f"Went bankrupt owing ${rent} rent to {owner.name}", property_involved=property_name, ) ) return events
[docs] def run_demo(turns: int = 10): """Run a simple Monopoly game demo.""" # Initialize game state players = create_players(["Alice", "Bob", "Charlie"]) properties = create_board() # Ensure Vermont Avenue exists in properties if "Vermont Avenue" not in properties: # Get position 8 (Vermont Avenue) vermont_data = get_property_at_position(8) if vermont_data: properties["Vermont Avenue"] = Property( name="Vermont Avenue", position=8, property_type=PropertyType.STREET, color=PropertyColor.LIGHT_BLUE, price=100, rent=[6, 30, 90, 270, 400, 550], house_cost=50, mortgage_value=50, ) state = GameState( players=players, properties=properties, current_player_index=0, turn_number=1, game_events=[], ) # Show initial state print_player_status(state) # Game loop for _turn in range(1, turns + 1): if len(state.active_players) <= 1: break current_player = state.current_player if current_player.bankrupt: state.next_player() continue print_divider() # Roll dice dice = roll_dice() state.last_roll = dice state.game_events.append( GameEvent( event_type="dice_roll", player=current_player.name, description=f"Rolled {dice.die1} and {dice.die2} for {dice.total}", details={"dice": [dice.die1, dice.die2]}, ) ) # Handle jail if current_player.in_jail: if dice.is_doubles: current_player.in_jail = False state.game_events.append( GameEvent( event_type="jail_release", player=current_player.name, description="Got out of jail by rolling doubles", ) ) else: current_player.jail_turns += 1 if current_player.jail_turns >= 3: # Must pay to get out after 3 turns if current_player.money >= 50: current_player.money -= 50 current_player.in_jail = False state.game_events.append( GameEvent( event_type="jail_release", player=current_player.name, description="Paid $50 to get out of jail after 3 turns", money_change=-50, ) ) else: # Simplified bankruptcy current_player.bankrupt = True state.game_events.append( GameEvent( event_type="bankruptcy", player=current_player.name, description="Went bankrupt unable to pay jail fine", ) ) state.next_player() continue else: state.game_events.append( GameEvent( event_type="jail_stay", player=current_player.name, description=f"Stays in jail (turn { current_player.jail_turns }/3)", ) ) state.next_player() continue # Move player new_position, passed_go = move_player(current_player, dice) # Handle passing GO if passed_go: current_player.money += 200 state.game_events.append( GameEvent( event_type="pass_go", player=current_player.name, description="Passed GO and collected $200", money_change=200, ) ) # Handle landing on property events = handle_property_landing(state, new_position) state.game_events.extend(events) # Print recent events and status print_recent_events(state.game_events) print_player_status(state) # Next player state.next_player() # Brief pause between turns for readability time.sleep(1) # Game summary print_divider() # Determine winner active_players = state.active_players if len(active_players) == 1: active_players[0] else: # Find player with highest money (simplified) best_player = None best_money = -1 for player in active_players: if player.money > best_money: best_money = player.money best_player = player if best_player: pass # Print final player status print_player_status(state) # Print statistics property_owners = {} for prop_name, prop in state.properties.items(): if prop.owner: if prop.owner not in property_owners: property_owners[prop.owner] = [] property_owners[prop.owner].append(prop_name) for _owner, props in property_owners.items(): for prop in props: pass
if __name__ == "__main__": try: run_demo(10) except KeyboardInterrupt: pass except Exception: traceback.print_exc() finally: pass