"""Game state machine management. Provides a base state manager that game implementations extend to detect and track game states (menu, in-game, inventory, etc.). """ from typing import Optional, Callable, Dict, Any from enum import Enum, auto from dataclasses import dataclass import logging import time import numpy as np logger = logging.getLogger(__name__) class BaseGameState(Enum): """Base states common to most games.""" UNKNOWN = auto() LOADING = auto() MAIN_MENU = auto() CHARACTER_SELECT = auto() IN_GAME = auto() INVENTORY = auto() DEAD = auto() DISCONNECTED = auto() @dataclass class StateTransition: """Records a state transition.""" from_state: BaseGameState to_state: BaseGameState timestamp: float metadata: Dict[str, Any] = None class GameStateManager: """Base class for game state detection and management. Game implementations should subclass this and implement detect_state() with game-specific screen analysis. """ def __init__(self): self._current_state: BaseGameState = BaseGameState.UNKNOWN self._previous_state: BaseGameState = BaseGameState.UNKNOWN self._state_enter_time: float = time.time() self._history: list[StateTransition] = [] self._callbacks: Dict[BaseGameState, list[Callable]] = {} @property def current_state(self) -> BaseGameState: return self._current_state @property def previous_state(self) -> BaseGameState: return self._previous_state @property def time_in_state(self) -> float: """Seconds spent in current state.""" return time.time() - self._state_enter_time def detect_state(self, screen: np.ndarray) -> BaseGameState: """Detect current game state from screenshot. Must be overridden by game implementations. """ raise NotImplementedError("Subclasses must implement detect_state()") def update(self, screen: np.ndarray) -> BaseGameState: """Update state from current screen. Triggers callbacks on change.""" new_state = self.detect_state(screen) if new_state != self._current_state: transition = StateTransition( from_state=self._current_state, to_state=new_state, timestamp=time.time(), ) self._history.append(transition) logger.info(f"State: {self._current_state.name} → {new_state.name}") self._previous_state = self._current_state self._current_state = new_state self._state_enter_time = time.time() # Fire callbacks for cb in self._callbacks.get(new_state, []): try: cb(transition) except Exception as e: logger.error(f"State callback error: {e}") return self._current_state def on_state(self, state: BaseGameState, callback: Callable) -> None: """Register a callback for when entering a state.""" self._callbacks.setdefault(state, []).append(callback) def is_state(self, state: BaseGameState) -> bool: return self._current_state == state