File size: 8,057 Bytes
f8ba6bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
"""
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()