Source code for haive.games.mastermind.state_manager

"""State manager for the Mastermind game.

This module defines the state manager for the Mastermind game, which manages the state
of the game and provides methods for initializing, updating, and analyzing the game
state.

"""

import itertools
import random

from haive.games.framework.base.state_manager import GameStateManager
from haive.games.mastermind.models import (
    ColorCode,
    MastermindAnalysis,
    MastermindFeedback,
    MastermindGuess,
)
from haive.games.mastermind.state import MastermindState


[docs] class MastermindStateManager(GameStateManager[MastermindState]): """Manager for Mastermind game state. This class provides methods for initializing, updating, and analyzing the game state. """ VALID_COLORS = ["red", "blue", "green", "yellow", "purple", "orange"]
[docs] @classmethod def initialize(cls, **kwargs) -> MastermindState: """Initialize a new Mastermind game. Args: **kwargs: Keyword arguments for game initialization. codemaker: Player who creates the code (player1 or player2). Default is player1. colors: List of valid colors. Default is standard 6 colors. code_length: Length of the secret code. Default is 4. max_turns: Maximum number of turns. Default is 10. secret_code: Optional predetermined secret code (List[str] or ColorCode). Returns: MastermindState: A new Mastermind game state. """ codemaker = kwargs.get("codemaker", "player1") codebreaker = "player2" if codemaker == "player1" else "player1" colors = kwargs.get("colors", cls.VALID_COLORS) code_length = kwargs.get("code_length", 4) max_turns = kwargs.get("max_turns", 10) # Unwrap or generate secret code secret_code = kwargs.get("secret_code") if isinstance(secret_code, ColorCode): secret_code = secret_code.code elif isinstance(secret_code, dict) and "code" in secret_code: secret_code = secret_code["code"] elif not secret_code: secret_code = random.choices(colors, k=code_length) return MastermindState( secret_code=secret_code, guesses=[], feedback=[], turn=codebreaker, # Codebreaker starts codemaker=codemaker, max_turns=max_turns, game_status="ongoing", )
[docs] @classmethod def apply_move( cls, state: MastermindState, move: MastermindGuess ) -> MastermindState: """Apply a guess to the current state and return the new state. Args: state: The current game state. move: The guess to apply. Returns: MastermindState: A new game state after applying the guess. Raises: ValueError: If the move is invalid. """ # Validate player's turn if move.player != state.turn: raise ValueError(f"Not {move.player}'s turn") # Validate turn limit if len(state.guesses) >= state.max_turns: raise ValueError("Maximum number of turns reached") # Create a new state new_state = state.model_copy() # Add the guess new_state.guesses.append(move) # Calculate feedback feedback = cls._calculate_feedback(new_state.secret_code, move.colors) new_state.feedback.append(feedback) # Check if the game is over if feedback.is_winning(): # Codebreaker wins new_state.game_status = f"{move.player}_win" new_state.winner = move.player elif len(new_state.guesses) >= new_state.max_turns: # Codemaker wins if max turns reached without correct guess new_state.game_status = f"{new_state.codemaker}_win" new_state.winner = new_state.codemaker return new_state
@classmethod def _calculate_feedback( cls, secret_code: list[str], guess: list[str] ) -> MastermindFeedback: """Calculate feedback for a guess compared to the secret code. Args: secret_code: The secret code to guess. guess: The player's guess. Returns: MastermindFeedback: Feedback with correct position and color counts. """ # Count exact matches (correct position and color) correct_position = sum( 1 for s, g in zip(secret_code, guess, strict=False) if s == g ) # Count color matches (regardless of position) # We need to handle duplicates carefully secret_counts = {} guess_counts = {} for color in secret_code: secret_counts[color] = secret_counts.get(color, 0) + 1 for color in guess: guess_counts[color] = guess_counts.get(color, 0) + 1 # Count colors that appear in both lists (take minimum count for each # color) correct_color_total = sum( min(secret_counts.get(color, 0), guess_counts.get(color, 0)) for color in set(secret_code + guess) ) # Subtract exact matches to get only color matches correct_color = correct_color_total - correct_position return MastermindFeedback( correct_position=correct_position, correct_color=correct_color )
[docs] @classmethod def check_game_status(cls, state: MastermindState) -> MastermindState: """Check and update the game status. For Mastermind, this is handled in apply_move, so this method just returns the state. Args: state: The current game state. Returns: MastermindState: The game state (unchanged). """ return state
[docs] @classmethod def get_winner(cls, state: MastermindState) -> str | None: """Get the winner of the game, if any. Args: state: The current game state. Returns: Optional[str]: The winner, or None if the game is ongoing. """ return state.winner
[docs] @classmethod def add_analysis( cls, state: MastermindState, player: str, analysis: MastermindAnalysis ) -> MastermindState: """Add an analysis to the state. Args: state: The current game state. player: The player who performed the analysis. analysis: The analysis to add. Returns: MastermindState: Updated state with the analysis added. """ new_state = state.model_copy() if player == "player1": new_state.player1_analysis.append(analysis) else: new_state.player2_analysis.append(analysis) return new_state
[docs] @classmethod def get_possible_codes(cls, state: MastermindState) -> set[tuple[str, ...]]: """Get all possible secret codes that are consistent with all guesses and. feedback so far. This is computationally expensive for a full game, so it's limited to use for analysis. Args: state: The current game state. Returns: Set[Tuple[str, ...]]: Set of possible codes as tuples. """ # Start with all possible codes colors = cls.VALID_COLORS code_length = 4 # Standard Mastermind code length # Generate all possible codes (expensive, but acceptable for analysis) all_codes = set(itertools.product(colors, repeat=code_length)) # Filter based on previous guesses and feedback for guess, feedback in zip(state.guesses, state.feedback, strict=False): guess_tuple = tuple(guess.colors) all_codes = { code for code in all_codes if cls._is_consistent_with_feedback(code, guess_tuple, feedback) } return all_codes
@classmethod def _is_consistent_with_feedback( cls, code: tuple[str, ...], guess: tuple[str, ...], feedback: MastermindFeedback, ) -> bool: """Check if a potential code is consistent with a guess and its feedback. Args: code: Potential secret code. guess: A previous guess. feedback: Feedback for the guess. Returns: bool: True if the code is consistent with the guess and feedback. """ # Calculate what the feedback would be if this code were the secret calculated_feedback = cls._calculate_feedback(list(code), list(guess)) # Check if it matches the actual feedback return ( calculated_feedback.correct_position == feedback.correct_position and calculated_feedback.correct_color == feedback.correct_color )