A chat-based virtual TTRPG platform where an AI acts as Dungeon Master.
The system splits into a Master node — the only component holding LLM
provider credentials and authoritative game state — and any number of thin
Slave clients that render UI and carry a network connection, with no
rules engine or provider credentials of their own. Everything mechanically
consequential (dice, rules resolution, PvP, character validation, content
policy) is a gate enforced in code at the tool-call layer, not something
left to the model's discretion. See docs/design.md for
the full design document — architecture, protocol, extensibility
interfaces, and governance model.
layforge.org is the project's public site: a
short pitch plus links to the Master redistributable/source on GitHub,
and a live directory of currently-joinable self-hosted campaigns (see
registry/README.md) — no central server your game
depends on, purely opt-in discovery for a Host who wants their table
listed.
- Go 1.24+ — the only hard requirement. Master (the only code so far) compiles to a single static binary with no other runtime dependency.
protoc+ the Go protobuf/gRPC plugins, but only to generatemaster/internal/systemenginepb/: those files are gitignored, not checked into this repo, and imported unconditionally — a fresh clone will not build until you runprotocol/generate.shonce (see Quick Start below; the script prints install commands for anything missing).- Everything else is optional, needed only for the feature it powers —
see
master/README.mdfor the full list (a System Engine sidecar, an Ollama server, a ComfyUI instance). No Node/npm/bundler is needed for either web client — both are hand-written HTML/CSS/JS with no build step.
git clone https://github.com/jamesplotts/layforge.git
cd layforge
./protocol/generate.sh # one-time: generates master/internal/systemenginepb/
cd master
go run .
Then open http://localhost:8080/. See
master/README.md for every flag — LLM/System-Engine/
ComfyUI endpoints, room passwords, the admin panel, and what each
optional dependency unlocks.
If you use the admin panel's "Save & Restart" while still running via
go run ., your terminal will look like the server died (a real,
confirmed go run .-specific quirk — see master/README.md's own
"go run . caveat") — it almost certainly didn't; check
http://localhost:8080/ before assuming something broke.
go run . is fine for a first look, but it recompiles on every
invocation and its self-restart behavior is confusing (see the caveat
just above) — build an actual binary instead for anything beyond a
quick test:
cd master
go build -o master .
./master
This produces a single static executable (master/master) with no
runtime dependency beyond what you explicitly configure (an Ollama
server, a System Engine sidecar, etc. — all optional). It looks for its
web/admin-web directories next to itself, so keep those two folders
alongside the binary if you move it elsewhere:
cp -r master/master master/web master/admin-web /wherever/you/want/
cd /wherever/you/want/
./master
Cross-compiling for another platform is standard Go — no special flags
this project needs beyond the usual GOOS/GOARCH:
GOOS=linux GOARCH=amd64 go build -o master-linux-amd64 .
GOOS=darwin GOARCH=arm64 go build -o master-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o master-windows-amd64.exe .
For a real, always-on deployment (not just running it in a terminal),
see registry/README.md
for a working systemd-unit example — the same pattern applies to
master, not just registry/.
Master is a single static binary plus plain files on disk — nothing in this Quick Start touches a system-wide location, a package manager, or a registry, so removing it is just deleting what you created:
- Stop the running process (
Ctrl+C, orsudo systemctl stop layforge-master/disable it if you set it up as a service — see below). - Back up
layforge.dbfirst if you want to keep anything — every campaign, character sheet, and adventure event Master has ever recorded lives in that one SQLite file (design doc §10), along with the admin panel's own settings. It defaults to sitting inside the working directory you rango run ./the compiled binary from, so the next step deletes it right along with everything else, with no separate confirmation and no recovering it afterward. Copy it somewhere first (cp layforge.db ~/layforge-backup.db) if that data matters to you, or open it withsqlite3 layforge.dbto export specific tables. - Delete the cloned directory (
rm -rf layforge). This removes the binary, the generatedmaster/internal/systemenginepb/stubs, the web client assets, andlayforge.dbitself (per the warning just above). If you pointed-db,-web-dir, or-admin-web-dirat a path outside the clone, remove those separately too.
That's the whole self-hosted footprint — no admin-panel data, campaign state, or credentials live anywhere else.
Two things Quick Start installs that are not Layforge-specific and are shared with any other Go/protobuf project on your machine — leave these alone unless you're sure nothing else needs them:
protoc-gen-go/protoc-gen-go-grpc(go installed into$(go env GOPATH)/bin) — remove withrm "$(go env GOPATH)/bin/protoc-gen-go" "$(go env GOPATH)/bin/protoc-gen-go-grpc".protobuf-compiler(installed via your OS package manager, e.g.sudo apt remove protobuf-compileron Debian/Ubuntu).
If you deployed Master (or the registry/ service) as a real systemd
unit — following the pattern documented in
registry/README.md —
uninstalling means reversing that setup on whatever machine runs it,
same as for any systemd service:
sudo systemctl disable --now <unit-name>
sudo rm /etc/systemd/system/<unit-name>.service
sudo systemctl daemon-reload
then delete the deployed binary/web/ directory and, if you set up an
Apache vhost fronting it, remove that vhost file and reload Apache too.
Master picks which LLM narrates and runs the DM tool-use loop via
-llm-provider (or the admin panel's System tab, live) — one of:
| Provider | -llm-provider |
Needs |
|---|---|---|
| Ollama (self-hosted) | ollama (default) |
-llm-url only, no API key |
| Anthropic (Claude) | anthropic |
-llm-api-key |
| OpenAI (ChatGPT) | openai |
-llm-api-key |
| OpenRouter | openrouter |
-llm-api-key |
| Z.ai | zai |
-llm-api-key |
Every provider but Ollama talks to its own published default endpoint
unless -llm-url overrides it (e.g. a self-hosted OpenAI-compatible
gateway). -llm-model takes whatever model name/tag the chosen provider
expects — an Ollama tag like qwen3.8:27b, or a vendor model id like
claude-opus-5/gpt-5/glm-4.6. Credentials never leave Master: no
Slave client ever receives an API key, and the admin panel that sets one
is itself a local-only listener (design doc §3.3). See
master/README.md for the full flag
reference.
Multiple players can connect to the same campaign, chat, and get real
AI-narrated responses: the system.connect handshake, then Master routes
messages between everyone connected — safety.flag broadcasts to the
whole table (design doc §9.2), and narrative.player_input renders
through an LLM into narrative.player_bubble (§7's fast pass only, no
DM/NPC reaction pass yet). Everything exchanged is durably logged to
SQLite, with bidirectional history paging — join and see what's recent,
page back further for scrollback. The V1 web client is served by Master
itself by default, straight from disk so a table can restyle it without
touching Go or rebuilding anything (see master/web/README.md).
Master can now dial a real OpenCombatEngine gRPC sidecar
(-system-engine-addr) and calls it for real: uploading a character
(character.upload) gets mechanically parsed and validated by the engine
and persisted, with the engine's warnings sent back
(character.validation_result, design doc §9.4's mechanical half), and a
player can roll an authoritative check for a character they own
(roll.check_request), with the real outcome — including individual
dice, not just a total — broadcast to the whole table as
roll.request/roll.result, animated on a real WebGL d20 (three.js +
cannon-es physics) with a swappable community-skin system in the web
client. A player can also read back their own character's current data
and mechanical status, rendered as a read-only sheet generated directly
from the system engine's own JSON Schema — no D&D-specific fields
hardcoded into the UI — and apply a real effect (damage/heal) to it,
persisted server-side and reflected back in the sheet. A DM/NPC reaction
pass (§7's slow pass) now runs after the fast pass: an LLM narrates what
happens next and can call real tools — resolve_check, apply_effect,
get_character_status (design doc §8) — against the same System Engine
RPCs already wired for players, with every call broadcast to the table as
tool.result and the final reaction as narrative.dm_prose. The DM can
also start real structured turn order (start_combat) — rolling
initiative through the System Engine rather than trusting the model to
order it — broadcast to everyone as turn.state, and now actually
enforced on players too: once combat is active, a player's own roll or
effect on their character is rejected unless it's currently that
character's turn (advance_turn, design doc §9.3). An unconscious/dying
character isn't skipped — they get a turn that automatically rolls a
death saving throw instead (a real System Engine RPC, StartTurn,
newly added so OpenCombatEngine's own SRD death-save logic — previously
built but unreachable — is actually wired up), broadcast as a genuine
roll just like any other. The DM can also give a narrated monster/NPC a
real
mechanical presence on the fly — create_npc (after get_character_schema
so the model authors a document that actually matches the campaign's
schema, never a guessed shape) persists it the same way a player's own
character upload does, so it can then be referenced by resolve_check,
apply_effect, or start_combat like any other character. A campaign
can now also configure a real PvP policy — pve_only/pvp_allowed
— that mechanically gates whether the DM can damage
one player's character on another's behalf, and a maturity-tier text
constraint injected into DM narration, both via a per-campaign JSON
config file (-campaign-policies); an unconfigured campaign gets the
strictest PvP setting by default, not an open one. Turns are now
mechanically tied to real rules too: landing a turn on any non-dead
character automatically rolls a death saving throw for one that's
unconscious/dying (a real System Engine RPC, StartTurn) — SRD's own
"deterministic bookkeeping, not something the DM has to remember," now
actually wired up rather than sitting unreachable in the engine. The DM
can also illustrate a scene — generate_scene_image calls a pluggable
image-gen provider (a self-hosted ComfyUI instance is the reference
implementation) and broadcasts the result to the table, now verified
live against a real running ComfyUI instance. A local-only admin/operator
settings panel now exists too — a second, 127.0.0.1-only listener
serving a tabbed web UI (Campaign/Security/System) for changing PvP
policy, maturity-tier prompts, and room passwords live, or process-level
settings (LLM/System-Engine/ComfyUI endpoints, listen address) via a
self-triggered graceful restart — that restart now genuinely applies a
saved System-tab change (a real gap fixed this pass: the settings
database wasn't actually re-read at boot before). Master can also opt
individual campaigns into a public directory at
layforge.org (registry/, a separate standalone
service) — a Host explicitly lists a campaign's adventure name, level
range, and player count; nothing is published without that per-campaign
opt-in, and a listing self-heals/expires on its own if Master stops
checking in. Narration and the DM tool-use loop can now also talk to a
hosted LLM provider — Anthropic, OpenAI, OpenRouter, or Z.ai — instead of
only a self-hosted Ollama server (see Model Providers above); credentials
for whichever one is configured never leave Master. Master now also
gates on a Host/operator terms acceptance (once, via the admin panel or
a scripted -accept-terms-version flag) and a per-connection player
disclaimer acceptance before processing any real message — see
master/README.md's Running section. The admin panel can also generate
a full campaign pack from a one-paragraph description via the
configured LLM, reviewed and editable before anything is written to
disk — live-verified against a real local model, which also surfaced
and fixed four real reliability issues (see master/README.md's
Status section). A
player's own roll still doesn't apply its own damage outside the DM
pass, there's no human review step on imported characters (that needs an
account/operator concept the admin panel doesn't cover yet), and no full markdown
campaign-pack directory tree (§6.4) — policy configuration today is a
flat JSON file, not campaign.md front matter. See
master/README.md for the full picture, including
exact roadmap gaps.
master/ Master process (Go) — session orchestration, turn-order
state machine, authoritative dice, tool-use dispatch,
and (master/web/) the V1 chat/player client it serves
registry/ Standalone Go service behind layforge.org — the public
campaign directory and project homepage
protocol/ AsyncAPI spec for the client-facing WebSocket protocol,
plus the System Engine gRPC/protobuf contract
campaign-packs/ Directory-based campaign content (markdown + YAML) —
sable-ravine/ is a full worked example, TEMPLATE/ an
annotated skeleton to copy
maturity-tiers/ Content-maturity tier definitions
docs/ Design document and supporting docs, including
authoring-campaign-packs.md
To write your own adventure, copy campaign-packs/TEMPLATE/ and follow
docs/authoring-campaign-packs.md.
Ready-made packs are published at
layforge.org and can be pulled straight onto a
Master from the admin panel's Campaign tab.
jamesplotts/opencombatengine— the D&D SRD-compatible system-engine reference implementation this harness plugs into over gRPC (see §6.1 and §12 of the design doc).
MIT for code. Game-mechanics content generated by or shipped with this project is intended to stay SRD-legal (see §6.4 and §12 of the design doc) — it does not reproduce proprietary published material.
- Author: James Duane Plotts
- Repository: https://github.com/jamesplotts/layforge
- Issues: https://github.com/jamesplotts/layforge/issues
- Donate: https://buymeacoffee.com/jamesplotts