- Python 76%
- JavaScript 19.2%
- CSS 4.7%
| devtools | ||
| games | ||
| roughshod | ||
| ui | ||
| .gitignore | ||
| conf.py | ||
| dev.py | ||
| main.py | ||
| pyrightconfig.json | ||
| README.md | ||
| requirements.txt | ||
| run_tests.sh | ||
| test_all.py | ||
| test_wiki_cow.py | ||
| todo.md | ||
| tsconfig.json | ||
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
ContextInfoobject gathers the current state (who's speaking, what scenes are visible, what tools are available, phase metadata), and aContextAssemblertransforms 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
LLMSpecobjects 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
ServerOKorServerError. 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:
- 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.
- Message creation — a blank message is created and slotted into the active scene
- RAG (optional) — a retrieval phase where the character can search knowledge bases and gather information, stored as a shadow
- Narrative Planning (optional) — the character plans out what would be interesting to do next, stored as a shadow
- Game Mechanics (optional) — dice rolls, stat checks, and other mechanical resolution via tool calls, stored as a shadow
- Monologue (optional) — the character's internal thoughts are generated and stored as a shadow
- Speak — the final generation, with all shadow data available in context as
<extra>blocks - 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:
- It must contain a
game.pywith a subclass ofGame. - Character classes (subclasses of
Character) can live in the game root or in acharacters/subdirectory. The engine auto-discovers them. If you have an intermediate base class that shouldn't be instantiated, set_abstract = Trueon 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 entirelypreassemble_hook(info)— modifyContextInfobefore assembly beginsassemble_hook(phase, info, context)— modify the message list during assembly at specific phasesget_tools(task)— provide game-level tools available to all characterssetting_changed(id, old, new)— react to runtime setting changesmessage_handlers— register custom WebSocket packet handlerssetup()/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). IncludesWikiGameMixinfor easy integration into games. - D&D (
dnd.py) —StatsMixinfor D&D-style attributes/modifiers/armor class, andDnDRollToolfor skill checks and attack rolls with optional user prompts for result narration. - Extra Tools (
extratools.py) —ReadMindTool(read a character's monologue shadow verbatim) andEmotionSenseTool(LLM-summarized emotional read with configurable flavor: accurate, low-fi machine, or low-fi magic). - Data Characters (
datacharacter.py) —DataDefinedCharacter,SnippetMixin, andLoreRegistryfor 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 sceneUserPromptTool— 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.