A small LLM IF engine
  • Python 76%
  • JavaScript 19.2%
  • CSS 4.7%
Find a file
2026-05-27 19:45:16 -06:00
devtools Signature fix, spacing fix, synthetics expansion, rag write, wiki page list 2026-05-23 14:56:58 -06:00
games Intitial character page, passage system 2026-05-26 22:10:21 -06:00
roughshod Grammar expansion 2026-05-27 19:45:16 -06:00
ui Intitial character page, passage system 2026-05-26 22:10:21 -06:00
.gitignore Massive backend task refactor, small proto refactor 2026-05-09 08:18:03 -06:00
conf.py Prompt adjustments, load scroll QoL, context estimation 2026-05-16 19:47:17 -06:00
dev.py Add dev server, improve error reports 2026-05-06 21:30:12 -06:00
main.py Pipeline family system, stricter RAG guardrails, auto_pov scene mode 2026-05-25 21:29:51 -06:00
pyrightconfig.json Bool setting added, type fixes 2026-05-05 21:08:13 -06:00
README.md README expansion, auto printing tools, prompt adjustments, wiki visibility adjustments 2026-05-24 17:58:10 -06:00
requirements.txt Add dev server, improve error reports 2026-05-06 21:30:12 -06:00
run_tests.sh Add scene summary viewing/editing. Modify role satisfaction to hopefully reduce character bleed 2026-05-17 14:13:50 -06:00
test_all.py Namespace engine, prompt tweaks, additional mixins, gamelib expansion 2026-05-23 21:38:09 -06:00
test_wiki_cow.py Namespace engine, prompt tweaks, additional mixins, gamelib expansion 2026-05-23 21:38:09 -06:00
todo.md Intitial character page, passage system 2026-05-26 22:10:21 -06:00
tsconfig.json Initial commit 2026-04-01 14:01:26 -06:00

Roughshod

Roughshod is an LLM-powered interactive fiction game engine. It's designed to be maximally flexible and hackable, but ships with interesting default behaviors.

Its philosophy is primarily centered around two key aspects.

Information Management

LLMs are terrible at not obliquely referencing content the characters they're playing shouldn't know. Information bleeds across scene boundaries. To solve that issue, Roughshod simply doesn't give them that information.

The context in Roughshod is a dynamic thing that's constantly being modified. Instead of just messages, there's scenes, which have display modes, and when each character speaks, it dynamically builds the context appropriate for that character. For example, by default, it hides any scene that character wasn't in. The player can modify scene display modes.

Messages themselves have shadows and views, where shadows are an abstraction representing any ephemeral tokens that went into a message (such as provider thinking tokens, or any other CoT or RAG tokens), and views are character-specific views of the content of a message.

The underlying theory to all of this is that tokens in the context act as attractors in some high dimensional state space the system lives within. Modifying what's in the context isn't just about what characters know, it's also about shaping the model's writing tone.

Context Management

The other bet that Roughshod makes is that we do not currently have the capability to meaningfully automate context management while keeping the results targeted and useful. As such, Roughshod doesn't manage context on your behalf at all. You are responsible for pulling in scenes as appropriate.

The user experience here is something of an analogy to a DAW. Adjust what scenes are visible until it knows the things you need it to know, but isn't being tonally influenced by things it doesn't need to know. Maybe it still doesn't quite work, add a nudge to the prompt, and regenerate again. Iterate until you find something cool.

Scenes & Display Modes

Scenes are the primary organizational unit. Each scene tracks its own messages and which characters are present. The player controls how each scene appears in the LLM's context by setting its display mode:

  • Auto — include all messages, but only if the speaking character was present in the scene
  • All — include everything regardless of character presence
  • Hidden — silently remove the scene from context entirely
  • Omitted — remove it from context, but leave a marker so the LLM knows something was there
  • Summary — replace the scene with a factual summary
  • POV — replace the scene with a summary written from the speaking character's perspective

When a scene ends, Roughshod generates both a factual summary and per-character POV summaries. These can be viewed and edited from the UI, or backfilled in bulk for scenes that are missing them.

Shadows, Takes, and Views

  • Takes are alternate generations for the same message slot. Generate as many as you want, pick the one you like. You can also clone takes (copy everything), revoice them (keep the shadows, regenerate only the final output), or retry from scratch.
  • Shadows are ephemeral artifacts attached to a take — thinking tokens, internal monologue, narrative plans, RAG retrieval results, game mechanics output, python tool output, etc. They're used during generation but aren't part of the final message content. Each shadow records which model produced it.
  • Views are character-specific renderings of a message's content, used when a character needs to "see" a message differently than what was written.

Technical Aspects

Internally, the engine schedules tasks, routes packets, and executes python. Almost everything else is overridable by the game, if desired. A default set of packet handlers and tasks ship with the engine, so going from zero to something functional can be done quickly. However, if the default engine behavior isn't fitting with what your game wants to do, it's easy to replace.

Architecture

The major moving parts:

  • Engine — the core runtime. Manages scenes, routes WebSocket packets, holds game state, dispatches work to the executor, and owns the LLM spec registry and settings system. Games can override which pipeline handles each engine action (generate, retry, player speak, etc.) by swapping entries in the pipeline registry.
  • Tasks & Pipelines — the execution model. A Task is a discrete unit of work. A Pipeline chains tasks sequentially. The Executor pulls pipelines off a queue and runs them one at a time. Tasks can spawn child tasks dynamically, suspend to wait for user input, and access a shared pipeline context for passing data between stages. Pipelines support cancellation from the UI.
  • Context Assembly — how the LLM prompt gets built. A ContextInfo object gathers the current state (who's speaking, what scenes are visible, what tools are available, phase metadata), and a ContextAssembler transforms that into a message list. Games can hook into assembly at multiple phases (INITIAL, AFTER_VIEWS, AFTER_SHADOWS, AFTER_FORMAT, AFTER_ROLES) or replace the assembler entirely.
  • LLM Layer — pluggable providers behind a common streaming interface. Ships with OpenRouter (OpenAI-compatible), DeepSeek (OpenAI-compatible), and Gemini (native Google GenAI SDK). Each provider supports both sync and async generation with streaming, thinking/reasoning token extraction, and detailed timing/throughput logging. The engine maintains a registry of LLMSpec objects mapping task names to provider/model/parameter combinations. Characters and tasks can override which LLM they use. Specs are fully manageable from the UI (add, update, delete) and persist across saves.
  • Protocol — WebSocket-based, framed JSON packets between a JS frontend and Python backend. The client sends requests; the server responds with state updates, token streams, and completion signals. Packets are terminated with ServerOK or ServerError. The protocol supports fan-out to multiple connected clients. Games can register custom packet handlers.
  • Wiki — a markdown-backed knowledge base (in roughshod/gamelib/wiki.py). Pages support frontmatter-controlled per-character visibility (whitelist/blacklist/tag-based), internal linking, and keyword search. An overlay filesystem allows per-character writable layers on top of a shared read-only base, with whiteout support for deletions. Characters can search, read, write, and update wiki pages via tools during generation.
  • Tools — characters can execute Python during generation via a sandboxed environment. The sandbox whitelists safe stdlib modules, blocks open(), exec(), eval(), and dangerous builtins, and captures printed output. Games and characters both contribute tools (dice rolls, wiki access, user prompts, D&D-style skill checks, emotion sensing, mind reading, character listing, etc). Tool code runs in a thread and feeds results back into the generation loop. Tools can also prompt the user for input mid-generation via a thread-safe waiter mechanism.

The Generation Pipeline

Roughshod ships with two pipeline families. The active default is the RPGTS (RAG-Plan-Game-Think-Speak) pipeline:

  1. Speaker selection — optionally, a lightweight model picks who speaks next based on the conversation state. For two-character scenes it alternates; for larger scenes it uses an LLM call.
  2. Message creation — a blank message is created and slotted into the active scene
  3. RAG (optional) — a retrieval phase where the character can search knowledge bases and gather information, stored as a shadow
  4. Narrative Planning (optional) — the character plans out what would be interesting to do next, stored as a shadow
  5. Game Mechanics (optional) — dice rolls, stat checks, and other mechanical resolution via tool calls, stored as a shadow
  6. Monologue (optional) — the character's internal thoughts are generated and stored as a shadow
  7. Speak — the final generation, with all shadow data available in context as <extra> blocks
  8. RAG Write (optional) — post-generation, the character can write important information back to its knowledge base

The older CTS (Code-Think-Speak) pipeline is also available, which only runs Python tools, monologue, and speak.

Each stage is a Task that can be swapped, skipped, or extended by the game. Every optional stage is controlled by an engine setting (iteration counts for tool phases, toggles for monologue and planning). The pipeline also tracks phase numbering so each task knows where it sits in the sequence (e.g., "phase 3/6"), which is injected into the LLM prompts.

When auto-generation is enabled, the pipeline chains into speaker selection again after each response, creating a round-robin conversation up to a configurable maximum.

Game Structure

A game is a directory under games/. The only hard requirements:

  1. It must contain a game.py with a subclass of Game.
  2. Character classes (subclasses of Character) can live in the game root or in a characters/ subdirectory. The engine auto-discovers them. If you have an intermediate base class that shouldn't be instantiated, set _abstract = True on it.

Everything else — how you organize portraits, character data files, custom tasks, tools — is up to you.

Characters

Characters define an id, display_name, and optionally capabilities (defaults to {'python', 'monologue'}) and tags (a set of strings used for wiki visibility and other filtering). Characters can:

  • Override llm_for(task) to use a different model for specific generation phases
  • Provide get_snippet(info, slot) to inject character descriptions into prompts
  • Return custom get_tools(task) for task-specific tool availability
  • Define get_portrait() to provide a portrait image path
  • Implement save() / load() for persistent character state

Data-Defined Characters

For games with many characters, roughshod/gamelib/datacharacter.py provides DataDefinedCharacter — a character class that loads its identity (display name, tags, portrait) from JSON files in a lore directory rather than requiring a Python class per character. Pair it with the SnippetMixin and LoreRegistry to load character descriptions from JSON snippet files.

Game Hooks

Games can customize behavior without replacing the whole system:

  • get_context_assembler() — replace the context assembler entirely
  • preassemble_hook(info) — modify ContextInfo before assembly begins
  • assemble_hook(phase, info, context) — modify the message list during assembly at specific phases
  • get_tools(task) — provide game-level tools available to all characters
  • setting_changed(id, old, new) — react to runtime setting changes
  • message_handlers — register custom WebSocket packet handlers
  • setup() / save() / load() — lifecycle hooks for initialization and persistence

Gamelib

The roughshod/gamelib/ package provides reusable building blocks:

  • Wiki (wiki.py) — full wiki system with overlay filesystem, frontmatter visibility, search, and ready-made tools (WikiSearchTool, WikiGetPageTool, WikiUpdatePageTool, WikiWritePageTool). Includes WikiGameMixin for easy integration into games.
  • D&D (dnd.py) — StatsMixin for D&D-style attributes/modifiers/armor class, and DnDRollTool for skill checks and attack rolls with optional user prompts for result narration.
  • Extra Tools (extratools.py) — ReadMindTool (read a character's monologue shadow verbatim) and EmotionSenseTool (LLM-summarized emotional read with configurable flavor: accurate, low-fi machine, or low-fi magic).
  • Data Characters (datacharacter.py) — DataDefinedCharacter, SnippetMixin, and LoreRegistry for JSON-driven character definitions.

Built-in Tools

The engine ships with several tools in roughshod/tool.py:

  • DiceRollTool — parse and roll dice formulas (e.g. 2d20+5)
  • ListCharactersTool — list characters in the current scene
  • UserPromptTool — ask the user a question mid-generation and wait for a response

Getting Started

pip install -r requirements.txt

Set up a .env with your API keys (OpenRouter, DeepSeek, Google — whatever providers you're using):

OPENROUTER_KEY=sk-or-...
DEEPSEEK_KEY=sk-...
GEMINI_KEY=...

Run the server:

python main.py

Or, for development with auto-reload on file changes:

python dev.py

Then open http://localhost:8180 in a browser. The server supports multiple simultaneous WebSocket clients with fan-out.

Configuration

Engine defaults can be overridden by creating a conf.py in the project root. Any uppercase attribute defined there will override the corresponding value on the Config object:

Setting Default Description
DEBUG False Enable debug logging and test endpoints
HOST 'localhost' Server bind address
PORT 8180 Server port
TPS 60 Ticks per second
GAME_IMAGE_DIR ./games Where game directories live
GAME_SAVE_DIR ./saves Where saves are stored
DUMP_ALL_INTERMEDIATE False Dump all LLM requests to ./dumps as JSON
DUMPS_DIR ./dumps Directory for intermediate dumps

At runtime, the engine also exposes a settings system. Engine settings (like toggling monologue, narrative planning, tool iteration counts, max auto-responses, and autosave) and game-defined settings (like custom sliders or dropdowns) are all surfaced in the UI and can be changed on the fly. LLM model assignments per task are also configurable from the UI.