GameLoop
Constants
Debug mode - set via CRYSTAL_QUEST_DEBUG=1 environment variable
Model used for the Dungeon Master. Sonnet 5 is the latest Sonnet shipped with anthropic-cr 0.8.0 — strong at tool use and following the strict "call tools before narrating" contract this game relies on.
Regex patterns for intercepting state queries locally (avoids asking Claude, which prevents it from inventing or misremembering game state).
Tightened to avoid hijacking "where is this goblin from?" / "what room has the boss?" — only phrases clearly about the player's OWN location match.
Minimum valid extended-thinking budget enforced by the Anthropic API.
Tools that mutate GameState. The marker fallback runs only when NONE of these fired this turn — calling a read-only tool (e.g. get_inventory) must not silently disable the fallback, otherwise narrated-but-uncalled state changes (damage, movement, ...) would be lost.
Token budget for the DM's extended thinking when enabled (see CRYSTAL_QUEST_THINKING). Extended thinking lets the DM reason about combat math, name consistency, and tool ordering before it narrates — which improves adherence to the tool-first rules at the cost of some latency.
Class methods
Build the system prompt with DM instructions and current state
Percentage of input tokens served from the prompt cache. Pure function so it can be unit-tested. Measures how effective the tools/system-prompt caching is across an autopilot run — a high rate means Anthropic is reusing the cached tool schemas instead of reprocessing them each turn.
Handle built-in slash/keyword commands. Returns :quit to exit the loop, :handled to skip to the next iteration, or nil if the input was not a meta command.
Intercept natural-language state queries (inventory/location/health) and answer them from the authoritative game state. Returns true if handled.
Map current HP to a descriptive condition label based on percentage of max_hp. Pure function so the thresholds can be unit-tested without capturing stdout.
Split input into a downcased command word and an optional remainder arg. Exposed for unit testing of save/load argument handling.
Marker safety-net: delegates to MarkerParser so parsing logic lives in one focused module. Kept as a GameLoop method for existing call sites/specs.
Resolve the extended-thinking budget from CRYSTAL_QUEST_THINKING, or nil if thinking is disabled. Accepts "1"/"on"/"yes" (→ default budget) or a token count (>= MIN_THINKING_BUDGET). Values below the API minimum are clamped up. Exposed for unit testing.
Run the main game loop. driver supplies player actions (STDIN for humans,
PlayerAgentDriver for autopilot). When autopilot is true the loop runs the
stress harness: caps turns at max_turns, checks state invariants each turn,
and prints a report at the end.
When resume is true (loaded save), skip the opening DM scene-setting call.
The large, static portion of the system prompt — DM role, the tool-first contract, and response-format rules. This text is identical every turn, so it benefits from Anthropic prompt caching (the tools array carries the cache breakpoint; the system prompt is sent as a stable prefix alongside it). Keeping it in its own method makes the static/dynamic split explicit.