Skip to content

Latest commit

ย 

History

36 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

โš”๏ธ Twin Fates โ€” Ironweave System

A solo-playable, AI-driven tabletop RPG. An AI model sits in the Dungeon Master's chair โ€” narrating the world, voicing every NPC, and deciding what the story needs next โ€” while a Go backend quietly owns every number behind the curtain: HP, dice, damage, inventory, XP, the works. A Python CLI puts it in your hands.

This is a boot.dev capstone project, built solo, end to end โ€” backend, game engine, AI orchestration layer, and CLI client.

๐Ÿง™ Step into a world. Roll the dice. Let the story go somewhere you didn't plan.

๐ŸŽฌ See It In Action

Character Creation: Character creation demo

Roleplay: Roleplay demo

Combat: Combat demo

Sped up for time โ€” but you can still see combat narration streaming in live via SSE as the NPC takes its turn, rather than waiting on one big blocking response of all actions taken.

๐Ÿ’ญ Motivation

There were a few motivations behind this project. In an earlier boot.dev project, I built a heavily D&D-inspired choose-your-own-adventure system that anyone could modify, extend, and play entirely through text in the command line (PythonicDragons). That project got me wanting to build something similar in Go, which I'd just started learning.

Around the same time, I'd been trying out a few AI Dungeon Master-style games โ€” interesting ideas, but usually rough around the edges or locked behind paywalls. I also tried running solo tabletop sessions with regular LLM chatbots, and kept running into the same problem: the AI was constantly fudging numbers, making up rules and outcomes on the fly. There was no real game underneath the story.

So I set out to build my own AI dungeon master โ€” one where the numbers are actually real. That's what became the Golden Rule this project is built around: the AI narrates, but Go owns every roll, every point of damage, every stat.

๐Ÿ† The Golden Rule

If it affects a number, Go owns it. If it affects a sentence, the AI owns it.

The AI never decides how much damage lands, whether a character survives a hit, or what a d20 rolled โ€” those are deterministic and enforced server-side, because letting an LLM "decide" the math would make the game inconsistent and trivially breakable. Instead, the AI narrates, roleplays every NPC, and makes narrative judgment calls (does this moment deserve a roll? does this loot fit this NPC?), then reaches for a fixed toolbox to actually apply outcomes. Go validates and executes every one of those calls against real game state in Postgres. The AI tells the story; it never gets to cheat the dice.

โœจ Features

  • ๐ŸŽญ AI Dungeon Master via OpenRouter, with separate narrative and combat system prompts โ€” and a distinct toolbox scoped to each mode (narrative, combat, character creation), so the AI is never sitting there holding tools that have nothing to do with what it's currently doing.
  • โš”๏ธ Turn-based tactical combat โ€” AP/HP/WP resource management, status effects, per-class skill trees, all validated and resolved server-side, not narrated into existence.
  • ๐Ÿ“– Data-driven classes and fates โ€” classes, skill trees, and narrative "fate" bonuses/drawbacks live as JSON under data/, not hardcoded, so adding new content is a content change, not a code change.
  • ๐ŸŽฒ Guided character creation โ€” a real, step-by-step AI-led flow (race โ†’ class โ†’ fate โ†’ stats โ†’ starting gear), backed by actual tool calls, not free-form text that may or may not stick.
  • ๐Ÿง  Rolling narrative memory โ€” as a campaign grows, older messages get periodically folded into a running narrative summary, so the AI holds onto long-term continuity without the raw message history blowing out its context window. Summarization is append-only: each pass writes one new paragraph onto the story so far, rather than regenerating the whole thing โ€” so earlier chapters never get quietly reworded or lossily compressed the longer you play.
  • ๐Ÿ”€ Model fallback chains โ€” narrative and combat each get their own configured model, and each one lists the other as backup before finally falling through to OpenRouter's free-model auto-router, so one congested or rate-limited model doesn't take the whole session down mid-story.
  • ๐Ÿ” JWT auth with refresh tokens, full campaign/character persistence, and a complete REST API underneath it all.

๐Ÿ—บ๏ธ The World, the Rules, and Building a Character

Every campaign starts with a theme โ€” a setting the AI builds the whole story around. Out of the box this project ships tuned for an arena-fighting theme (step into the ring, roleplay and shop between bouts), but the theme is just a prompt-level setting, so you can point it anywhere: a haunted frontier, a heist ring, a war-torn kingdom. The narrative/combat split and every mechanical system underneath work the same no matter what story you tell.

Classes โ€” three domains, three very different playstyles:

  • ๐Ÿ›ก๏ธ Warrior (physical) โ€” hits hard, takes punishment, outlasts everything. Skills like Power Strike and Adrenaline reward committing to the fight, with talent branches like Walking Armory and Weapon Master to specialize further.
  • โœ’๏ธ Runeblade (hybrid) โ€” combines arcane power with a blade through Etched Runes, sketching magic into the air and detonating it on a physical hit. Branches into paths like Blood and Lightning.
  • ๐Ÿ”ฎ Seer (magical) โ€” an oracle who reads the battlefield, curses enemies, shields allies, and deals steady magic damage from range โ€” naturally squishy, more skills per level to compensate. Branches into Hexblade and Shaman.

Fates โ€” every character picks one Driving Fate (a bonus) and one Binding Fate (a drawback) from their class's domain. A Warrior might become Ironborn (+5 Fortitude, a body that refuses to quit), a Seer might be an Arcane Vessel (+5 Willpower, an innate well of power), and a Runeblade might be Marked by War (+4 Strength/+4 Fortitude, at their best when the fighting's fiercest) โ€” balanced against a real cost elsewhere. Fates are locked in at creation and shape a character's identity beyond just a stat block.

Stats โ€” six stats, all governing something you'll actually feel at the table:

Stat Governs
Strength Physical damage, physical checks
Dexterity Max AP per turn, contributes to AC, agility checks
Fortitude Max HP, contributes to AC (more than Dexterity does), endurance checks
Willpower WP pool, magic damage, magical checks
Alacrity WP regeneration, quick-thinking/fast-talk checks
Wisdom AP overcap, magic defense, perception/insight checks

A Seer who dumps Fortitude and Dexterity is a glass cannon โ€” valid, but the game makes sure you choose that on purpose, not by accident.

๐Ÿค– A Note on AI Model Quality

This project runs on free OpenRouter models by design (see Quick Start below), and model quality varies a lot between them โ€” sometimes dramatically, even between two models that both claim tool-calling support. Narrative flavor and, especially, combat reliability (correct tool sequencing, respecting AP/HP rules, not hallucinating mechanics) have differed noticeably from one free model to the next in testing, and the "best" model can shift over time as providers update or retire them.

If combat starts ignoring rules, dishing out inconsistent damage, or the DM seems to be "forgetting" tools it should be calling โ€” try swapping OPENROUTER_MODEL / OPENROUTER_COMBAT_MODEL before assuming the backend is broken. The tool-calling loop, validation, and state management are all model-agnostic; the model itself is doing all the actual reasoning about when to call what. Combat asks a lot more of a model than freeform narrative does, which is exactly why narrative and combat get independently configurable models instead of one shared setting.

๐Ÿ”’ A Note on Password Security

Password requirements follow current NIST SP 800-63B guidance rather than older composition-rule conventions: a minimum length is enforced, but there's no forced mix of uppercase/numbers/symbols โ€” NIST's latest revision explicitly recommends against those rules in favor of length alone. The minimum is a single constant, easy to tighten or loosen depending on your threat model.

One NIST-recommended practice not implemented here: screening new passwords against known-breached/common-password lists (e.g., via the Have I Been Pwned API). For a single-user hobby project this felt like disproportionate complexity for the actual risk, but it's a real, known gap worth calling out rather than silently leaving out.

๐Ÿ› ๏ธ Tech Stack

  • Backend: Go, net/http (standard library router)
  • Database: PostgreSQL, queries via sqlc, migrations via goose
  • AI: OpenRouter (model-agnostic chat completion + tool calling)
  • CLI Client: Python 3, requests
  • Auth: JWT (access + refresh tokens)

๐Ÿš€ Quick Start

Prerequisites

  • Go (see go.mod for version)
  • PostgreSQL (via Docker or local install)
  • goose for migrations
  • sqlc if you plan to modify queries
  • An OpenRouter API key (free tier works โ€” see the model quality note above)
  • Python 3 + pip for the CLI
  • make โ€” usually pre-installed on Linux/macOS; on Windows, use WSL or install via Chocolatey (choco install make)

Setup

  1. Configure your environment:

    cp .env.example .env

    Fill in .env with your own values. At minimum you'll need a real DB_URL, a generated JWT_SECRET (openssl rand -hex 32), an INTERNAL_SECRET, and your OPENROUTER_API_KEY. OPENROUTER_MODEL and OPENROUTER_COMBAT_MODEL can point at any OpenRouter model ID.

  2. Start Postgres:

    docker compose up -d
  3. Run migrations:

    make migrate-up
  4. Run the server:

    make run

    Starts on the port set by PORT in .env (default 8080).

Other useful Make targets

Command Description
make run Run the server
make run-log Run the server, also tee output to logs/server.log
make build Build a binary to bin/server
make migrate-up / make migrate-down / make migrate-status Manage database schema
make generate Regenerate sqlc code after changing a query
make test Run Go tests
make dbcon Open a psql shell against the configured database

๐Ÿ“– Usage

Once the server's running, play through the Python CLI:

cd cli
pip install -r requirements.txt
python dm_ai.py

By default the CLI connects to http://localhost:8080; override with export DM_AI_URL=http://your-server:8080. register to create an account (or login if you already have one), then new to roll up a character and start your first campaign. See cli/readme.md for the full command list.

๐Ÿ“ก API Overview

All endpoints are JSON over HTTP unless noted. Routes marked auth require a Bearer JWT; routes marked internal require the internal secret and exist for internal testing, not end users.

Method & Path Description
GET /health Health check
POST /auth/register Create an account
POST /auth/login Log in, receive JWT + refresh token
POST /auth/refresh Exchange a refresh token for a new JWT
POST /auth/logout Revoke a refresh token
POST /characters (auth) Create a character
GET /characters (auth) List your characters
GET /characters/{id} Get a character by ID
DELETE /characters/{id} (auth) Delete a character
POST /campaigns (auth) Create a campaign
GET /campaigns (auth) List your campaigns
POST /ai/action (auth) Send a player message; routes to narrative, character creation, or combat (SSE stream) depending on request state
GET /combat/{id} (auth) Get an active combat session's state
GET /characters/{id}/status_effects (auth) Get a character's active status effects
POST /rolls Roll dice (used internally by the AI tool layer, also callable directly)

Internal-only endpoints (requireInternal) cover direct combat/status-effect manipulation and AI test endpoints used during development โ€” these exist to exercise the game engine and tool-calling loop without needing a full campaign flow.

๐Ÿ“ Project Structure

dm-ai-backend/
โ”œโ”€โ”€ cli/               Python CLI client
โ”œโ”€โ”€ data/              Data-driven class/fate definitions (JSON)
โ”œโ”€โ”€ internal/
โ”‚   โ”œโ”€โ”€ ai/            OpenRouter client, prompts, tool definitions, dispatcher, summarization
โ”‚   โ”œโ”€โ”€ auth/          Password hashing, JWT, refresh tokens
โ”‚   โ”œโ”€โ”€ config/        Environment-based configuration
โ”‚   โ”œโ”€โ”€ database/      sqlc-generated queries
โ”‚   โ”œโ”€โ”€ game/          Core game logic: combat, stats, inventory, leveling, dice
โ”‚   โ””โ”€โ”€ server/        HTTP handlers and routing
โ”œโ”€โ”€ sql/
โ”‚   โ”œโ”€โ”€ queries/       Hand-written SQL queries (sqlc input)
โ”‚   โ””โ”€โ”€ schema/        Goose migrations
โ””โ”€โ”€ main.go

๐Ÿ”ญ Future Work

Things on the roadmap beyond the capstone, roughly in the order they'd get tackled(order not set in stone):

  • ๐Ÿ““ Player journaling โ€” a lightweight, player-facing log of key moments, meant to reinforce long-term memory alongside the rolling summary, without the cost of full semantic search.
  • ๐Ÿ“บ Quick-test embedding โ€” a browser/TV-friendly embed (e.g. via ttyd) for fast playtesting without needing a full client install.
  • ๐ŸŽฏ Smarter summarization triggers โ€” swapping the flat message-count threshold for something token-aware, since combat and narrative generate very different token densities per message.
  • ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Multiplayer / couch co-op โ€” the combat engine is already party-aware under the hood; this is the natural next branch (kept separate from the free base).
  • ๐Ÿ–ฅ๏ธ A real front-end โ€” something beyond the CLI, whether a lightweight web client or a Godot-based app, so it's easier to hand to someone who isn't comfortable in a terminal.
  • ๐Ÿ”‘ Breached-password screening โ€” check new passwords against a known-breached-password API (e.g., Have I Been Pwned) at registration time, instead of relying on length alone.
  • ๐Ÿงฌ Embeddings / RAG โ€” proper semantic retrieval over full campaign history, for near-complete long-term recall once the free-tier-friendly approaches above stop being enough.
  • ๐Ÿ“œ Licensing follow-up โ€” exploring dual-licensing (AGPL-3.0 public base + a separate commercial license) if this grows past a pet project.

๐Ÿค Contributing

Clone the repo

git clone https://github.com/AggroSec/dm-ai-backend.git
cd dm-ai-backend

Build the compiled binary

make build

See Quick Start above for full environment setup before building.

Run the test suite

make test

Submit a pull request

If you'd like to contribute, please fork the repository and open a pull request against the main branch.

๐Ÿ“œ License

Licensed under AGPL-3.0. See LICENSE.

About

Capstone project for boot.dev using AI models to act as a DM for tabletop style game, controlling the narrative while having a Golang backend that controls login in, sessions, character creation and storage, gamestate, and handling sending and recieving AI input. See project plan for how its currently planned.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages