Skip to content

PRD: LLM-Driven TUI Tabletop RPG — Game Master #1

Description

@PatrickFanella

Problem Statement

Tabletop RPGs are one of the richest forms of interactive storytelling, but they require a human game master — someone who can improvise narrative, manage world state, portray NPCs, adjudicate rules, and react to unpredictable player choices. Finding a good GM, coordinating schedules, and maintaining campaign continuity across sessions are persistent barriers to play. Existing digital RPG tools either automate away the creative freedom (video game RPGs) or provide only passive reference (digital character sheets, VTTs). There is no tool that provides the improvisational, reactive, narrative-rich experience of a human GM in a solo-playable format.

Solution

Build Game Master — an open-source, terminal-based (TUI) tabletop RPG powered by large language models. The application provides a solo RPG experience where a Go game engine owns all authoritative game state (characters, locations, NPCs, quests, world facts) while an LLM acts as an orchestrated narration and improvisation engine. The LLM receives structured context about the current game state and returns narrative prose alongside structured tool calls that propose state changes. The system uses Postgres with pgvector for persistence and semantic memory retrieval, enabling long-running campaigns with rich contextual recall. The TUI is built with the Charm stack (Bubble Tea) and presents a hybrid interface — narrative text scrolling as the primary view with tabbed secondary views for character sheet, inventory, and quest log. A REST + WebSocket API layer wraps the same core engine library, enabling future web/mobile UIs and multiplayer.

User Stories

  1. As a solo RPG player, I want to start a new campaign by describing my preferences (genre, tone, themes), so that the game world is tailored to what I find interesting.
  2. As a player, I want the LLM to interview me about my character concept during creation, so that my character feels personally meaningful.
  3. As a player, I want to see a narrative description of each scene as I enter it, so that I feel immersed in the world.
  4. As a player, I want to be presented with suggested actions for each situation, so that I know what options are available without breaking immersion.
  5. As a player, I want to type free-text commands in addition to selecting suggested actions, so that I can attempt creative or unexpected solutions.
  6. As a player, I want NPCs to have distinct personalities and remember past interactions with me, so that the world feels alive and reactive.
  7. As a player, I want to explore a world that generates detail on demand as I encounter new locations, so that the world feels vast without requiring everything to be pre-authored.
  8. As a player, I want combat to feel tense and consequential with real stakes (HP, conditions, death), so that dangerous situations create genuine drama.
  9. As a player, I want to choose my combat approach narratively ("I try to disarm them," "I go for the legs") rather than picking from an abstract action menu, so that combat feels like storytelling.
  10. As a player, I want dice rolls and skill checks at pivotal moments, so that randomness adds tension and surprise.
  11. As a player, I want the game to track my quests with short-term, medium-term, and long-term goals, so that I have a sense of direction and progress.
  12. As a player, I want quests to branch and evolve based on my decisions, so that my choices have meaningful consequences.
  13. As a player, I want to check my character sheet, inventory, and quest log without losing my place in the narrative, so that I can reference game state without disrupting flow.
  14. As a player, I want to save and resume campaigns, so that I can play across multiple sessions.
  15. As a player, I want to run multiple campaigns simultaneously and switch between them, so that I can explore different characters and worlds.
  16. As a player, I want the game to remember events from many sessions ago and reference them when relevant, so that the world has long-term continuity.
  17. As a player, I want world lore, factions, belief systems, and cultures to be generated with depth and internal consistency, so that the setting feels believable.
  18. As a player, I want the game to generate cities with districts, economies, and governance systems, so that settlements feel like real places.
  19. As a player, I want NPCs to belong to factions with their own agendas, so that the political landscape creates organic conflict.
  20. As a player, I want the game to create constructed languages and naming conventions for different cultures, so that the world has linguistic texture.
  21. As a player, I want the narrative to stream to my terminal as it generates rather than appearing all at once, so that the reading experience feels natural.
  22. As a player, I want to choose my LLM provider (Claude, OpenAI, Ollama/local models), so that I can use what I prefer or can afford.
  23. As a player, I want the game to work with a local LLM via Ollama, so that I can play without internet or API costs.
  24. As a player, I want established world facts to be respected across all future interactions, so that the narrative doesn't contradict itself.
  25. As a player, I want the game to recover gracefully when the LLM produces an error, so that a bad response doesn't crash my session.
  26. As a future web/mobile user, I want to connect to a running game via an API, so that I can play with a graphical interface.
  27. As a future multiplayer participant, I want to join a campaign hosted by another player, so that I can share the experience.
  28. As a developer, I want the game engine to be a standalone Go library, so that I can build alternative frontends.
  29. As a developer, I want the rules engine to be pluggable behind an interface, so that I can implement D&D 5e, Fate, PbtA, or custom rulesets.
  30. As a developer, I want the project to have CI running unit and integration tests, so that contributions don't break existing functionality.
  31. As a developer, I want to run the entire dev environment with docker compose up, so that setup is effortless.
  32. As a developer, I want sqlc-generated type-safe database code, so that schema changes are caught at compile time.
  33. As a player, I want in-game time to pass and affect the world (day/night, seasons, NPC schedules), so that the world feels dynamic.
  34. As a player, I want my character to gain experience and level up, so that I feel progression over time.
  35. As a player, I want items to have meaningful properties beyond flavor text, so that loot matters to gameplay.

Implementation Decisions

Architecture

  • Engine-as-library: The core game engine lives in internal/engine and exposes a GameEngine interface. Both cmd/tui (Bubble Tea app) and cmd/server (REST + WebSocket API) import and use this same library. The TUI calls the engine in-process; the API server wraps the same calls over HTTP.
  • Provider-agnostic LLM: An LLMProvider interface in internal/llm with implementations for Claude, OpenAI, and Ollama. Tool use / function calling is the integration pattern — the LLM returns narrative prose alongside structured tool calls.
  • Provider-agnostic embeddings: An Embedder interface in internal/memory with an Ollama implementation as the starting default. Used for generating embeddings of turn summaries, lore, and world facts.

Data Layer

  • Postgres + pgvector: All game state is relational (players, NPCs, locations, quests, items, campaigns, world facts, session logs). Narrative memory (turn summaries, lore, dialogue history) is also stored with vector embeddings for semantic retrieval.
  • sqlc + goose: SQL migrations in migrations/, sqlc generates type-safe Go code from queries and schema. goose manages migration execution.
  • Relational vs. semantic retrieval: Game engine decisions (is this NPC alive? what's in inventory?) use standard SQL. LLM context building (what's relevant for this scene?) uses pgvector similarity search.
  • Schema supports multiplayer from day one: users table, foreign keys from players to campaigns, even though only single-player is implemented initially.

Turn Pipeline

  1. Player input (free text or choice selection)
  2. Input classification (game action, meta action, narrative)
  3. State gathering from Postgres (current location, NPCs present, active quests, player status)
  4. Semantic retrieval from pgvector (relevant lore, past interactions, memories)
  5. LLM context assembly (system instructions + structured state + semantic context + player action)
  6. LLM call with tool definitions
  7. Tool call validation and state application (silent post-hoc validation — create freely, validate after)
  8. Narrative response streaming to TUI
  9. Async: summarize turn with rich metadata (time, location, players, NPCs, type) and embed for future retrieval

Context Window Management

  • Tiered system: Always-in-prompt (current scene/state), sliding window of recent turns (dynamic size based on scene type, starting at ~10), semantic retrieval from pgvector, archived (never auto-included).
  • Turn summaries: When turns age out of the sliding window, a cheap/fast LLM call summarizes them into structured memory documents with metadata fields (time, location, NPCs involved, type). These are embedded and stored for semantic retrieval.

LLM Tools (expanded set)

  • Narrative: describe_scene, npc_dialogue, present_choices, inner_monologue, passage_of_time
  • Player character: update_player_stats, add_experience, level_up, add/remove_ability, update_player_status
  • Entity generation: create_npc, create_location, create_city, create_faction, create_lore, create_language, create_belief_system, create_economic_system, create_culture, create_item
  • Inventory: add_item, remove_item, modify_item
  • Quest management: create_quest (short/medium/long term), create_subquest, update_quest, branch_quest
  • World facts: establish_fact, revise_fact, establish_relationship
  • Combat: initiate_combat, combat_round, apply_damage, apply_condition, resolve_combat
  • Dice: roll_dice, skill_check
  • Entity updates: update_npc — with silent post-hoc validation (create freely, fix conflicts silently)

Combat System

  • Structured narrative: Initiative order exists, combatants have HP/stats, but actions are described narratively. Player chooses approach in natural language, LLM narrates and resolves via skill checks.
  • Rules engine interface: CombatResolver interface in internal/combat. Default narrative implementation provided. Interface designed so future rule sets (D&D 5e, Fate, PbtA) can provide their own implementations.

Campaign & World Creation

  • Collaborative: LLM interviews the player about preferences (genre, tone, themes, what they want to explore), then generates a world skeleton (major factions, key locations, central conflict).
  • Skeleton-first, JIT detail: Broad strokes generated upfront, specific locations/NPCs/details fleshed out on first encounter.
  • Path to pre-authored modules: Generated worlds and module-loaded worlds use the same data structures.

API Layer

  • REST + WebSockets: REST endpoints (chi router) for CRUD on game state. WebSocket channel for streaming narrative text during turns.
  • Auth: users table in schema, auth middleware interface in place (no-op for now), to be implemented when multiplayer ships.

TUI

  • Charm stack: Bubble Tea (framework), Lip Gloss (styling), Bubbles (components), Huh (forms/campaign creation).
  • Hybrid layout: Scrolling narrative text as primary view, tabbed secondary views for character sheet, inventory, quest log.

Error Recovery

  • When the LLM returns bad tool calls (invalid IDs, impossible state changes, malformed JSON): retry once with the error fed back, if still bad, skip the tool call and deliver the narrative text. Log failures for debugging.

Configuration

  • koanf: Supports config file + environment variables. Stores LLM provider settings, API keys, database connection, Ollama endpoint, player preferences.

Dev Environment

  • Docker Compose: Postgres+pgvector and Ollama in containers. Go binary runs on host.
  • Taskfile (go-task): task dev, task migrate, task generate, task test.

Key Dependencies

Concern Library
TUI framework charmbracelet/bubbletea
TUI styling charmbracelet/lipgloss
TUI components charmbracelet/bubbles
TUI forms charmbracelet/huh
Postgres driver jackc/pgx/v5
SQL generation sqlc-dev/sqlc
Migrations pressly/goose/v3
HTTP router go-chi/chi/v5
WebSockets coder/websocket
pgvector pgvector/pgvector-go
Logging charmbracelet/log
Config koanf
Local embeddings Ollama HTTP API

Testing Decisions

What makes a good test

Tests should verify external behavior through public interfaces, not implementation details. A test should answer: "does this module do what its callers expect?" If you can refactor the internals without breaking the test, the test is well-scoped. Tests should not depend on LLM output.

Modules under test

  • internal/engine — Unit tests. Mock the LLM provider and state layer. Verify the turn pipeline orchestrates correctly: state is gathered, context is assembled, tool calls are processed, state is applied in the right order. Test error recovery (retry then skip).
  • internal/llm — Unit tests. Parse fixture responses (canned JSON) for each provider format. Verify tool call extraction, streaming chunk assembly, error mapping. No live LLM calls.
  • internal/memory — Unit tests + integration tests. Unit: test context assembly logic, sliding window management, summary metadata extraction. Integration: test pgvector similarity queries with testcontainers.
  • internal/state — Integration tests. testcontainers-go spins up Postgres+pgvector. Run all sqlc-generated queries against real schema. Verify migrations apply cleanly. Test pgvector storage and retrieval.
  • internal/tools — Unit tests. Every tool handler tested with valid and invalid inputs. Verify validation catches bad NPC IDs, impossible state transitions. Verify silent post-hoc validation (deduplication, constraint enforcement).
  • internal/combat — Unit tests. Test round processing, initiative ordering, damage application, condition tracking, combat resolution. Verify the CombatResolver interface contract.
  • internal/rules — Interface contract tests. Any implementation of the rules engine interface must pass this shared test suite.
  • tui/ — TUI tests via teatest. Test view transitions, input handling, rendering of narrative text, tab switching.
  • cmd/server — API tests. HTTP tests against chi router with testcontainers Postgres. Verify REST endpoints return correct state. Test WebSocket connection and message format.

Modules NOT tested (initially)

  • internal/world — Heavy LLM dependency. Generation quality is subjective and non-deterministic. Will be tested indirectly through engine tests with mock LLM responses.
  • pkg/api — Type definitions only. No logic to test.

Out of Scope

  • Pre-authored campaign modules: The system is designed to support them, but no modules will be shipped in the initial implementation.
  • Multiplayer implementation: The schema and auth middleware interface support it, but the actual multiplayer turn coordination, lobby, and multi-client management are deferred.
  • Web or mobile UI: The API enables it, but no frontend client is built.
  • D&D 5e or other mechanical rule set implementations: The rules engine interface is defined, but only the narrative-first default is implemented.
  • LLM integration tests: No tests that make live LLM API calls. Too flaky, too expensive, too slow for CI.
  • Map/grid-based combat: Combat is structured narrative only. No spatial positioning or grid system.
  • Voice or audio: Text-only interface.
  • User account management UI: The users table exists but there is no registration, login, or profile management flow.

Further Notes

  • The project's narrative and worldbuilding skills (game-facilitator, worldbuilding, character-arc, settlement-design, governance-systems, belief-systems, economic-systems, language-evolution, conlang, character-naming, dialogue, etc.) should inform the system prompts and tool designs for world generation. These skills represent codified narrative craft knowledge that can be embedded in the LLM's instructions.
  • The semantic memory system (pgvector) is a core differentiator. Most LLM game projects lose coherence over long campaigns because they rely only on sliding context windows. The combination of relational state + semantic retrieval + rich turn summaries should enable multi-session campaigns with genuine continuity.
  • The provider-agnostic design is important for accessibility. Not every player can afford API costs. Local model support via Ollama makes the project usable by anyone with sufficient hardware.
  • The milestones are ordered to deliver a playable experience as early as possible (Milestone 2), then layer depth and breadth incrementally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions