Source code for haive.games.risk.models

"""Pydantic models for Risk game components.

This module defines comprehensive data models for the Risk strategy game,
including territories, continents, players, cards, moves, and game analysis.
All models use Pydantic for validation with extensive documentation and examples.

The Risk implementation supports classic world domination gameplay with
AI-powered strategic analysis, multi-phase turns, and complex territorial
control mechanics.

Examples:
    Creating a territory::

        territory = Territory(
            name="Eastern Australia",
            continent="Australia",
            owner="player_1",
            armies=5,
            adjacent=["Western Australia", "New Guinea"]
        )

    Setting up a player::

        player = Player(
            name="General Smith",
            cards=[Card(card_type=CardType.INFANTRY, territory_name="Alaska")],
            unplaced_armies=3
        )

    Creating an attack move::

        attack = RiskMove(
            move_type=MoveType.ATTACK,
            player="player_1",
            from_territory="Ukraine",
            to_territory="Middle East",
            attack_dice=3
        )

"""

from enum import Enum

from pydantic import BaseModel, Field, computed_field, field_validator


[docs] class CardType(str, Enum): """Types of Risk cards for army reinforcement trading. Risk cards are collected by conquering territories and can be traded in sets for additional armies. The four card types correspond to different military units and provide strategic options for players. Attributes: INFANTRY: Basic ground unit card, most common type. CAVALRY: Mobile unit card, medium rarity. ARTILLERY: Heavy weapon card, provides firepower bonus. WILD: Special card that can substitute for any other type. Examples: Standard card types:: infantry_card = CardType.INFANTRY cavalry_card = CardType.CAVALRY artillery_card = CardType.ARTILLERY Special wild card:: wild_card = CardType.WILD # Can be used as any type Trading combinations:: # Valid trading sets: # - 3 of same type (3 infantry, 3 cavalry, 3 artillery) # - 1 of each type (1 infantry, 1 cavalry, 1 artillery) # - Any combination with wild cards Note: Card trading values typically increase each time cards are traded, starting at 4 armies for the first trade and increasing by 2 each time. """ INFANTRY = "infantry" #: Basic ground unit, represents foot soldiers CAVALRY = "cavalry" #: Mobile unit, represents mounted troops ARTILLERY = "artillery" #: Heavy weapons, represents cannons and siege equipment WILD = "wild" #: Special card that can substitute for any type
[docs] class Card(BaseModel): """A Risk card that can be traded for army reinforcements. Risk cards are earned by conquering at least one territory per turn and can be accumulated and traded in strategic sets for additional armies. Each card typically shows a territory and has a unit type. The card system adds strategic depth by encouraging aggressive play (to earn cards) while providing timing decisions about when to trade for maximum advantage. Attributes: card_type (CardType): The military unit type shown on the card. territory_name (Optional[str]): Territory depicted on the card, if any. Examples: Territory-specific card:: alaska_card = Card( card_type=CardType.INFANTRY, territory_name="Alaska" ) Generic unit card:: cavalry_card = Card( card_type=CardType.CAVALRY # No specific territory ) Wild card:: wild_card = Card( card_type=CardType.WILD, territory_name="Special" ) Note: Territory names on cards are typically for thematic purposes and don't affect trading value, but may provide strategic information about board state. """ card_type: CardType = Field( ..., description="The military unit type represented by this card", examples=[ CardType.INFANTRY, CardType.CAVALRY, CardType.ARTILLERY, CardType.WILD, ], ) territory_name: str | None = Field( None, max_length=50, description="Name of the territory depicted on the card, if any", examples=["Alaska", "Brazil", "China", "Egypt", "Ukraine", None], ) def __str__(self) -> str: """String representation of the card. Returns: str: Human-readable card description. Examples: >>> card = Card(card_type=CardType.INFANTRY, territory_name="Alaska") >>> str(card) "Infantry (Alaska)" >>> card = Card(card_type=CardType.WILD) >>> str(card) "Wild" """ if self.territory_name: return f"{self.card_type.value.capitalize()} ({self.territory_name})" return f"{self.card_type.value.capitalize()}" @computed_field @property def is_wild(self) -> bool: """Check if this is a wild card. Returns: bool: True if this is a wild card that can substitute for any type. Examples: >>> wild_card = Card(card_type=CardType.WILD) >>> wild_card.is_wild True >>> infantry_card = Card(card_type=CardType.INFANTRY) >>> infantry_card.is_wild False """ return self.card_type == CardType.WILD
[docs] class Territory(BaseModel): """A territory on the Risk game board. Territories are the fundamental units of control in Risk. Each territory belongs to a continent, can be owned by a player, contains armies for defense, and connects to adjacent territories for movement and attack. The territorial system forms the core of Risk strategy, as players must control territories to earn reinforcements and achieve victory conditions (total world domination or objective completion). Attributes: name (str): Unique identifier and display name for the territory. continent (str): The continent this territory belongs to. owner (Optional[str]): The player who currently controls this territory. armies (int): Number of armies stationed in this territory for defense. adjacent (List[str]): Names of territories that border this one. Examples: Strategic territory setup:: ukraine = Territory( name="Ukraine", continent="Europe", owner="player_1", armies=8, adjacent=["Scandinavia", "Northern Europe", "Southern Europe", "Ural", "Afghanistan", "Middle East"] ) Neutral territory:: greenland = Territory( name="Greenland", continent="North America", # No owner initially armies=2, adjacent=["Northwest Territory", "Ontario", "Quebec", "Iceland"] ) Fortified position:: egypt = Territory( name="Egypt", continent="Africa", owner="player_2", armies=12, # Heavily fortified adjacent=["Libya", "East Africa", "Middle East"] ) Note: Territory adjacency defines the game's strategic geography. Control of key chokepoints and continental borders often determines game outcomes. """ name: str = Field( ..., min_length=1, max_length=30, description="Unique name identifying this territory on the game board", examples=["Alaska", "Brazil", "China", "Egypt", "Ukraine", "Madagascar"], ) continent: str = Field( ..., min_length=1, max_length=20, description="Name of the continent this territory belongs to", examples=[ "North America", "South America", "Europe", "Africa", "Asia", "Australia", ], ) owner: str | None = Field( None, description="Player ID who currently controls this territory", examples=["player_1", "player_2", "red_player", "blue_player", None], ) armies: int = Field( default=0, ge=0, le=100, description="Number of armies stationed in this territory (0-100)", examples=[1, 5, 12, 25, 3], ) adjacent: list[str] = Field( default_factory=list, description="List of territory names that border this territory", examples=[ ["Western Europe", "Northern Europe", "Southern Europe"], ["Quebec", "Ontario", "Western United States"], ["China", "India", "Ural"], ], )
[docs] @field_validator("name") @classmethod def validate_name(cls, v: str) -> str: """Validate territory name is properly formatted. Args: v (str): Territory name to validate. Returns: str: Cleaned and validated territory name. Raises: ValueError: If name is empty or contains invalid characters. """ name = v.strip() if not name: raise ValueError("Territory name cannot be empty") return name
[docs] @field_validator("armies") @classmethod def validate_armies(cls, v: int) -> int: """Validate army count is reasonable. Args: v (int): Army count to validate. Returns: int: Validated army count. Raises: ValueError: If army count is negative. """ if v < 0: raise ValueError("Army count cannot be negative") return v
def __str__(self) -> str: """String representation of the territory. Returns: str: Human-readable territory description. Examples: >>> territory = Territory(name="Alaska", continent="North America", ... owner="player_1", armies=3) >>> str(territory) "Alaska (player_1, 3 armies)" """ owner_str = f"{self.owner}" if self.owner else "Unoccupied" return f"{self.name} ({owner_str}, {self.armies} armies)" @computed_field @property def is_occupied(self) -> bool: """Check if territory is currently occupied by a player. Returns: bool: True if territory has an owner, False if neutral. """ return self.owner is not None @computed_field @property def defense_strength(self) -> int: """Calculate defensive strength of this territory. Returns: int: Maximum dice this territory can roll in defense (1-2). Note: Territories with 1 army can defend with 1 die, territories with 2+ armies can defend with 2 dice. """ return min(2, max(1, self.armies))
[docs] class Continent(BaseModel): """A continent grouping of territories with bonus reinforcements. Continents provide structure to the Risk board and offer strategic objectives. Players who control all territories in a continent receive bonus armies each turn, creating natural strategic goals. Continental control is often the key to victory in Risk, as the bonus armies compound over time and provide significant advantages in the lengthy conquest campaigns. Attributes: name (str): The name of the continent. bonus (int): Bonus armies awarded for complete continental control. territories (List[str]): Names of all territories in this continent. Examples: High-value continent:: asia = Continent( name="Asia", bonus=7, # Highest bonus but hardest to hold territories=[ "Ural", "Siberia", "Yakutsk", "Kamchatka", "Irkutsk", "Mongolia", "China", "Siam", "India", "Middle East", "Afghanistan", "Japan" ] ) Balanced continent:: europe = Continent( name="Europe", bonus=5, territories=[ "Iceland", "Scandinavia", "Ukraine", "Great Britain", "Northern Europe", "Western Europe", "Southern Europe" ] ) Easy-to-defend continent:: australia = Continent( name="Australia", bonus=2, # Small but defensible territories=["Indonesia", "New Guinea", "Western Australia", "Eastern Australia"] ) Note: Continent bonuses create risk-reward tradeoffs. Larger continents offer bigger bonuses but are harder to conquer and defend. Australia is traditionally the easiest to defend but offers the smallest bonus. """ name: str = Field( ..., min_length=1, max_length=20, description="Name of the continent", examples=[ "North America", "South America", "Europe", "Africa", "Asia", "Australia", ], ) bonus: int = Field( ..., ge=1, le=10, description="Number of bonus armies awarded for controlling all territories (1-10)", examples=[2, 3, 5, 7], # Australia=2, Europe=5, Asia=7 ) territories: list[str] = Field( default_factory=list, min_length=1, description="Names of all territories that belong to this continent", examples=[ [ "Alaska", "Northwest Territory", "Greenland", "Alberta", "Ontario", "Quebec", "Western United States", "Eastern United States", "Central America", ], ["Venezuela", "Brazil", "Peru", "Argentina"], ["Indonesia", "New Guinea", "Western Australia", "Eastern Australia"], ], )
[docs] @field_validator("territories") @classmethod def validate_territories(cls, v: list[str]) -> list[str]: """Validate territory list is not empty and contains valid names. Args: v (List[str]): Territory names to validate. Returns: List[str]: Validated territory list. Raises: ValueError: If territory list is empty or contains invalid names. """ if not v: raise ValueError("Continent must contain at least one territory") # Remove empty territory names valid_territories = [t.strip() for t in v if t.strip()] if not valid_territories: raise ValueError("Continent must contain valid territory names") return valid_territories
def __str__(self) -> str: """String representation of the continent. Returns: str: Human-readable continent description. Examples: >>> continent = Continent(name="Europe", bonus=5, territories=["..."]) >>> str(continent) "Europe (Bonus: 5)" """ return f"{self.name} (Bonus: {self.bonus})" @computed_field @property def territory_count(self) -> int: """Get the number of territories in this continent. Returns: int: Count of territories in the continent. """ return len(self.territories) @computed_field @property def bonus_per_territory(self) -> float: """Calculate bonus armies per territory in this continent. Returns: float: Bonus efficiency ratio for strategic evaluation. Note: Higher ratios indicate more efficient continents to control. Australia typically has the highest ratio (0.5), while Asia has a lower ratio (0.58) despite its large bonus. """ return self.bonus / max(1, len(self.territories))
[docs] class Player(BaseModel): """A player in the Risk game. Players are the strategic decision-makers in Risk, controlling territories, managing armies, and executing complex multi-turn plans for world domination. Each player maintains their own hand of cards and army reserves. Player management includes tracking eliminated status, which occurs when a player loses all territories, transferring their cards to the conquering player and removing them from play. Attributes: name (str): The player's identifier and display name. cards (List[Card]): Risk cards in the player's hand for trading. unplaced_armies (int): Armies available for placement but not yet deployed. eliminated (bool): Whether this player has been defeated. Examples: Active player with resources:: player = Player( name="General Patton", cards=[ Card(card_type=CardType.INFANTRY, territory_name="Alaska"), Card(card_type=CardType.CAVALRY, territory_name="Brazil"), Card(card_type=CardType.ARTILLERY, territory_name="Egypt") ], unplaced_armies=5 ) Starting player:: new_player = Player( name="Commander Lee", unplaced_armies=20 # Initial army allocation ) Eliminated player:: defeated_player = Player( name="Admiral Nelson", eliminated=True # Cards transferred to conquering player ) Note: Card accumulation is crucial for late-game army generation. Players must balance aggressive expansion (to earn cards) with defensive positioning (to avoid elimination). """ name: str = Field( ..., min_length=1, max_length=30, description="Player's name or identifier", examples=[ "General Patton", "Commander Lee", "Admiral Nelson", "player_1", "red_player", ], ) cards: list[Card] = Field( default_factory=list, description="Risk cards in the player's hand available for trading", examples=[ [], # No cards [Card(card_type=CardType.INFANTRY)], # Single card [ # Trading set Card(card_type=CardType.INFANTRY, territory_name="Alaska"), Card(card_type=CardType.CAVALRY, territory_name="Brazil"), Card(card_type=CardType.ARTILLERY, territory_name="Egypt"), ], ], ) unplaced_armies: int = Field( default=0, ge=0, le=200, description="Number of armies available for placement but not yet deployed (0-200)", examples=[0, 3, 5, 12, 25], ) eliminated: bool = Field( default=False, description="Whether this player has been eliminated from the game", examples=[False, True], )
[docs] @field_validator("unplaced_armies") @classmethod def validate_unplaced_armies(cls, v: int) -> int: """Validate unplaced army count is reasonable. Args: v (int): Unplaced army count to validate. Returns: int: Validated army count. Raises: ValueError: If army count is negative. """ if v < 0: raise ValueError("Unplaced armies cannot be negative") return v
def __str__(self) -> str: """String representation of the player. Returns: str: Human-readable player description. Examples: >>> player = Player(name="General Patton", unplaced_armies=5, ... cards=[Card(card_type=CardType.INFANTRY)]) >>> str(player) "General Patton (Active, 5 unplaced armies, 1 cards)" """ status = "Eliminated" if self.eliminated else "Active" return f"{self.name} ({status}, {self.unplaced_armies} unplaced armies, { len(self.cards) } cards)" @computed_field @property def can_trade_cards(self) -> bool: """Check if player has enough cards to make a trade. Returns: bool: True if player has 3+ cards (minimum for trading). Note: Players with 5+ cards must trade at the start of their turn. Trading with exactly 3 cards is optional but often strategic. """ return len(self.cards) >= 3 @computed_field @property def must_trade_cards(self) -> bool: """Check if player is forced to trade cards. Returns: bool: True if player has 5+ cards and must trade before placing armies. Note: This rule prevents card hoarding and maintains game flow. """ return len(self.cards) >= 5
[docs] class MoveType(str, Enum): """Types of moves available in Risk gameplay. Risk turns are structured around specific move types that correspond to different phases of play. Each move type has distinct rules, timing requirements, and strategic implications. The move type system ensures proper game flow and enables the AI to understand and plan appropriate actions for each phase. Attributes: PLACE_ARMIES: Deploy reinforcement armies to controlled territories. ATTACK: Launch military attacks against adjacent enemy territories. FORTIFY: Transfer armies between connected friendly territories. TRADE_CARDS: Exchange card sets for additional armies. Examples: Turn phase progression:: # 1. Start of turn - reinforcement phase trade_move = MoveType.TRADE_CARDS # If required/desired place_move = MoveType.PLACE_ARMIES # 2. Combat phase attack_moves = [MoveType.ATTACK, MoveType.ATTACK, ...] # 3. End of turn - reorganization fortify_move = MoveType.FORTIFY # Optional Strategic move sequencing:: # Aggressive expansion turn moves = [ MoveType.PLACE_ARMIES, # Strengthen attack position MoveType.ATTACK, # Primary assault MoveType.ATTACK, # Follow-up attack MoveType.FORTIFY # Consolidate gains ] Note: Move types correspond to Risk's traditional turn structure: reinforcement → attack → fortification. Card trading can occur at the start if the player has 3+ cards. """ PLACE_ARMIES = "place_armies" #: Deploy reinforcement armies to territories ATTACK = "attack" #: Launch attacks against enemy territories FORTIFY = "fortify" #: Move armies between friendly territories TRADE_CARDS = "trade_cards" #: Exchange card sets for bonus armies
[docs] class RiskMove(BaseModel): """A move action in the Risk game. Risk moves represent all player actions during gameplay, from army placement and attacks to strategic repositioning. Each move contains all necessary information for validation and execution. The flexible move structure accommodates Risk's varied action types while providing clear validation rules and strategic context for AI decision-making systems. Attributes: move_type (MoveType): The category of action being performed. player (str): The player executing this move. from_territory (Optional[str]): Source territory for attacks and fortifications. to_territory (Optional[str]): Target territory for all move types. armies (Optional[int]): Number of armies to deploy, attack with, or transfer. cards (Optional[List[Card]]): Cards to trade for army bonuses. attack_dice (Optional[int]): Number of dice to use in attack (1-3). Examples: Army placement move:: place_move = RiskMove( move_type=MoveType.PLACE_ARMIES, player="player_1", to_territory="Ukraine", armies=5 ) Attack move:: attack_move = RiskMove( move_type=MoveType.ATTACK, player="player_1", from_territory="Ukraine", to_territory="Middle East", armies=6, # Attacking with 6 armies (max 3 dice) attack_dice=3 ) Fortification move:: fortify_move = RiskMove( move_type=MoveType.FORTIFY, player="player_1", from_territory="Siberia", to_territory="China", armies=4 ) Card trading move:: trade_move = RiskMove( move_type=MoveType.TRADE_CARDS, player="player_1", cards=[ Card(card_type=CardType.INFANTRY), Card(card_type=CardType.CAVALRY), Card(card_type=CardType.ARTILLERY) ] ) Note: Move validation depends on game state, including territory ownership, army counts, and adjacency relationships. Invalid moves should be rejected with clear error messages. """ move_type: MoveType = Field( ..., description="The type of action being performed", examples=[ MoveType.PLACE_ARMIES, MoveType.ATTACK, MoveType.FORTIFY, MoveType.TRADE_CARDS, ], ) player: str = Field( ..., min_length=1, description="Identifier of the player making this move", examples=["player_1", "player_2", "red_player", "General Patton"], ) from_territory: str | None = Field( None, description="Source territory for attacks and fortifications", examples=["Ukraine", "Alaska", "Brazil", "Egypt", None], ) to_territory: str | None = Field( None, description="Target territory for army placement, attacks, and fortifications", examples=["Middle East", "Greenland", "Argentina", "Libya", None], ) armies: int | None = Field( None, ge=1, le=100, description="Number of armies to place, attack with, or transfer (1-100)", examples=[1, 3, 5, 8, 12, None], ) cards: list[Card] | None = Field( None, min_length=3, max_length=5, description="Cards to trade in for bonus armies (must be 3-5 cards)", examples=[ None, [ Card(card_type=CardType.INFANTRY), Card(card_type=CardType.CAVALRY), Card(card_type=CardType.ARTILLERY), ], ], ) attack_dice: int | None = Field( None, ge=1, le=3, description="Number of dice to use in attack (1-3, limited by attacking armies)", examples=[1, 2, 3, None], )
[docs] @field_validator("armies") @classmethod def validate_armies(cls, v: int | None) -> int | None: """Validate army count is positive if provided. Args: v (Optional[int]): Army count to validate. Returns: Optional[int]: Validated army count or None. Raises: ValueError: If army count is not positive. """ if v is not None and v <= 0: raise ValueError("Army count must be positive") return v
[docs] @field_validator("attack_dice") @classmethod def validate_attack_dice(cls, v: int | None) -> int | None: """Validate attack dice count is within valid range. Args: v (Optional[int]): Dice count to validate. Returns: Optional[int]: Validated dice count or None. Raises: ValueError: If dice count is not 1-3. """ if v is not None and not (1 <= v <= 3): raise ValueError("Attack dice must be between 1 and 3") return v
def __str__(self) -> str: """String representation of the move. Returns: str: Human-readable move description. Examples: >>> move = RiskMove(move_type=MoveType.ATTACK, player="player_1", ... from_territory="Ukraine", to_territory="Middle East", ... attack_dice=3) >>> str(move) "player_1 attacks from Ukraine to Middle East with 3 dice" """ if self.move_type == MoveType.PLACE_ARMIES: return f"{self.player} places {self.armies} armies on {self.to_territory}" elif self.move_type == MoveType.ATTACK: return f"{self.player} attacks from {self.from_territory} to { self.to_territory } with {self.attack_dice} dice" elif self.move_type == MoveType.FORTIFY: return f"{self.player} fortifies {self.to_territory} with { self.armies } armies from {self.from_territory}" elif self.move_type == MoveType.TRADE_CARDS: cards_str = ( ", ".join(str(card) for card in self.cards) if self.cards else "no cards" ) return f"{self.player} trades in cards: {cards_str}" return f"Unknown move: {self.move_type}" @computed_field @property def is_aggressive(self) -> bool: """Determine if this move is aggressive (attack or large army placement). Returns: bool: True if move represents aggressive action. Note: Used for AI personality and strategic analysis. """ if self.move_type == MoveType.ATTACK: return True if self.move_type == MoveType.PLACE_ARMIES and self.armies and self.armies >= 3: return True return False
[docs] class PhaseType(str, Enum): """Game phases in Risk turn structure. Risk gameplay follows a structured turn sequence with distinct phases that determine available actions and strategic timing. Understanding phase transitions is crucial for AI planning and move validation. The phase system provides clear game flow while allowing for strategic flexibility within each phase's constraints. Attributes: SETUP: Initial game setup with territory allocation and army placement. REINFORCE: Army reinforcement and card trading phase. ATTACK: Combat phase with territorial conquest attempts. FORTIFY: End-of-turn army repositioning and consolidation. GAME_OVER: Game completion with victory conditions met. Examples: Standard turn progression:: turn_phases = [ PhaseType.REINFORCE, # Get armies, place them PhaseType.ATTACK, # Optional combat PhaseType.FORTIFY # Optional repositioning ] Game lifecycle:: game_phases = [ PhaseType.SETUP, # Initial setup # ... many turns of REINFORCE → ATTACK → FORTIFY PhaseType.GAME_OVER # Victory achieved ] Note: Phase enforcement ensures proper Risk rule compliance and provides structure for AI decision-making algorithms. """ SETUP = "setup" #: Initial game setup and territory allocation REINFORCE = "reinforce" #: Army reinforcement and card trading phase ATTACK = "attack" #: Combat and territorial conquest phase FORTIFY = "fortify" #: End-of-turn army movement and positioning GAME_OVER = "game_over" #: Game completion with victory achieved
[docs] class GameStatus(str, Enum): """Overall status of the Risk game. Tracks the high-level state of the game to determine whether play should continue or if victory conditions have been met. Attributes: IN_PROGRESS: Game is actively being played. FINISHED: Game has ended with a clear victory. Examples: Active game:: status = GameStatus.IN_PROGRESS # Players continue taking turns Completed game:: status = GameStatus.FINISHED # Victory conditions met, declare winner """ IN_PROGRESS = "in_progress" #: Game is actively being played FINISHED = "finished" #: Game has ended with victory achieved
[docs] class RiskAnalysis(BaseModel): """Comprehensive analysis of a player's Risk position. Strategic analysis is crucial for AI decision-making in Risk's complex, multi-turn gameplay. This model provides structured evaluation of territorial control, military strength, and strategic positioning. The analysis system enables sophisticated AI that can evaluate long-term strategic positions beyond immediate tactical moves, considering continental bonuses, defensive positioning, and overall game progression. Attributes: player (str): The player being analyzed. controlled_continents (List[str]): Continents fully controlled by the player. controlled_territories (int): Total number of territories owned. total_armies (int): Sum of all armies across all territories. position_evaluation (str): Overall strategic assessment. recommended_move (RiskMove): Suggested optimal move. explanation (str): Detailed reasoning for the analysis and recommendation. Examples: Strong position analysis:: analysis = RiskAnalysis( player="player_1", controlled_continents=["Australia", "South America"], controlled_territories=18, total_armies=45, position_evaluation="winning", recommended_move=RiskMove( move_type=MoveType.ATTACK, player="player_1", from_territory="Brazil", to_territory="North Africa", attack_dice=3 ), explanation="Player controls two continents providing 5 bonus armies per turn. Should continue aggressive expansion into Africa while maintaining defensive positions." ) Defensive position analysis:: analysis = RiskAnalysis( player="player_2", controlled_continents=[], controlled_territories=8, total_armies=12, position_evaluation="losing", recommended_move=RiskMove( move_type=MoveType.FORTIFY, player="player_2", from_territory="Greenland", to_territory="Quebec", armies=3 ), explanation="Player is behind in territory count and lacks continental bonuses. Should consolidate forces and defend key chokepoints to survive." ) Note: Analysis quality directly impacts AI performance. Sophisticated evaluation considers not just current position but also trajectory, opponent threats, and multi-turn strategic opportunities. """ player: str = Field( ..., description="Identifier of the player being analyzed", examples=["player_1", "player_2", "red_player", "General Patton"], ) controlled_continents: list[str] = Field( default_factory=list, description="Names of continents fully controlled by this player", examples=[ [], # No continents ["Australia"], # Single continent ["Australia", "South America", "Europe"], # Multiple continents ], ) controlled_territories: int = Field( ..., ge=0, le=42, # Maximum territories on standard board description="Total number of territories controlled by this player (0-42)", examples=[5, 12, 18, 25, 35], ) total_armies: int = Field( ..., ge=0, le=500, description="Sum of all armies across all controlled territories (0-500)", examples=[8, 25, 45, 78, 120], ) position_evaluation: str = Field( ..., description="Overall strategic assessment of the player's position", examples=[ "winning", "favorable", "neutral", "unfavorable", "losing", "critical", ], ) recommended_move: RiskMove = Field( ..., description="Strategically optimal move for the current position" ) explanation: str = Field( ..., min_length=10, max_length=1000, description="Detailed reasoning behind the analysis and move recommendation", examples=[ "Player controls Australia providing defensive bonus. Should expand into Asia while maintaining territorial integrity.", "Critical position requires immediate consolidation. Recommend fortifying chokepoints and avoiding risky attacks.", "Strong continental control provides sustained army advantage. Aggressive expansion recommended to capitalize on momentum.", ], )
[docs] @field_validator("position_evaluation") @classmethod def validate_position_evaluation(cls, v: str) -> str: """Validate position evaluation uses standard terminology. Args: v (str): Position evaluation to validate. Returns: str: Validated evaluation string. Raises: ValueError: If evaluation is not recognized. """ valid_evaluations = { "winning", "favorable", "neutral", "unfavorable", "losing", "critical", } evaluation = v.strip().lower() if evaluation not in valid_evaluations: raise ValueError( f"Position evaluation must be one of: {', '.join(valid_evaluations)}" ) return evaluation
def __str__(self) -> str: """String representation of the analysis. Returns: str: Comprehensive analysis summary. Examples: >>> analysis = RiskAnalysis(player="player_1", controlled_continents=["Australia"], ... controlled_territories=12, total_armies=25, ... position_evaluation="favorable", ...) >>> str(analysis) "Analysis for player_1: Controlled continents: Australia Controlled territories: 12 Total armies: 25 Position evaluation: favorable ..." """ continents_str = ( ", ".join(self.controlled_continents) if self.controlled_continents else "None" ) return ( f"Analysis for {self.player}:\n" f"Controlled continents: {continents_str}\n" f"Controlled territories: {self.controlled_territories}\n" f"Total armies: {self.total_armies}\n" f"Position evaluation: {self.position_evaluation}\n" f"Recommended move: {self.recommended_move}\n" f"Explanation: {self.explanation}" ) @computed_field @property def continent_bonus(self) -> int: """Calculate total bonus armies from controlled continents. Returns: int: Total bonus armies per turn from continental control. Note: This is a simplified calculation. Real implementation would need access to continent definitions with their bonus values. """ # Simplified continent bonuses (would normally reference game data) continent_bonuses = { "Australia": 2, "South America": 2, "Africa": 3, "North America": 5, "Europe": 5, "Asia": 7, } return sum( continent_bonuses.get(continent, 0) for continent in self.controlled_continents ) @computed_field @property def average_armies_per_territory(self) -> float: """Calculate average army strength per territory. Returns: float: Average armies per territory for strategic assessment. Note: Higher ratios indicate stronger defensive positions or preparation for major offensives. """ return self.total_armies / max(1, self.controlled_territories)