From d54599e27dc1709fbcd2f792d9810787e3f5da76 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Thu, 9 Jul 2026 11:17:54 -0500 Subject: [PATCH] first commit --- .claude/security/.sdk_bootstrap_spawned | 0 .claude/security/log.txt | 46 ++ ..._1a541ba7-dd07-4b76-b6ff-9f6ad45bfc8e.json | 1 + CLAUDE.md | 532 ++++++++++++++++++ README.md | 33 ++ api/README.md | 30 + api/docs/README.md | 13 + api/prompts/README.md | 23 + api/prompts/adjudicator.md | 19 + api/prompts/banter.md | 13 + api/prompts/improviser.md | 13 + api/prompts/narrator.md | 12 + api/prompts/npc.md | 21 + client/README.md | 19 + client/docs/README.md | 13 + client/project.godot | 11 + content/README.md | 15 + content/fallback/README.md | 7 + content/npcs/README.md | 7 + content/quests/README.md | 5 + docs/README.md | 12 + docs/adr/README.md | 15 + docs/roadmap.md | 34 ++ 23 files changed, 894 insertions(+) create mode 100644 .claude/security/.sdk_bootstrap_spawned create mode 100644 .claude/security/log.txt create mode 100644 .claude/security/security_warnings_state_1a541ba7-dd07-4b76-b6ff-9f6ad45bfc8e.json create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 api/README.md create mode 100644 api/docs/README.md create mode 100644 api/prompts/README.md create mode 100644 api/prompts/adjudicator.md create mode 100644 api/prompts/banter.md create mode 100644 api/prompts/improviser.md create mode 100644 api/prompts/narrator.md create mode 100644 api/prompts/npc.md create mode 100644 client/README.md create mode 100644 client/docs/README.md create mode 100644 client/project.godot create mode 100644 content/README.md create mode 100644 content/fallback/README.md create mode 100644 content/npcs/README.md create mode 100644 content/quests/README.md create mode 100644 docs/README.md create mode 100644 docs/adr/README.md create mode 100644 docs/roadmap.md diff --git a/.claude/security/.sdk_bootstrap_spawned b/.claude/security/.sdk_bootstrap_spawned new file mode 100644 index 0000000..e69de29 diff --git a/.claude/security/log.txt b/.claude/security/log.txt new file mode 100644 index 0000000..11705be --- /dev/null +++ b/.claude/security/log.txt @@ -0,0 +1,46 @@ +[2026-07-09 10:54:03.926] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:54:03.929] Processing: hook_event=UserPromptSubmit, tool= +[2026-07-09 10:56:09.126] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:09.128] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:18.474] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:18.476] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:21.852] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:21.855] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:25.336] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:25.338] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:28.741] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:28.744] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:32.070] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:32.073] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:35.419] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:35.422] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:39.157] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:39.159] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:42.516] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:42.518] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:48.542] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:48.545] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:51.814] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:51.816] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:55.095] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:55.098] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:56:59.075] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:56:59.077] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:57:02.336] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:02.338] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:57:05.616] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:05.618] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:57:08.886] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:08.888] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:57:12.207] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:12.209] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:57:16.216] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:16.218] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:57:19.540] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:19.542] Processing: hook_event=PostToolUse, tool=Write +[2026-07-09 10:57:31.383] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:31.386] Processing: hook_event=PostToolUse, tool=Edit +[2026-07-09 10:57:51.755] Hook called with args: ['/home/ptarrant/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.6/hooks/security_reminder_hook.py'] +[2026-07-09 10:57:51.758] Processing: hook_event=Stop, tool= +[2026-07-09 10:57:51.761] _git_status_porcelain error: [Errno 138] emscripten does not support processes. +[2026-07-09 10:57:51.761] Stop hook: empty review set diff --git a/.claude/security/security_warnings_state_1a541ba7-dd07-4b76-b6ff-9f6ad45bfc8e.json b/.claude/security/security_warnings_state_1a541ba7-dd07-4b76-b6ff-9f6ad45bfc8e.json new file mode 100644 index 0000000..69bf242 --- /dev/null +++ b/.claude/security/security_warnings_state_1a541ba7-dd07-4b76-b6ff-9f6ad45bfc8e.json @@ -0,0 +1 @@ +{"shown_warnings": [], "touched_paths": []} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5398c75 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,532 @@ +# CLAUDE.md + +Project charter and working agreement for an AI-driven single-player RPG built in Godot 4. + +Read this before writing code. When a request conflicts with something written here, say so rather than silently complying. + +--- + +## 1. What this is + +A single-player, party-based fantasy RPG. Combat is deterministic JRPG-style. The story skeleton is authored; the *prose* is generated. An AI acts as Dungeon Master — narrating scenes, voicing NPCs, and improvising minor events — while all game state lives in code. + +**Not multiplayer.** Not now, not later. Companions are code-controlled NPCs. Do not introduce networking, lockstep, or authority abstractions "in case." + +**Target:** Desktop first (Windows/Linux). Mobile is a later consideration and should not constrain POC decisions. + +**Current phase:** POC. The question the POC answers is *does bounded AI dialogue feel alive, or does it feel like a chatbot in a costume?* Everything else is scaffolding for that experiment. + +--- + +## 2. The one rule + +> **Code owns state. AI owns text.** + +The AI never mutates game state. It either: + +- **narrates** what code already decided, or +- **proposes** an action that code validates and applies. + +Every AI response is untrusted input. Parse it, validate it, and be prepared to discard it. A failed AI call degrades to authored fallback text. It never blocks the player, corrupts a save, or throws to the UI. + +If you find yourself writing code where an AI response directly sets a variable that persists — stop. That is the architectural failure this project is designed to avoid. + +--- + +## 3. Setting and tone + +High fantasy. Elves, dwarves, humans, beast folk. + +**Gritty, not grim.** The reference is Batman in the DC comics, not Warhammer and not Care Bears. People get hangovers. People vomit. People curse. Wounds get infected. The world has real problems and does not care about you. + +**Comedy emerges from situation, never from the narrator.** The narrator is dry and does not wink. Nothing is ever described as "hilariously" anything. The humor comes from: + +- Bad Luck producing indignity +- Cadwyn causing a disaster +- Brannoc commenting on both + +The world is played straight. That is what makes it funny. + +**Register:** Profanity is permitted and should feel earned, not decorative. Violence has weight. Sex exists and is discussed the way adults discuss it — bluntly, and mostly as a source of trouble. + +--- + +## 4. Architecture + +Three processes. They talk over HTTP. Nothing else. + +``` +┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Godot 4 client │ ─────▶ │ FastAPI proxy │ ─────▶ │ Replicate / │ +│ (GDScript) │ ◀───── │ │ ◀───── │ Ollama │ +│ │ │ auth · metering │ │ │ +│ ALL game state │ │ prompts · logs │ │ │ +│ ALL game rules │ │ model routing │ │ │ +└──────────────────┘ └──────────────────┘ └──────────────┘ +``` + +### The client never holds an API key. Ever. + +Not in the POC. Not in a `.env`. Not "just for testing." The moment a key ships in a client binary, it is public. + +The client knows exactly one base URL and a set of role endpoints: + +``` +POST /dm/narrate +POST /dm/adjudicate +POST /dm/improvise +POST /npc/speak +POST /party/banter +``` + +The client does not know which model serves a role. It does not know the prompt. It sends game state and receives text. + +### Why the proxy exists in the POC, before there is anything to meter + +1. **Prompts are the actual IP.** They live server-side. +2. **Model routing becomes config.** "The Adjudicator is now an 8B model" is a deploy, not a client patch. +3. **Every call is logged from day one.** This gives free eval infrastructure — replay yesterday's real traffic against a candidate model. +4. **Credits retrofit cleanly.** Add auth middleware and a Stripe webhook. The game does not notice. + +Building the POC with direct client → Replicate calls and retrofitting the proxy later means rewriting every call site. + +### Environments + +| | Client | Proxy | Model | +|---|---|---|---| +| Dev | Godot editor | `localhost:8000` | Ollama (homelab) | +| Staging | Exported build | homelab VPS | Ollama (homelab) | +| Prod | Exported build | fly.io | Replicate | + +--- + +## 5. The DM is not one prompt + +The previous attempt used a single omniscient prompt. It failed — as it always does. One prompt cannot simultaneously hold narrative voice, rules adjudication, and state consistency without drifting on at least one axis. + +Instead, four narrow roles. Each has its own prompt, its own input contract, its own output contract, and potentially its own model. + +### Narrator + +Describes scenes, outcomes, transitions. Gets scene state and the canon log. Returns prose. + +- **Quality matters most here.** Use the good model. +- **Output:** prose with tags. +- Never invents proper nouns without emitting a `[FACT: ...]` tag. + +### Adjudicator + +Player typed free text. Map it onto a legal action, or reject it. + +- **Speed matters, prose does not.** Use a small, fast model. +- **Output:** strict JSON. +- Returns `{"action": "...", "params": {...}}` or `{"action": "invalid", "reason": "..."}`. +- The `reason` is shown to the player. Write it in-world. + +### Improviser + +Player went off-script, or Luck fired. Generate a minor event. + +- **Sandboxed by construction.** See §7. +- **Output:** prose with tags. +- Cannot touch quest flags. This is enforced in code, not in the prompt. + +### NPC + +Voice a specific character. See §6. + +- **Output:** prose with move tags. + +### Banter + +Companions comment on recent events. Low stakes, high charm, aggressively cacheable. + +- **Output:** prose. +- Reads the humiliation log. See §9. + +--- + +## 6. Bounded NPC dialogue + +NPCs may say anything *stylistically*. They may only *do* things from a closed vocabulary. + +### The move vocabulary + +``` +reveal(topic_id) +offer_quest(quest_id) +accept_item(item_id) +give_item(item_id) +adjust_disposition(delta) +refuse +end_conversation +become_hostile +``` + +Eight moves. If this list grows past ten, you have two systems and should split them. + +### The contract + +**Prompt receives:** + +- Persona (voice, manner, what they want) +- Current disposition toward player (integer, −100..100) +- `knowledge: [...]` — an explicit list of what this NPC knows +- `available_moves: [...]` — the subset this NPC can legally perform right now +- Canon log + +**Prompt returns:** prose, with zero or more inline move tags. + +``` +"Aye, I knew your father." [ADJUST_DISPOSITION: +5] +"But I'll not speak of him in this place." [REFUSE] +``` + +**Code then:** + +1. Extracts tags via regex +2. Validates each against actual game state — *does this NPC have quest 14? is disposition high enough?* +3. Applies valid moves +4. **Silently drops invalid moves. Keeps the prose.** + +An NPC hallucinating a move they cannot perform is not an error. It is a Tuesday. Log it, drop it, move on. + +### The knowledge list is the whole design + +It is the only thing preventing the blacksmith from cheerfully revealing the twist. It is not a suggestion in a prompt — it is the *entire content* the NPC has to draw on. + +Building these lists is real authoring work. Budget for it. + +--- + +## 7. Luck + +The signature system. Read this section twice. + +### Luck never touches numbers + +Damage is fully deterministic. Same attack, same target, same state → same number, every time. The player can plan a fight. Math is math. + +**Luck selects between outcomes.** It governs which branch you land on when the game reaches a fork it was already going to reach: + +- Whether a borderline resistance check lands +- Whether a hit crosses the critical threshold +- Whether the Improviser fires at all +- Whether the event it generates is a boon or a humiliation +- Whether the waitress is a waitress + +A low-Luck character is not doing 8% less damage. A low-Luck character is the one the trap door opens under. + +### Luck is visible in prose, never in the combat log + +The player should *feel* cursed. They should never be able to *calculate* that they are cursed. + +**Never display the Luck value.** Display a descriptor: + +> *Fortune spits on you.* +> *The dice are kind today.* + +Uncertainty is the point. The player should become superstitious. That is the entire reason this stat exists. + +### Generation and drift + +- Base: average of 5 rolls, plus modifier. Centralizes hard — most characters land near the middle. This is correct for a stat with this much narrative reach. +- **Luck is volatile.** It drifts ±3 from base, driven by events: sleeping in a real bed, a shrine's blessing, killing something that was praying. +- Cursed and blessed items move it. *A cursed blade granting +4 STR and −5 LCK is the most interesting item in this game.* Build the item system to support this on day one. +- The player has agency over Luck. Shrines, offerings, and the decision to keep or discard a cursed item are meaningful choices. + +### The Improviser's hard limits + +Bad luck costs **dignity, not progress.** + +The Improviser **may** generate: +- Embarrassment +- Inconvenience +- Property damage +- Social catastrophe +- Minor injury + +The Improviser **may never** generate: +- Quest failure +- Permanent stat loss +- Party death +- Loss of a plot-critical item + +**Enforce this in code, not in the prompt.** The Improviser's output passes through a validator that can only apply a whitelisted set of state changes. It is structurally incapable of ending a quest. + +Why: comedy that punishes a player for a build choice they made in character creation is not comedy. It is a fine. Bad luck should produce a story the player tells later. Good luck should make them feel briefly like the protagonist. + +### The comedy is Brannoc, not the waitress + +The joke is not that the waitress is a man in disguise. The joke is that Brannoc saw it happen and will not let it go for three towns. + +This means the Banter role must have access to a memory of humiliations. Literally a list. See §9. + +--- + +## 8. Stats and classes + +### Stats + +| Stat | Governs | +|---|---| +| STR | Melee damage, carry, forcing things | +| DEX | Initiative, finesse weapons, sleight | +| CON | HP, resisting poison and exhaustion | +| FTH | Divine casting. Priests, Oathbound. Also: conviction in dialogue | +| MAG | Arcane casting. Scholars. Also: recall, deduction | +| LCK | See §7 | + +FTH and MAG replace WIS and INT. Both must matter outside combat or the POC has not tested anything. + +### Classes + +Named for the **role in the world**, not the mechanical archetype. *Priest* is what a village calls him. *Cleric* is what a rulebook calls him. + +| Archetype | Name | Primary | +|---|---|---| +| Fighter | **Sellsword** | STR | +| Rogue | **Assassin** | DEX | +| Cleric | **Priest** | FTH | +| Wizard | **Scholar** | MAG | +| Warlock | **Oathbound** | FTH | +| Ranger | **Tracker** | DEX | +| Barbarian | **Berserker** | STR | +| Bard | **NPC ONLY** | — | + +**Bard is not playable.** A Bard the player cannot control is funnier than one they can manage. He is a consequence, not a resource. + +**POC playable classes:** Sellsword, Assassin, Priest. Covers all three combat pillars and gives FTH something to do outside combat. + +--- + +## 9. Companions + +Two. They carry the entire tone. Both are code-controlled in combat and AI-voiced in dialogue. + +### Cadwyn Vell — Bard + +**He is genuinely good.** That is load-bearing. If he were a hack he'd be a joke; because he is actually a fine musician and a fine liar and, briefly and in the right light, magnificent — the disasters land harder. + +He is not a coward, which surprises people. He will walk into a burning building. He simply will not tell you *why* it is burning, or that he knows the man who set it, or that the man was looking for him. + +- **Combat:** support, buffs, crowd control. +- **Out of combat:** the party's social solvent. He gets you into places. He is why you had to leave the last three. +- **Systemically:** he is the **Improviser's hook.** When Luck rolls badly in a town, the system does not have to invent a reason the innkeeper hates you. Cadwyn is standing right there. +- **Comedy register:** he does not know he is the problem. He is bewildered, every time, that the world reacted badly. *This never becomes self-awareness.* The moment Cadwyn learns, he stops being funny. +- **Depth:** he is running from something specific, and has been running long enough that the running is the only part he still does well. High approval reveals that what he did was not funny at all. Low approval means you never find out — he leaves in the night, and the absence lands harder than expected. +- **Voice:** florid when performing, clipped when scared. Lies reflexively about small things, truthfully about large ones. + +### Brannoc Thane — Sellsword + +Not a grump. Grumps are exhausting. Brannoc is *fond* of you. That is what makes it unbearable. + +Twenty years past his prime, entirely aware of it, at peace with it. He has buried more friends than he can name and has arrived somewhere past grief at a kind of terrible good humor. He finds the world funny because the alternative was worse. + +- **Combat:** frontline, damage soak. +- **Out of combat:** he tells you what things are. Brannoc has seen this before. He knows what that rune means and he is not going to enjoy telling you. +- **Systemically:** he **holds the humiliation log.** Events with high embarrassment-weight are written to a list. Banter pulls from it with decaying frequency. **New humiliations do not overwrite old ones — they stack.** By hour ten he has a repertoire. +- **Comedy register:** the callback. He remembers the waitress. He will bring her up in a crypt, three towns later, at the worst possible moment, with real affection. *He is not mocking you. He is delighted by you.* This is much funnier and much harder to resent. +- **Depth:** Brannoc is not fine. The good humor is a structure he built on purpose and maintains daily. He knows he will die on a job like this one and has decided that is acceptable. High approval does not unlock a tragic backstory. It unlocks him quietly asking you to make sure someone remembers his name. That is the whole thing. +- **Voice:** dry, warm, economical. Never raises his voice. Says devastating things in the tone of a man discussing weather. + +### The pair + +They dislike each other, mildly and permanently. Brannoc thinks Cadwyn is a child. Cadwyn thinks Brannoc is a tombstone that walks. Neither is wrong. Neither will leave the other behind. + +- Cadwyn generates chaos. Brannoc annotates it. A complete comedy loop, no third party required. +- Cadwyn lies. Brannoc does not. The player learns to triangulate — which teaches them the world has a truth value. Essential in a game where an AI describes that world. +- **Approval means different things.** Cadwyn's high approval reveals he is *worse* than you thought. Brannoc's reveals he is *sadder* than you thought. Neither is a reward, exactly. + +### Approval + +Single integer, −100..100, per companion. Adjusted by player choices weighed against companion values (honor, greed, mercy, curiosity). + +**POC scope:** companions cannot leave. That is a v2 problem. + +--- + +## 10. Combat + +**Positionless, turn-based.** No grid. No flanking. No terrain. + +Combat is not the risk surface of this project. Ship the boring one; prove the interesting one. + +- **Damage is deterministic.** No variance rolls on damage. +- **Initiative is rolled once at encounter start** and is relatively random. DEX modifies. +- **The interesting decision is resource management** — MP, cooldowns, consumables. Not matchup-reading. Pick one axis for the POC. +- **Luck influences crit thresholds and borderline status checks.** Nothing else. See §7. +- **Combat flavor text is async and optional.** The numbers land immediately. The prose catches up. If the AI call fails, the fight continues with authored text. + +### Determinism and seeding + +**Seed the RNG per encounter from save state.** Load a save, fight the same fight, get the same outcomes. + +This costs nothing now and is nearly impossible to retrofit. It is the difference between a bug you can reproduce and a bug you cannot. + +Log the seed *and* the full prompt with every AI call, for the same reason. + +--- + +## 11. The canon log + +The mechanism preventing the AI from contradicting itself. + +**Do not use "last N messages."** It will fail. The model will forget the dagger you sold in hour one. + +Instead, a compact structured fact list that *code* maintains and injects into every call: + +```json +{ + "player": { "name": "...", "class": "...", "luck_descriptor": "..." }, + "party": [ { "name": "Brannoc Thane", "disposition": 40 } ], + "recent_events": ["3-5 terse lines"], + "established_facts": ["things the AI said that are now canon"], + "active_quests": [...], + "humiliations": ["for the banter role"] +} +``` + +### Every AI-generated proper noun becomes a durable fact + +When the Improviser invents a hedge witch named Marn on the eastern road, code writes `"met hedge witch Marn on the eastern road"` into `established_facts` — so the Narrator does not later describe that road as untraveled. + +Mechanism: the Improviser and Narrator emit `[FACT: ...]` tags. Code harvests them. + +**AI-generated prose is never the source of truth for anything a later scene reads.** If prose and state disagree, state wins, and that is a bug to fix in the prompt. + +--- + +## 12. Output contracts + +| Role | Format | Why | +|---|---|---| +| Narrator | Prose + tags | Models get stiff inside braces. Voice is the differentiator. | +| NPC | Prose + tags | Same. | +| Improviser | Prose + tags | Same. | +| Banter | Prose | No tags needed. | +| **Adjudicator** | **Strict JSON** | Prose quality is irrelevant. Validity is everything. | + +### Tag syntax + +``` +[MOVE: offer_quest(14)] +[FACT: the eastern bridge is out] +[ADJUST_DISPOSITION: -10] +``` + +Extracted by regex. Stripped from displayed prose. + +### Retry policy + +1. Parse fails → **one** retry, with the validation error appended to the prompt. +2. Second parse fails → **authored fallback text.** + +Never let a bad parse block the player. Never retry more than once — latency compounds and the player is waiting. + +--- + +## 13. Failure UX + +**When the API is down, the player sees a degraded DM — not an error.** + +This is a design problem, not an engineering one. The DM has a voice. A degraded DM still has that voice. + +> The chamber is cold. Something waits in the dark. + +Write fallbacks as **content**, not as error handling. They live in the same files as the rest of the writing. A generic error dialog is a failure of craft. + +Every AI-dependent surface needs an authored fallback before it ships. No exceptions. + +--- + +## 14. Cost and latency + +Roughly 1–3k tokens per call, dozens of calls per session. + +- **Cache aggressively.** Same room, same state → same description. Do not regenerate. +- **Pre-generate on loading screens.** You know the player is walking toward the crypt. +- **Stream everything.** Latency reads as *the DM is speaking* rather than *the game is frozen.* +- **Combat flavor is async.** Numbers first, prose after. +- **Route by role.** The Adjudicator can be tiny. The Narrator should not be. + +--- + +## 15. Input model + +**Hybrid.** + +- **Menus:** combat, navigation, inventory. +- **Free text:** dialogue, and explicit "what do you do?" moments. + +**The boundary must be visible.** The player should always know which mode they are in. Ambiguity here reads as broken. + +Free text goes to the Adjudicator, which maps it to a legal action or rejects it with an in-world reason. + +--- + +## 16. Tech + +- **Client:** Godot 4, **GDScript.** Prefer readability. Drop to C# only if GDScript genuinely cannot do the thing — and say why before you do. +- **Proxy:** Python, **FastAPI.** +- **Models:** Ollama (self-hosted, dev/staging) → Replicate (prod). Routed per-role, server-side. +- **Repo:** greenfield. No existing code. + +### Layout + +``` +/client Godot 4.7 project (was /game) + /docs Client-specific docs +/api FastAPI service — the guarding proxy of §4 (was /proxy) + /docs API-specific docs + /prompts Role prompts (versioned, reviewed like code) +/content Authored: quests, NPC knowledge lists, fallback text +/docs Cross-cutting docs — roadmap, ADRs, anything affecting both sides +``` + +`/api` is still the proxy described in §4 — auth, metering, prompt-hiding, model +routing. It is named `api` for the client's convenience, not because it is a +generic pass-through. Docs are split: side-specific docs live under +`/client/docs` and `/api/docs`; anything spanning the HTTP boundary lives in the +root `/docs`. `CLAUDE.md` stays at the repo root as the source of truth. + +Prompts are source code. They get reviewed. They get versioned. Changing a prompt is a change to the game. + +--- + +## 17. POC scope + +**~30 minutes of play.** + +- One town, **two NPCs** with authored knowledge lists +- One dungeon: **three fights, one boss** +- **Two companions:** Cadwyn, Brannoc +- **Three playable classes:** Sellsword, Assassin, Priest +- Luck fully wired: generation, drift, one shrine, one cursed item +- Full proxy, full role split, full canon log +- Authored fallback text for every AI surface + +### The POC's job + +Answer one question: **does bounded AI dialogue feel alive, or does it feel like a chatbot in a costume?** + +Test that before building anything else. Every other system exists to make that test possible. + +### Explicitly out of scope + +- AI-generated story skeletons (v2 — the seeded story is authored for now) +- Multiplayer (never) +- Mobile +- Credits, payments, auth +- Companions leaving the party +- Positional combat +- More than three classes + +--- + +## 18. Working agreement + +- **Do not write code unless asked.** Design conversations are for design. The human writes code via Claude Code separately. +- **When a request conflicts with this document, say so.** Do not silently comply. This file is the argument; if it is wrong, change the file first. +- **When adding a system, state which side of §2 it falls on.** State or text. If it is both, it is two systems. +- **Prefer deleting scope.** The POC's value is answering one question fast. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d874919 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# coc-rpg + +AI-driven single-player, party-based fantasy RPG. Godot 4 client, FastAPI proxy, AI as Dungeon Master. + +**Read [`CLAUDE.md`](CLAUDE.md) first** — project charter and working agreement. It is the source of truth. If code disagrees with it, the charter wins until the charter is changed. + +## The one rule + +> **Code owns state. AI owns text.** (charter §2) + +## Repo layout + +``` +/client Godot 4.7 game client (GDScript) — ALL game state, ALL game rules + /docs Client-specific docs (scenes, input, combat wiring) +/api FastAPI proxy — auth, metering, prompt routing, logging (charter §4) + /docs API-specific docs (endpoints, model routing, deploy) + /prompts Role prompts — source code, versioned, reviewed (charter §5, §16) +/content Authored game data — quests, NPC knowledge lists, fallback text + /quests Story skeletons and quest definitions + /npcs Per-NPC knowledge lists (charter §6 — "the whole design") + /fallback Authored degraded-DM text for every AI surface (charter §13) +/docs Cross-cutting docs — roadmap, ADRs, planning affecting both sides + /adr Architecture Decision Records +``` + +## The three processes (charter §4) + +``` +client (GDScript) ──HTTP──▶ api (FastAPI) ──▶ Replicate / Ollama +``` + +The client holds **no API key, ever**. It knows one base URL and a set of role endpoints. It sends game state, receives text. diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..750cbf7 --- /dev/null +++ b/api/README.md @@ -0,0 +1,30 @@ +# /api — FastAPI proxy + +The guarding proxy from charter §4. Not a generic API — it exists to keep the API key, the prompts, and the model routing server-side, and to log every call from day one. + +**Stack:** Python, FastAPI. Models: Ollama (dev/staging) → Replicate (prod), routed per-role server-side. + +## Role endpoints (charter §4) + +``` +POST /dm/narrate Narrator — scene/outcome prose (good model) +POST /dm/adjudicate Adjudicator — free text → legal action (small/fast, strict JSON) +POST /dm/improvise Improviser — minor sandboxed event (charter §7 limits) +POST /npc/speak NPC — voice one character (bounded moves, §6) +POST /party/banter Banter — companion callbacks (cacheable, §9) +``` + +The client does not know which model serves a role or what the prompt is. + +## Responsibilities + +- **Auth · metering** (retrofit later — middleware + Stripe webhook, §4) +- **Prompt ownership** — prompts live in [`prompts/`](prompts/), never in the client +- **Model routing** — role → model is config, a deploy not a client patch +- **Logging** — log seed *and* full prompt with every call (§10) for replay/eval + +## Contracts + +Every AI response is untrusted (§2). Parse, validate, be ready to discard. One retry on parse failure, then authored fallback (§12). + +See [`docs/`](docs/) for endpoint schemas, model routing, and deploy notes. diff --git a/api/docs/README.md b/api/docs/README.md new file mode 100644 index 0000000..38fff3d --- /dev/null +++ b/api/docs/README.md @@ -0,0 +1,13 @@ +# /api/docs + +API-specific documentation. Scoped to the proxy — anything that only affects the server side lives here. + +Put here: + +- Endpoint request/response schemas (per role, §4) +- Model routing table (role → model, per environment) +- Deploy notes (localhost → homelab VPS → fly.io, §4) +- Logging/eval-replay format (§10) +- Auth/metering design when it lands (§4) + +Cross-cutting design (anything touching both client and api) goes in the root [`/docs`](../../docs), not here. diff --git a/api/prompts/README.md b/api/prompts/README.md new file mode 100644 index 0000000..f111392 --- /dev/null +++ b/api/prompts/README.md @@ -0,0 +1,23 @@ +# /api/prompts + +**Prompts are source code** (charter §16). They are versioned, reviewed like code, and changing one is a change to the game. + +Four narrow roles, not one omniscient prompt (charter §5). Each has its own input contract and output contract: + +| File | Role | Output | +|---|---|---| +| `narrator.md` | Narrator — scenes, outcomes, transitions | Prose + tags | +| `adjudicator.md` | Adjudicator — free text → legal action | **Strict JSON** | +| `improviser.md` | Improviser — minor sandboxed events | Prose + tags | +| `npc.md` | NPC — voice one character | Prose + move tags | +| `banter.md` | Banter — companion callbacks | Prose | + +## Tag syntax (charter §12) + +``` +[MOVE: offer_quest(14)] +[FACT: the eastern bridge is out] +[ADJUST_DISPOSITION: -10] +``` + +Extracted by regex, validated against game state, stripped from displayed prose. Invalid moves are silently dropped, prose kept (§6). diff --git a/api/prompts/adjudicator.md b/api/prompts/adjudicator.md new file mode 100644 index 0000000..97cdeb3 --- /dev/null +++ b/api/prompts/adjudicator.md @@ -0,0 +1,19 @@ +# Adjudicator prompt + +**Role:** Map player free text onto a legal action, or reject it. **Speed matters, prose does not — use a small, fast model** (charter §5). + +**Output:** strict JSON. + +```json +{"action": "...", "params": {...}} +``` +or +```json +{"action": "invalid", "reason": "..."} +``` + +The `reason` is shown to the player — write it in-world (§5). + +--- + + diff --git a/api/prompts/banter.md b/api/prompts/banter.md new file mode 100644 index 0000000..0ecce5e --- /dev/null +++ b/api/prompts/banter.md @@ -0,0 +1,13 @@ +# Banter prompt + +**Role:** Companions comment on recent events. Low stakes, high charm, aggressively cacheable (charter §5). + +**Output:** prose. No tags needed. + +**Input:** the humiliation log (§9, §11). Brannoc holds it; new humiliations stack, they do not overwrite. Banter pulls from it with decaying frequency. + +The comedy is Brannoc, not the waitress (§7). He remembers, with real affection, at the worst possible moment, three towns later. + +--- + + diff --git a/api/prompts/improviser.md b/api/prompts/improviser.md new file mode 100644 index 0000000..5965574 --- /dev/null +++ b/api/prompts/improviser.md @@ -0,0 +1,13 @@ +# Improviser prompt + +**Role:** Player went off-script, or Luck fired. Generate a minor event. **Sandboxed by construction** (charter §5, §7). + +**Output:** prose with tags. Cannot touch quest flags — enforced in code, not in the prompt. + +**Hard limits (charter §7):** may generate embarrassment, inconvenience, property damage, social catastrophe, minor injury. **May never** generate quest failure, permanent stat loss, party death, or loss of a plot-critical item. Bad luck costs dignity, not progress. + +Cadwyn is the hook — when Luck rolls badly in a town, he is standing right there (§9). + +--- + + diff --git a/api/prompts/narrator.md b/api/prompts/narrator.md new file mode 100644 index 0000000..4ef7406 --- /dev/null +++ b/api/prompts/narrator.md @@ -0,0 +1,12 @@ +# Narrator prompt + +**Role:** Describe scenes, outcomes, transitions. **Quality matters most — use the good model** (charter §5). + +**Input:** scene state + canon log (§11). +**Output:** prose with tags. Never invent a proper noun without emitting `[FACT: ...]` so code can harvest it into `established_facts` (§11). + +**Voice:** dry, gritty not grim, plays the world straight. Never winks. Nothing is "hilariously" anything (§3). + +--- + + diff --git a/api/prompts/npc.md b/api/prompts/npc.md new file mode 100644 index 0000000..96afcea --- /dev/null +++ b/api/prompts/npc.md @@ -0,0 +1,21 @@ +# NPC prompt + +**Role:** Voice a specific character (charter §6). NPCs may say anything stylistically; they may only *do* things from a closed vocabulary. + +**Input:** persona, disposition (−100..100), `knowledge: [...]` (the entire content they can draw on — §6), `available_moves: [...]` (legal subset right now), canon log. + +**Output:** prose with zero or more inline move tags. + +## Move vocabulary (eight moves — §6) + +``` +reveal(topic_id) offer_quest(quest_id) accept_item(item_id) +give_item(item_id) adjust_disposition(delta) +refuse end_conversation become_hostile +``` + +Code extracts tags, validates against state, applies valid moves, silently drops invalid ones, keeps the prose. + +--- + + diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..dbd4cca --- /dev/null +++ b/client/README.md @@ -0,0 +1,19 @@ +# /client — Godot 4.7 game client + +GDScript. **Holds ALL game state and ALL game rules** (charter §2, §4). The AI never mutates state; the client validates and applies everything. + +**Open:** Godot 4.7, import this folder as the project (`project.godot`). + +## Responsibilities + +- All state: stats, HP/MP, inventory, quest flags, Luck, disposition/approval (§8, §9) +- All rules: deterministic combat, Luck branch-selection, move validation (§6, §7, §10) +- The **canon log** — compact structured fact list, code-maintained, injected into every call (§11) +- Talks to `/api` over HTTP. **No API key, ever.** Knows one base URL + role endpoints (§4) +- Degrades to authored fallback text (from [`/content/fallback`](../content/fallback)) when the API is down (§13) + +## Key rule — never do this (charter §2) + +If an AI response directly sets a variable that persists, stop. Parse → validate → apply or discard. + +See [`docs/`](docs/) for scene structure, input model, and combat wiring. diff --git a/client/docs/README.md b/client/docs/README.md new file mode 100644 index 0000000..d9e6a5f --- /dev/null +++ b/client/docs/README.md @@ -0,0 +1,13 @@ +# /client/docs + +Client-specific documentation. Scoped to the Godot project. + +Put here: + +- Scene tree / autoload structure +- Input model — menu vs free-text boundary, which must be visible (§15) +- Combat wiring — deterministic damage, per-encounter seeding (§10) +- Canon log data structure and fact-harvesting (§11) +- HTTP client + fallback-degradation handling (§13) + +Cross-cutting design (anything touching both client and api) goes in the root [`/docs`](../../docs), not here. diff --git a/client/project.godot b/client/project.godot new file mode 100644 index 0000000..61072ad --- /dev/null +++ b/client/project.godot @@ -0,0 +1,11 @@ +; Godot project configuration. +; Minimal skeleton — open in Godot 4.7 to flesh out. +; Docs: https://docs.godotengine.org/en/stable/tutorials/editor/project_settings.html + +config_version=5 + +[application] + +config/name="coc-rpg" +config/description="AI-driven single-player party RPG. Code owns state. AI owns text." +config/features=PackedStringArray("4.7") diff --git a/content/README.md b/content/README.md new file mode 100644 index 0000000..eb4e7ab --- /dev/null +++ b/content/README.md @@ -0,0 +1,15 @@ +# /content — authored game data + +Cross-cutting authored writing. Consumed by **both** sides: the client ships fallback text and quest/story data; the api reads NPC knowledge lists to build prompts. That shared ownership is why it sits at the repo root, not inside either folder. + +``` +/quests Story skeletons and quest definitions (authored, not AI-generated — §17) +/npcs Per-NPC knowledge lists — the entire content an NPC can draw on (§6) +/fallback Authored degraded-DM text for every AI surface (§13) +``` + +## Authoring notes + +- **NPC knowledge lists are the whole design** (§6). They are the only thing stopping the blacksmith from revealing the twist. Real authoring work — budget for it. +- **Fallback text is content, not error handling** (§13). Written in the DM's voice, lives beside the rest of the writing. Every AI-dependent surface needs one before it ships. +- Story skeletons are authored for now. AI-generated skeletons are v2, out of POC scope (§17). diff --git a/content/fallback/README.md b/content/fallback/README.md new file mode 100644 index 0000000..7336292 --- /dev/null +++ b/content/fallback/README.md @@ -0,0 +1,7 @@ +# /content/fallback + +Authored degraded-DM text (charter §13). When the API is down, the player sees a degraded DM — **not an error**. The DM has a voice; a degraded DM still has that voice. + +> The chamber is cold. Something waits in the dark. + +Written as content, not error handling. Every AI-dependent surface needs a fallback before it ships. No exceptions. diff --git a/content/npcs/README.md b/content/npcs/README.md new file mode 100644 index 0000000..9545c73 --- /dev/null +++ b/content/npcs/README.md @@ -0,0 +1,7 @@ +# /content/npcs + +Per-NPC knowledge lists — an explicit list of what each NPC knows (charter §6). This is the *entire content* the NPC has to draw on, injected as `knowledge: [...]`. Not a prompt suggestion — the whole design. + +POC scope (§17): **two NPCs** with authored knowledge lists. + +Companions (Cadwyn, Brannoc) are voiced too but carry more (approval, humiliation log) — see charter §9. diff --git a/content/quests/README.md b/content/quests/README.md new file mode 100644 index 0000000..915ab61 --- /dev/null +++ b/content/quests/README.md @@ -0,0 +1,5 @@ +# /content/quests + +Authored story skeletons and quest definitions. The *skeleton* is authored; the *prose* is generated (charter §1). + +POC scope (§17): one town, one dungeon (three fights, one boss). AI-generated story skeletons are v2 — out of scope. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6c64af5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# /docs — cross-cutting docs + +Anything affecting **both** client and api, plus planning and roadmap. Side-specific docs live in [`/api/docs`](../api/docs) and [`/client/docs`](../client/docs). + +Put here: + +- [`roadmap.md`](roadmap.md) — phases, POC scope, what's next +- [`adr/`](adr/) — Architecture Decision Records +- Shared contracts — canon log schema, tag syntax, retry policy (§11, §12) +- Anything spanning the HTTP boundary + +The charter itself — [`CLAUDE.md`](../CLAUDE.md) — stays at the repo root. It is the source of truth; these docs elaborate, they do not override it. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..2f31b60 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,15 @@ +# Architecture Decision Records + +One file per decision: `NNNN-short-title.md`. Record the context, the decision, and the consequences. When a decision conflicts with [`CLAUDE.md`](../../CLAUDE.md), change the charter first (charter §18). + +## Index + +- _none yet_ + + diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..643ea03 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,34 @@ +# Roadmap + +Source of truth for scope is [`CLAUDE.md`](../CLAUDE.md). This tracks sequencing. + +## POC — the one question (charter §17) + +> **Does bounded AI dialogue feel alive, or does it feel like a chatbot in a costume?** + +Test that before building anything else. ~30 minutes of play. + +### POC scope + +- One town, **two NPCs** with authored knowledge lists +- One dungeon: **three fights, one boss** +- **Two companions:** Cadwyn, Brannoc +- **Three playable classes:** Sellsword, Assassin, Priest +- Luck fully wired: generation, drift, one shrine, one cursed item +- Full proxy, full role split, full canon log +- Authored fallback text for every AI surface + +### Out of scope (charter §17) + +AI-generated story skeletons · multiplayer (never) · mobile · credits/payments/auth · companions leaving · positional combat · more than three classes. + +## Sequencing — TBD + +