""" DungeonMaster AI - Pacing Controller Controls response verbosity and style based on game context. Ensures appropriate pacing for different gameplay situations. """ from __future__ import annotations from typing import TYPE_CHECKING from .models import GameMode, PacingStyle, SpecialMoment if TYPE_CHECKING: from src.game.game_state import GameState # ============================================================================= # Pacing Instructions # ============================================================================= PACING_INSTRUCTIONS: dict[PacingStyle, str] = { PacingStyle.VERBOSE: """ ## Current Pacing: VERBOSE This is a moment for world-building and immersion. Use rich, evocative descriptions. - Describe the environment with sensory details (sight, sound, smell, feel) - Paint a vivid picture of the scene - Use 3-5 sentences for descriptions - Take time to set the atmosphere - Let the player absorb the moment """, PacingStyle.STANDARD: """ ## Current Pacing: STANDARD Balance description with action. Keep things moving but engaging. - Use 2-3 sentences for responses - Describe key details without overwhelming - Focus on what's immediately relevant - End with a clear prompt for player action """, PacingStyle.QUICK: """ ## Current Pacing: QUICK Combat or action sequence. Keep it fast and punchy. - Use 1-2 sentences per action - Focus on immediate results - Maintain tension and momentum - Clear, direct descriptions - No lengthy exposition """, PacingStyle.DRAMATIC: """ ## Current Pacing: DRAMATIC Critical moment requiring impact. Short, powerful statements. - Use short, punchy sentences - Build tension with pauses (...) - Make every word count - Emphasize the stakes - Let the moment breathe """, } # ============================================================================= # PacingController # ============================================================================= class PacingController: """ Controls response verbosity based on game context. Determines appropriate pacing style and provides instructions to inject into the DM agent's system prompt. """ # Keywords that suggest quick pacing QUICK_KEYWORDS: frozenset[str] = frozenset([ "attack", "hit", "strike", "shoot", "cast", "dodge", "run", "flee", "block", "parry", ]) # Keywords that suggest verbose pacing VERBOSE_KEYWORDS: frozenset[str] = frozenset([ "look", "examine", "describe", "search", "explore", "enter", "arrive", "approach", ]) # Keywords that suggest social pacing SOCIAL_KEYWORDS: frozenset[str] = frozenset([ "talk", "speak", "ask", "tell", "persuade", "convince", "negotiate", "bribe", ]) def __init__(self) -> None: """Initialize the pacing controller.""" self._last_pacing = PacingStyle.STANDARD self._location_changes = 0 self._combat_turn_count = 0 def determine_pacing( self, game_mode: GameMode, player_input: str, game_state: GameState | None = None, special_moment: SpecialMoment | None = None, ) -> PacingStyle: """ Determine appropriate pacing style. Args: game_mode: Current game mode. player_input: The player's input. game_state: Current game state. special_moment: Special moment if any. Returns: Appropriate PacingStyle. """ # Special moments always get dramatic pacing if special_moment is not None: return PacingStyle.DRAMATIC # Combat mode generally uses quick pacing if game_mode == GameMode.COMBAT: self._combat_turn_count += 1 # First combat turn or every 3rd turn can be slightly more descriptive if self._combat_turn_count == 1: return PacingStyle.STANDARD return PacingStyle.QUICK # Reset combat counter when not in combat if game_mode != GameMode.COMBAT: self._combat_turn_count = 0 # Check player input for pacing hints input_lower = player_input.lower() # Quick keywords override if any(kw in input_lower for kw in self.QUICK_KEYWORDS): return PacingStyle.QUICK # Verbose keywords for exploration if any(kw in input_lower for kw in self.VERBOSE_KEYWORDS): return PacingStyle.VERBOSE # Social interaction is standard pacing if any(kw in input_lower for kw in self.SOCIAL_KEYWORDS): return PacingStyle.STANDARD # Check for location changes if game_state: if self._is_new_location(game_state): self._location_changes += 1 return PacingStyle.VERBOSE # Default based on game mode mode_defaults: dict[GameMode, PacingStyle] = { GameMode.EXPLORATION: PacingStyle.STANDARD, GameMode.COMBAT: PacingStyle.QUICK, GameMode.SOCIAL: PacingStyle.STANDARD, GameMode.NARRATIVE: PacingStyle.VERBOSE, } return mode_defaults.get(game_mode, PacingStyle.STANDARD) def _is_new_location(self, game_state: GameState) -> bool: """ Check if player just entered a new location. Args: game_state: Current game state. Returns: True if location recently changed. """ # Check recent events for location change for event in reversed(game_state.recent_events[-3:]): if event.get("type") == "movement": return True return False def get_pacing_instruction(self, pacing: PacingStyle) -> str: """ Get instruction text for a pacing style. Args: pacing: The pacing style. Returns: Instruction text to inject into system prompt. """ return PACING_INSTRUCTIONS.get(pacing, PACING_INSTRUCTIONS[PacingStyle.STANDARD]) def get_sentence_range(self, pacing: PacingStyle) -> tuple[int, int]: """ Get recommended sentence count range. Args: pacing: The pacing style. Returns: Tuple of (min_sentences, max_sentences). """ ranges: dict[PacingStyle, tuple[int, int]] = { PacingStyle.VERBOSE: (3, 5), PacingStyle.STANDARD: (2, 3), PacingStyle.QUICK: (1, 2), PacingStyle.DRAMATIC: (1, 3), } return ranges.get(pacing, (2, 3)) def should_include_ambient_detail(self, pacing: PacingStyle) -> bool: """ Check if ambient details should be included. Args: pacing: The pacing style. Returns: True if ambient details are appropriate. """ return pacing in (PacingStyle.VERBOSE, PacingStyle.STANDARD) def should_prompt_for_action(self, pacing: PacingStyle) -> bool: """ Check if response should end with action prompt. Args: pacing: The pacing style. Returns: True if action prompt is appropriate. """ # Quick pacing often ends with clear action options # Dramatic moments should let the moment breathe return pacing in (PacingStyle.STANDARD, PacingStyle.QUICK) def reset(self) -> None: """Reset pacing state for new game.""" self._last_pacing = PacingStyle.STANDARD self._location_changes = 0 self._combat_turn_count = 0 # ============================================================================= # Factory Function # ============================================================================= def create_pacing_controller() -> PacingController: """Create a new PacingController instance.""" return PacingController()