iso-bot/engine/state/manager.py

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