""" Game engine for Cluedo Custom. Handles game logic, card distribution, turn management, and game rules. """ import random from typing import List, Optional, Tuple from backend.models import Game, Card, CardType, Solution, GameStatus, Player from datetime import datetime # Default character names (fallback if no custom suspects provided) DEFAULT_CHARACTERS = [ "Miss Scarlett", "Colonel Mustard", "Mrs. White", "Reverend Green", "Mrs. Peacock", "Professor Plum" ] # Default weapon names (fallback if no custom weapons provided) DEFAULT_WEAPONS = [ "Candlestick", "Knife", "Lead Pipe", "Revolver", "Rope", "Wrench" ] class GameEngine: """Core game logic engine for Cluedo Custom.""" @staticmethod def initialize_game(game: Game) -> Game: """ Initialize a game with cards and solution. Distributes cards among players after setting aside the solution. """ # Use custom suspects or defaults suspects = game.custom_suspects if game.custom_suspects else DEFAULT_CHARACTERS weapons = game.custom_weapons if game.custom_weapons else DEFAULT_WEAPONS # Create character cards game.characters = [ Card(name=name, card_type=CardType.CHARACTER) for name in suspects ] # Create weapon cards game.weapons = [ Card(name=name, card_type=CardType.WEAPON) for name in weapons ] # Create room cards game.room_cards = [ Card(name=room, card_type=CardType.ROOM) for room in game.rooms ] # Select solution (one of each type) solution_character = random.choice(game.characters) solution_weapon = random.choice(game.weapons) solution_room = random.choice(game.room_cards) game.solution = Solution( character=solution_character, weapon=solution_weapon, room=solution_room ) # Remaining cards to distribute remaining_cards = [] remaining_cards.extend([c for c in game.characters if c.name != solution_character.name]) remaining_cards.extend([w for w in game.weapons if w.name != solution_weapon.name]) remaining_cards.extend([r for r in game.room_cards if r.name != solution_room.name]) # Shuffle and distribute random.shuffle(remaining_cards) GameEngine._distribute_cards(game, remaining_cards) # Set game status to in progress game.status = GameStatus.IN_PROGRESS game.current_player_index = 0 return game @staticmethod def _distribute_cards(game: Game, cards: List[Card]): """ Distribute cards evenly among all players. """ num_players = len(game.players) if num_players == 0: return for i, card in enumerate(cards): player_index = i % num_players game.players[player_index].cards.append(card) @staticmethod def check_suggestion( game: Game, player_id: str, character: str, weapon: str, room: str ) -> Tuple[bool, Optional[str], Optional[Card]]: """ Process a player's suggestion. Returns (can_disprove, disprover_name, card_shown). Starting with the next player clockwise, check if anyone can disprove the suggestion by showing one matching card. """ player_index = next( (i for i, p in enumerate(game.players) if p.id == player_id), None ) if player_index is None: return False, None, None num_players = len(game.players) # Check other players clockwise for offset in range(1, num_players): check_index = (player_index + offset) % num_players checker = game.players[check_index] # Find matching cards matching_cards = [ card for card in checker.cards if card.name in [character, weapon, room] ] if matching_cards: # Show one random matching card card_to_show = random.choice(matching_cards) return True, checker.name, card_to_show # No one can disprove return False, None, None @staticmethod def check_accusation( game: Game, character: str, weapon: str, room: str ) -> bool: """ Check if an accusation is correct. Returns True if the accusation matches the solution. """ if not game.solution: return False return ( game.solution.character.name == character and game.solution.weapon.name == weapon and game.solution.room.name == room ) @staticmethod def process_accusation( game: Game, player_id: str, character: str, weapon: str, room: str ) -> Tuple[bool, str]: """ Process a player's accusation. Returns (is_correct, message). If correct, player wins. If incorrect, player is eliminated from the game. """ player = next((p for p in game.players if p.id == player_id), None) if not player: return False, "Player not found" is_correct = GameEngine.check_accusation(game, character, weapon, room) if is_correct: game.winner = player.name game.status = GameStatus.FINISHED return True, f"{player.name} wins! The accusation was correct." else: # Eliminate player player.is_active = False # Check if only one or no players remain active active_players = [p for p in game.players if p.is_active] if len(active_players) <= 1: game.status = GameStatus.FINISHED if active_players: game.winner = active_players[0].name return False, f"{player.name}'s accusation was wrong. {game.winner} wins by elimination!" else: return False, "All players eliminated. Game over!" return False, f"{player.name}'s accusation was wrong and is eliminated from the game." @staticmethod def add_turn_record( game: Game, player_id: str, action: str, details: Optional[str] = None, ai_comment: Optional[str] = None ): """Add a turn record to the game history.""" from backend.models import Turn player = next((p for p in game.players if p.id == player_id), None) if not player: return turn = Turn( player_id=player_id, player_name=player.name, action=action, details=details, ai_comment=ai_comment, timestamp=datetime.now().isoformat() ) game.turns.append(turn) @staticmethod def get_player_card_names(player: Player) -> List[str]: """Get list of card names for a player.""" return [card.name for card in player.cards] @staticmethod def can_player_act(game: Game, player_id: str) -> bool: """Check if it's the player's turn and they can act.""" current_player = game.get_current_player() if not current_player: return False return ( current_player.id == player_id and current_player.is_active and game.status == GameStatus.IN_PROGRESS ) @staticmethod def roll_dice() -> int: """Roll a single die (1-6).""" return random.randint(1, 6) @staticmethod def move_player(game: Game, player_id: str, dice_roll: int) -> Tuple[bool, str, int]: """ Move a player to a room based on dice roll. Dice value maps directly to room index (1→room 0, 2→room 1, etc.) Returns (success, message, new_room_index). """ player = next((p for p in game.players if p.id == player_id), None) if not player: return False, "Joueur introuvable", -1 num_rooms = len(game.rooms) if num_rooms == 0: return False, "Pas de pièces disponibles", -1 # Map dice roll to room (1-6 maps to rooms, if more rooms, use modulo) new_room_index = (dice_roll - 1) % num_rooms old_room = game.rooms[player.current_room_index] if player.current_room_index < num_rooms else "???" new_room = game.rooms[new_room_index] player.current_room_index = new_room_index message = f"🎲 Dé: {dice_roll} → {new_room}" return True, message, new_room_index @staticmethod def can_make_suggestion(game: Game, player_id: str, room: str) -> Tuple[bool, str]: """ Check if a player can make a suggestion. Players can only suggest in the room they're currently in. """ player = next((p for p in game.players if p.id == player_id), None) if not player: return False, "Joueur introuvable" if not player.has_rolled: return False, "Tu dois d'abord lancer les dés avant de faire une suggestion !" current_room = game.rooms[player.current_room_index] if current_room != room: return False, f"Tu dois être dans {room} pour faire cette suggestion ! Tu es actuellement dans {current_room}." return True, ""