Try it: rpg.earl-mcgowen.com


The itch

Every time I saw an "AI RPG" demo, it was the same shape: one narrator prompt, one chat window, one merged wall of text. Fun for ten turns. Flat after that. None of it felt like something an author was building — it felt like someone was poking a chatbot and calling it a game.

The second itch was more selfish. I wanted a real project — not a toy — to get actual reps with LangGraph, Azure OpenAI as a hosted backend, and Ollama for local models. Story turns are a surprisingly good excuse to build non-trivial graphs: branching logic, state you actually care about, tool-ish nodes, phase transitions. Perfect for learning out loud.

So I built RPG Engine.


What it is

RPG Engine is a self-hostable text-RPG authoring and play platform. Authors define a story — opening, narrator instructions, a player character, an NPC cast — and pick a LangGraph subgraph that acts as the turn-loop "brain." Players chat turn-by-turn, the engine runs the subgraph, and the narrator and each NPC speak in separate bubbles in the same turn, not one merged blob.

Stack is nothing exotic: Python + Flask + LangGraph/LangChain on the backend, SvelteKit on the front, SQLite for state. The interesting parts aren't the boxes, they're what flows between them.


The turn loop is the game design

The thing I'm most happy with conceptually is treating the turn loop as a pluggable subgraph. The engine ships with a few built-ins — narrator_chat, narrator_chat_lite, chat_direct — and authors can drop in their own validated LangGraph JSON. Different pipelines produce genuinely different games from the same story data.

Conceptually a subgraph is just:

{
  "nodes": ["load_state", "summarize_memory", "route_speakers", "narrate", "emit_bubbles"],
  "edges": [
    ["load_state", "summarize_memory"],
    ["summarize_memory", "route_speakers"],
    ["route_speakers", "narrate"],
    ["narrate", "emit_bubbles"]
  ]
}

That's the lever. Want a lighter, cheaper pipeline for a chatty romance story? Different subgraph. Want a combat-heavy one with stricter state checks? Different subgraph. The story doesn't change — the brain running the turn does.

On top of that, the main graph handles phased play via templates: transitions driven by turn count, milestone text, location state, or rule-driven conditions. That's the piece I'll get into properly in a follow-up — it's where most of the "feels like a structured story, not a freeform chat" comes from.

Multi-bubble output turned out to be load-bearing in a way I didn't expect. Once the narrator and each NPC have their own bubble — their own voice, their own turn slot — the fiction stops feeling like one omniscient AI doing impressions. The scene has a cast again.


The dual-provider design constraint

The engine runs against either Azure OpenAI or a local Ollama instance, selected by one LLM_PROVIDER env switch. Same codebase, same prompts, same subgraphs.

This has been the single most useful design constraint in the project. Writing prompts and pipelines that behave reasonably on both a frontier hosted model and a local 7B–14B forces you to stop relying on "the model will just figure it out." Short context windows make you take rolling memory summaries seriously. Smaller models make you break work into smaller nodes with tighter responsibilities. Optional mood tracking — so tone drifts with the fiction — came straight out of local-model prompt-tuning, because you can't just vibe your way to consistent tone on a 7B.

It's also just a good learning setup: I get to see what production-flavored hosted inference looks like (deployments, rate limits, auth) and what it takes to run capable models on my own box, in the same project.


What's working, what's next

Working today:

  • Authoring flow with AI-assist — generate a story draft from a concept, polish fields, suggest titles, names, mood axes.
  • Play loop with multi-bubble narrator + NPC output.
  • Built-in subgraphs plus custom subgraph upload (validated before use).
  • Rolling memory summaries and optional mood tracking.
  • Books — turn completed play history into polished prose via LLM and save it. I didn't expect this to be the feature I'd demo most, but it is.
  • Optional ComfyUI hookup for cover art, character portraits, and scene images.

What I'm still working on, and will write about:

  • A proper deep dive on the LangGraph side — subgraphs, conditional edges, state shape, where I tripped.
  • The main-graph phase machine and why I moved to template-driven transitions.
  • Azure vs. local prompt trade-offs — the same pipeline, same story, side by side.
  • Author ergonomics: making "pick a subgraph" feel like a real design choice and not a jargon trap.

I'm not going to pretend this is finished. It's a learning lab that happens to work.


Try it

If any of this sounds interesting, the easiest path is just to go play one of the built-in stories: rpg.earl-mcgowen.com.

If you'd rather self-host, the engine is designed to run against your own Ollama box or Azure OpenAI deployment — one env var decides. Clone it, point LLM_PROVIDER at whichever backend you've got, author a story, or load one of the built-ins. Setup is in the README.

More posts incoming. If you're poking at LangGraph and feeling like tutorials stop exactly where things get interesting — same. That's most of what I'm writing about next.