105 lines
3.2 KiB
Python
105 lines
3.2 KiB
Python
"""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
|