|
|
""" |
|
|
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: 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 |
|
|
""", |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
QUICK_KEYWORDS: frozenset[str] = frozenset([ |
|
|
"attack", |
|
|
"hit", |
|
|
"strike", |
|
|
"shoot", |
|
|
"cast", |
|
|
"dodge", |
|
|
"run", |
|
|
"flee", |
|
|
"block", |
|
|
"parry", |
|
|
]) |
|
|
|
|
|
|
|
|
VERBOSE_KEYWORDS: frozenset[str] = frozenset([ |
|
|
"look", |
|
|
"examine", |
|
|
"describe", |
|
|
"search", |
|
|
"explore", |
|
|
"enter", |
|
|
"arrive", |
|
|
"approach", |
|
|
]) |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
if special_moment is not None: |
|
|
return PacingStyle.DRAMATIC |
|
|
|
|
|
|
|
|
if game_mode == GameMode.COMBAT: |
|
|
self._combat_turn_count += 1 |
|
|
|
|
|
if self._combat_turn_count == 1: |
|
|
return PacingStyle.STANDARD |
|
|
return PacingStyle.QUICK |
|
|
|
|
|
|
|
|
if game_mode != GameMode.COMBAT: |
|
|
self._combat_turn_count = 0 |
|
|
|
|
|
|
|
|
input_lower = player_input.lower() |
|
|
|
|
|
|
|
|
if any(kw in input_lower for kw in self.QUICK_KEYWORDS): |
|
|
return PacingStyle.QUICK |
|
|
|
|
|
|
|
|
if any(kw in input_lower for kw in self.VERBOSE_KEYWORDS): |
|
|
return PacingStyle.VERBOSE |
|
|
|
|
|
|
|
|
if any(kw in input_lower for kw in self.SOCIAL_KEYWORDS): |
|
|
return PacingStyle.STANDARD |
|
|
|
|
|
|
|
|
if game_state: |
|
|
if self._is_new_location(game_state): |
|
|
self._location_changes += 1 |
|
|
return PacingStyle.VERBOSE |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_pacing_controller() -> PacingController: |
|
|
"""Create a new PacingController instance.""" |
|
|
return PacingController() |
|
|
|