DungeonMaster-AI / src /agents /pacing_controller.py
bhupesh-sf's picture
first commit
f8ba6bf verified
"""
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()