crystal_quest

Version, currently main branch1 version
  • main branchlatestJul 10, 2026

github.com/amscotti/crystal_quest

A text-based adventure game powered by Claude, demonstrating the anthropic-cr SDK for Crystal.

0 stars
0 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  crystal_quest:
    github: amscotti/crystal_quest
    branch: main

main is a branch, not a release, so this tracks it rather than pinning a version.

Then run:

shards install

shard.yml

Crystal
>= 1.18.2
License
MIT
Author
Anthony Scotti
Target
  • crystal_quest from src/cli.cr

Dependencies

Runtime Dependencies

Development Dependencies

  • ameba~> 1.6.0github: crystal-ameba/amebadev

README

⚔️ Crystal Quest

Crystal Quest — 80s fantasy banner art

“Come closer, traveller — and mind the torch. You’ve stumbled into a dungeon that thinks for itself.

I am your Dungeon Master. I will conjure the dark, the cold stone, the things that slither just beyond the torchlight. I will give you a name, a blade, and a single, fragile thread of hit points. What you do with them… is between you, the dice, and whatever answers when you speak its name.

But heed this: every wound is real. Every coin you pocket is weighed. Every door you open stays open. The world remembers — because beneath the story, cold and impartial code is keeping score.

So. Step through the archway. The adventure is never the same twice, and the only way out is to play.”

The Dungeon Master, powered by Claude


Crystal Quest is a text-based fantasy adventure where Claude is the Dungeon Master and Crystal is the rules engine. It's also a living demonstration of the anthropic-cr SDK — the unofficial Anthropic SDK for the Crystal language.

Every adventure is generative: no two playthroughs produce the same rooms, NPCs, or story. There is no fixed campaign ending — the dungeon keeps going until you die, quit, or save for later. But the mechanics — HP, inventory, dice rolls, companions, remembered lore — live in deterministic Crystal code that Claude drives through real tool calls. The story can invent freely; the numbers can't quietly lie.

[!IMPORTANT] This is a demonstration application for the anthropic-cr SDK. It showcases the Tool Runner, prompt caching, extended thinking, and token-usage tracking in a complete, runnable app — not just a snippet.

✨ What makes it special

  • A real AI Dungeon Master. Claude narrates atmospheric, branching adventures — dark sanctums, rune-lit passages, entities with names and agendas. It controls NPCs, adjudicates uncertain actions with dice, and locks in the lore it establishes.
  • Code, not trust. HP, inventory, companions, and a journal of hard-won facts live in authoritative Crystal state. Claude changes them by calling tools — never by merely saying it did. The story can lie; the rules won't.
  • Save and resume. Mid-run save / load (or --load on the CLI) persist the full game state as JSON, so a long delve doesn't vanish when you close the terminal.
  • Battle-tested consistency. An AI player agent can run dozens or hundreds of turns against the DM while structural invariants (HP bounds, item stacking, name stability) are checked after every turn.
  • An SDK showcase, not a toy. Prompt caching cuts repeat-turn cost, extended thinking is available for richer DM reasoning, and autopilot runs report token usage and cache hit rate.

🎮 A taste of the adventure

╔═════════════════╗
║  CRYSTAL QUEST  ║
╚═════════════════╝

Your eyes snap open to darkness and the cold bite of stone against your back.
Testhero, you have no memory of how you came to be here — only a pounding
headache and the distant drip of water echoing somewhere in the black...
Beside you lies a guttering torch and a rusty dagger clutched loosely in your
hand. Strange runes glow faintly along the walls in sickly blue-green light.

> inspect the runes

The runes pulse with a cold blue light, shifting almost imperceptibly as if
reacting to your gaze... they trace a path along the wall, leading toward a
darker archway at the far end of the chamber.

❤ 20/20  •  📍 Starting Chamber  •  🎒 2 item(s)

Narration is generated live by Claude. The HP, location, and item count come from the game's authoritative state.

🚀 Quick start

Prerequisites: Crystal ≥ 1.18.2 and an Anthropic API key.

git clone https://github.com/amscotti/crystal_quest.git
cd crystal_quest
shards install
export ANTHROPIC_API_KEY=your_api_key_here   # from https://console.anthropic.com/
crystal run src/cli.cr

You're in the dungeon. Type natural-language actions and the DM takes it from there:

  • Move: "go north", "enter the cave", "head to the village"
  • Act: "search the room", "attack the goblin", "open the chest"
  • Survive: "drink the potion", "check my inventory", "how's my health?"
System commandAction
stats / statusCharacter sheet
inventory / invInventory (authoritative game state)
companions / partyTraveling companions
journal / notesRemembered facts
save [slot|path]Save the game (default: saves/autosave.json)
load [slot|path]Load a saved game
helpAvailable commands
debugFull internal game state
quit / exitEnd the adventure

Resume a previous run from the CLI:

crystal run src/cli.cr -- --load hero1          # loads saves/hero1.json
crystal run src/cli.cr -- --load ./myrun.json   # loads an explicit path

After every DM turn, a compact status line shows the truth of the mechanics:

❤ HP • 📍 location • 🎒 items

Environment variables

VariablePurpose
ANTHROPIC_API_KEYRequired. Your Anthropic API key.
CRYSTAL_QUEST_DEBUG=1Print tool calls, marker processing, and per-turn token usage.
CRYSTAL_QUEST_THINKING=1Enable extended thinking for the DM (default 2000-token budget, or a number like =4000).

🧪 Autopilot — AI vs. AI stress testing

Hand the controller to an AI player agent and watch it explore on its own. The DM narrates; the agent acts; the engine checks that state stayed consistent after every turn. Runs end on death, quit, or the turn limit.

# 50 turns of AI-vs-AI, with a consistency report at the end
crystal run src/cli.cr -- --autopilot --turns 50 --name "Testhero"

The closing report covers turns played, tool calls per turn, final state, token usage with cache hit rate, and any invariant violations:

═══ Autopilot Stress-Test Report ═══
Turns played: 38
Total tool calls: 105
Avg tool calls/turn: 2.76
Final HP: 0/20
Invariant violations: 0
═══ Token Usage (anthropic-cr SDK) ═══
Cache read: 254904   •   Cache hit rate: 36%
✅ All state invariants held — no consistency bugs detected.

The player agent runs on Haiku so long runs stay affordable. Autopilot makes live, billed API calls — it's a development stress tool, not part of the unit-test suite.

🛠️ The anthropic-cr SDK features on display

FeatureHow Crystal Quest uses it
Tool RunnerClaude drives 15 in-process Crystal tools (roll_dice, modify_hp, move_to_room, remember, …) that mutate authoritative game state. State changes come from real tool calls, not narration.
Prompt cachingThe tool schemas carry an ephemeral cache breakpoint so Anthropic reuses them turn after turn — visible as cache_read tokens in the report.
Extended thinkingOpt-in (CRYSTAL_QUEST_THINKING): the DM reasons about combat math, tool ordering, and name consistency before narrating.
Token-usage trackingUsage from every Tool Runner hop is accumulated and reported (input / output / cache creation / cache read / thinking tokens).
Claude Sonnet 5The DM runs on Sonnet for strong tool use and instruction-following; the autopilot player agent uses Haiku.

🏗️ Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        AI Dungeon Master                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌─────────────────┐    ┌──────────────────┐    ┌───────────────┐  │
│  │   Game Loop     │───▶│  System Prompt   │───▶│  Anthropic    │  │
│  │   (game_loop)   │    │    Builder       │    │   Client      │  │
│  └────────┬────────┘    └──────────────────┘    └───────┬───────┘  │
│           │                                             │          │
│           │                                             ▼          │
│           │             ┌──────────────────┐    ┌───────────────┐  │
│           │             │    Game State    │◀───│  Tool Runner  │  │
│           ▼             │  (game_state)    │    │ (game_tools)  │  │
│  ┌─────────────────┐    └────────▲─────────┘    └───────────────┘  │
│  │     Display     │             │                                 │
│  │    (display)    │    ┌────────┴─────────┐                       │
│  └─────────────────┘    │ Marker / Save    │                       │
│                         │ (fallback, I/O)  │                       │
│                         └──────────────────┘                       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Each turn, GameLoop builds a system prompt from the current GameState, registers GameTools with the Tool Runner, and lets Claude execute tools automatically. The final narrative is shown with a status line that reflects the resulting state. Optional text markers (MarkerParser) act as a safety net when the DM narrates a change without a tool call; SaveGame persists and restores full state as JSON.

The tools (Claude → Crystal):

ToolPurposeToolPurpose
roll_diceDice for checks/combatinspect_itemRead a specific item
get_character_statsRead player statsget_current_roomCheck location
modify_hpApply damage/healingmove_to_roomChange location
get_inventoryList inventoryadd_companionRegister a named NPC
add_to_inventoryAdd itemsupdate_companionRename/re-describe
remove_from_inventoryRemove itemsremove_companionRemove a companion
get_companionsList companionsrememberLock in a fact (lore/names)
get_journalRecall facts

Marker fallback: on the rare turn where Claude narrates a state change without calling a tool, lightweight text markers ([ROLL:1d20+5], [DAMAGE:5], [MOVE:Cave], …) apply the change as a safety net — but only when no state-mutating tool ran, so nothing is ever double-counted.

📦 Development

This project pins Crystal via mise (mise.toml1.20.3). With mise installed:

mise install          # install the pinned Crystal
mise run install      # shards install
mise run build        # release binary → bin/crystal_quest
mise run spec         # test suite
mise run check        # format + lint + type-check + tests
mise run              # play the game (pass CLI args after --)
mise run autopilot -- --turns 20
CommandPurpose
shards install / mise run installInstall dependencies
crystal run src/cli.cr / mise runRun the game
crystal spec / mise run specRun the test suite (184 specs)
crystal tool format / mise run formatFormat code
./bin/ameba / mise run lintRun the Ameba linter
./scripts/check.sh / mise run checkFormat + lint + type-check + tests
shards build --release / mise run buildBuild bin/crystal_quest

🤝 Contributing

  1. Fork it (https://github.com/amscotti/crystal_quest/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

📄 License

This project is licensed under the MIT License — see the LICENSE file for details.


Crystal Quest is an unofficial, community demonstration of the anthropic-cr SDK and is not affiliated with or endorsed by Anthropic.