"""State manager for the Risk game.This module defines the RiskStateManager class that manages game state transitions, ruleenforcement, and game progression."""importrandomfrompydanticimportBaseModel,Fieldfromhaive.games.risk.configimportRiskConfigfromhaive.games.risk.modelsimportCardType,GameStatus,MoveType,PhaseType,RiskMovefromhaive.games.risk.stateimportRiskState
[docs]classRiskStateManager(BaseModel):"""Manages state transitions and rule enforcement for the Risk game. This class is responsible for applying moves to the game state, enforcing game rules, and managing game progression through different phases. Attributes: state: The current game state. config: Configuration settings for the game. move_history: History of all moves made in the game. """state:RiskStateconfig:RiskConfig=Field(default_factory=RiskConfig.classic)move_history:list[RiskMove]=Field(default_factory=list)
[docs]@classmethoddefinitialize(cls,player_names:list[str],config:RiskConfig|None=None)->"RiskStateManager":"""Initialize a new Risk game state manager. Args: player_names: List of player names. config: Optional configuration for the game. If not provided, classic Risk rules will be used. Returns: A new RiskStateManager with initialized state. Raises: ValueError: If the number of players is invalid. """# Use default config if none providedifconfigisNone:config=RiskConfig.classic()# Validate player countiflen(player_names)<2orlen(player_names)>6:raiseValueError("Risk requires 2-6 players")# Initialize statestate=RiskState.initialize(player_names)returncls(state=state,config=config)
[docs]defapply_move(self,move:RiskMove)->RiskState:"""Apply a move to the current game state. Args: move: The move to apply. Returns: The updated game state after applying the move. Raises: ValueError: If the move is invalid or violates game rules. """# Validate moveself._validate_move(move)# Apply the move based on its typeifmove.move_type==MoveType.PLACE_ARMIES:self._apply_place_armies(move)elifmove.move_type==MoveType.ATTACK:self._apply_attack(move)elifmove.move_type==MoveType.FORTIFY:self._apply_fortify(move)elifmove.move_type==MoveType.TRADE_CARDS:self._apply_trade_cards(move)# Add move to historyself.move_history.append(move)# Check if game is overself._check_game_over()returnself.state
def_validate_move(self,move:RiskMove)->None:"""Validate that a move is legal according to the game rules. Args: move: The move to validate. Raises: ValueError: If the move is invalid. """# Check if it's the player's turnifmove.player!=self.state.current_player:raiseValueError(f"Not {move.player}'s turn")# Check if player is eliminatedifself.state.players[move.player].eliminated:raiseValueError(f"Player {move.player} is eliminated")# Validate based on move typeifmove.move_type==MoveType.PLACE_ARMIES:self._validate_place_armies(move)elifmove.move_type==MoveType.ATTACK:self._validate_attack(move)elifmove.move_type==MoveType.FORTIFY:self._validate_fortify(move)elifmove.move_type==MoveType.TRADE_CARDS:self._validate_trade_cards(move)def_validate_place_armies(self,move:RiskMove)->None:"""Validate a place armies move. Args: move: The move to validate. Raises: ValueError: If the move is invalid. """# Check if to_territory is specifiedifnotmove.to_territory:raiseValueError("Territory to place armies on not specified")# Check if territory existsifmove.to_territorynotinself.state.territories:raiseValueError(f"Territory {move.to_territory} does not exist")# Check if player controls the territoryifself.state.territories[move.to_territory].owner!=move.player:raiseValueError(f"Player {move.player} does not control {move.to_territory}")# Check if player has enough unplaced armiesifmove.armiesisNoneormove.armies<=0:raiseValueError("Must place at least 1 army")ifself.state.players[move.player].unplaced_armies<move.armies:raiseValueError(f"Player {move.player} only has {self.state.players[move.player].unplaced_armies} unplaced armies")def_validate_attack(self,move:RiskMove)->None:"""Validate an attack move. Args: move: The move to validate. Raises: ValueError: If the move is invalid. """# Check if from_territory and to_territory are specifiedifnotmove.from_territory:raiseValueError("Territory to attack from not specified")ifnotmove.to_territory:raiseValueError("Territory to attack not specified")# Check if territories existifmove.from_territorynotinself.state.territories:raiseValueError(f"Territory {move.from_territory} does not exist")ifmove.to_territorynotinself.state.territories:raiseValueError(f"Territory {move.to_territory} does not exist")# Check if player controls the from_territoryifself.state.territories[move.from_territory].owner!=move.player:raiseValueError(f"Player {move.player} does not control {move.from_territory}")# Check if player does not control the to_territoryifself.state.territories[move.to_territory].owner==move.player:raiseValueError(f"Cannot attack your own territory {move.to_territory}")# Check if territories are adjacentif(move.to_territorynotinself.state.territories[move.from_territory].adjacent):raiseValueError(f"{move.to_territory} is not adjacent to {move.from_territory}")# Check if there are enough armies to attackifself.state.territories[move.from_territory].armies<2:raiseValueError(f"Need at least 2 armies in {move.from_territory} to attack")# Validate attack diceifnotmove.attack_diceormove.attack_dice<1:raiseValueError("Must attack with at least 1 die")ifmove.attack_dice>min(3,self.state.territories[move.from_territory].armies-1):raiseValueError(f"Cannot attack with {move.attack_dice} dice, maximum is {min(3,self.state.territories[move.from_territory].armies-1)}")def_validate_fortify(self,move:RiskMove)->None:"""Validate a fortify move. Args: move: The move to validate. Raises: ValueError: If the move is invalid. """# Skip validation for empty fortify (end turn)ifnotmove.from_territoryandnotmove.to_territory:return# Check if from_territory and to_territory are specifiedifnotmove.from_territory:raiseValueError("Territory to fortify from not specified")ifnotmove.to_territory:raiseValueError("Territory to fortify not specified")# Check if territories existifmove.from_territorynotinself.state.territories:raiseValueError(f"Territory {move.from_territory} does not exist")ifmove.to_territorynotinself.state.territories:raiseValueError(f"Territory {move.to_territory} does not exist")# Check if player controls both territoriesifself.state.territories[move.from_territory].owner!=move.player:raiseValueError(f"Player {move.player} does not control {move.from_territory}")ifself.state.territories[move.to_territory].owner!=move.player:raiseValueError(f"Player {move.player} does not control {move.to_territory}")# Check if territories are adjacentif(move.to_territorynotinself.state.territories[move.from_territory].adjacent):raiseValueError(f"{move.to_territory} is not adjacent to {move.from_territory}")# Check if there are enough armies to fortifyifnotmove.armiesormove.armies<=0:raiseValueError("Must fortify with at least 1 army")ifself.state.territories[move.from_territory].armies<=move.armies:raiseValueError(f"Must leave at least 1 army in {move.from_territory}")def_validate_trade_cards(self,move:RiskMove)->None:"""Validate a trade cards move. Args: move: The move to validate. Raises: ValueError: If the move is invalid. """# Check if cards are specifiedifnotmove.cardsorlen(move.cards)!=3:raiseValueError("Must trade exactly 3 cards")# Check if player has the cardsplayer=self.state.players[move.player]forcardinmove.cards:ifcardnotinplayer.cards:raiseValueError(f"Player {move.player} does not have {card}")# Check if the set is valid (3 of a kind, 1 of each, or includes a# wild)card_types=[card.card_typeforcardinmove.cards]has_wild=CardType.WILDincard_typesifhas_wild:# Any set with a wild card is validpasseliflen(set(card_types))==1:# 3 of a kind (all infantry, all cavalry, or all artillery)passeliflen(set(card_types))==3:# 1 of each (infantry, cavalry, artillery)passelse:raiseValueError("Invalid card set: must be 3 of a kind, 1 of each type, or include a wild card")def_apply_place_armies(self,move:RiskMove)->None:"""Apply a place armies move to the game state. Args: move: The place armies move to apply. """# Add armies to territoryterritory=self.state.territories[move.to_territory]territory.armies+=move.armies# Reduce player's unplaced armiesplayer=self.state.players[move.player]player.unplaced_armies-=move.armies# If all armies are placed and in setup phase, advance to next playerifself.state.phase==PhaseType.SETUPandplayer.unplaced_armies==0:self._advance_to_next_player()# If all players have placed initial armies, transition to# reinforcement phaseifself.state.phase==PhaseType.SETUPandall(p.unplaced_armies==0forpinself.state.players.values()):self.state.phase=PhaseType.REINFORCEdef_apply_attack(self,move:RiskMove)->None:"""Apply an attack move to the game state. Args: move: The attack move to apply. """# Simulate dice rollsattacker_territory=self.state.territories[move.from_territory]defender_territory=self.state.territories[move.to_territory]attacker_dice=move.attack_dicedefender_dice=min(2,defender_territory.armies)# Roll attacker diceattacker_rolls=[random.randint(1,6)for_inrange(attacker_dice)]attacker_rolls.sort(reverse=True)# Roll defender dicedefender_rolls=[random.randint(1,6)for_inrange(defender_dice)]defender_rolls.sort(reverse=True)# Compare dice and calculate casualtiesattacker_casualties=0defender_casualties=0foriinrange(min(len(attacker_rolls),len(defender_rolls))):ifattacker_rolls[i]>defender_rolls[i]:defender_casualties+=1else:attacker_casualties+=1# Apply casualtiesattacker_territory.armies-=attacker_casualtiesdefender_territory.armies-=defender_casualties# Check if defender is defeatedifdefender_territory.armies==0:# Transfer ownershipdefender_player=defender_territory.ownerdefender_territory.owner=move.player# Move attacking armiesmin_armies=1# At least 1 army must be movedmax_armies=attacker_territory.armies-1# At least 1 army must remainarmies_to_move=min(min_armies,max_armies)# Default to minimumattacker_territory.armies-=armies_to_movedefender_territory.armies=armies_to_move# Mark that attacker captured a territory this turnself.state.attacker_captured_territory=True# Check if defender is eliminateddefender_territories=self.state.get_controlled_territories(defender_player)ifnotdefender_territories:self.state.players[defender_player].eliminated=True# Transfer cards from defender to attackerdefender_cards=self.state.players[defender_player].cardsself.state.players[move.player].cards.extend(defender_cards)self.state.players[defender_player].cards=[]# Force attacker to trade cards if they have too manyiflen(self.state.players[move.player].cards)>=5:# This would trigger a card trade prompt in a real# implementationpassdef_apply_fortify(self,move:RiskMove)->None:"""Apply a fortify move to the game state. Args: move: The fortify move to apply. """# Skip for empty fortify (end turn)ifnotmove.from_territoryandnotmove.to_territory:self._end_turn()return# Move armiesfrom_territory=self.state.territories[move.from_territory]to_territory=self.state.territories[move.to_territory]from_territory.armies-=move.armiesto_territory.armies+=move.armies# End turn after fortificationself._end_turn()def_apply_trade_cards(self,move:RiskMove)->None:"""Apply a trade cards move to the game state. Args: move: The trade cards move to apply. """# Calculate armies receivedarmies_received=self.state.next_card_set_value# Update next card set value if using escalating valuesifself.config.escalating_card_values:ifself.state.next_card_set_value<12:self.state.next_card_set_value+=2else:self.state.next_card_set_value+=5# Give armies to playerplayer=self.state.players[move.player]player.unplaced_armies+=armies_received# Remove cards from player's handforcardinmove.cards:player.cards.remove(card)# Add cards back to deckself.state.deck.extend(move.cards)random.shuffle(self.state.deck)def_end_turn(self)->None:"""End the current player's turn and prepare for the next player."""# Give card if territory was capturedifself.state.attacker_captured_territory:ifself.state.deck:card=self.state.deck.pop(0)self.state.players[self.state.current_player].cards.append(card)self.state.attacker_captured_territory=False# Move to next playerself._advance_to_next_player()# Calculate reinforcements for the new playerself._calculate_reinforcements()# Set phase to reinforceself.state.phase=PhaseType.REINFORCEdef_advance_to_next_player(self)->None:"""Advance to the next active player."""player_names=list(self.state.players.keys())current_index=player_names.index(self.state.current_player)# Find next non-eliminated playerforiinrange(1,len(player_names)+1):next_index=(current_index+i)%len(player_names)next_player=player_names[next_index]ifnotself.state.players[next_player].eliminated:self.state.current_player=next_playerbreak# Increment turn number when returning to first playerifnext_index<current_index:self.state.turn_number+=1def_calculate_reinforcements(self)->None:"""Calculate reinforcements for the current player."""player=self.state.players[self.state.current_player]# Base reinforcements (minimum 3)territories=self.state.get_controlled_territories(player.name)base_reinforcements=max(3,len(territories)//3)# Continent bonusescontinent_bonus=0forcontinentinself.state.get_controlled_continents(player.name):continent_bonus+=continent.bonus# Total reinforcementstotal_reinforcements=base_reinforcements+continent_bonus# Add to player's unplaced armiesplayer.unplaced_armies+=total_reinforcementsdef_check_game_over(self)->None:"""Check if the game is over."""active_players=[p.nameforpinself.state.players.values()ifnotp.eliminated]iflen(active_players)==1:self.state.game_status=GameStatus.FINISHEDself.state.phase=PhaseType.GAME_OVER