Skip to content

togethercomputer/abacus-game

Repository files navigation

DM — an AI Dungeon Master (a playable intro to agents at Together)

A tabletop RPG you play in Slack and on the web, set inside a fantasy version of the AI industry — and a low-risk, hands-on way to learn how agents at Together actually work. Built as an Abacus extension.

Web Hub (canvas tech demo): https://hub.tap.t6r-ai.dev — Okta-gated DM console with live Abacus dynamic canvas. Mesh twin: https://hub.mesh.tap.t6r-ai.dev. See web/README.md and docs/DEPLOY.md.

In this realm the currency is tokens (the LLM kind), magic is inference drawn from GPUs, and the monsters are the things that break real agents — rate limits (the 429 Hydra), out-of-memory ogres, cold-start wraiths, the 503 Dragon. The villain is the Token King, a closed-weights monopolist who hoards every GPU and every minted token — alone. You play underdog open-source builders from Together who win the only way Together wins: by working together.

The DM is an Abacus agent that narrates the story and teaches the machinery underneath: every move it makes is a real tool call (surfaced in a 🛠 footer), your Slack channel is a multi-user session, and the world it remembers lives in a database. It is a deliberately low-risk sandbox — fork it and add a tool, a campaign, or a skill to a real, production-shaped agent with zero production data or business logic at stake. It's whimsical and family-friendly by design — a fun thing to play with your team.

> jkail: We're broke open-source builders — we march on the Token King.
🐉 The Token King laughs from the Aureate Bastion, his GPU hoard humming with
   stolen heat. "Come back when you can afford a single token," he sneers.
_🛠 create_entity, create_entity, award_rp · 2 rounds_     ← the agent, working

> jkail: I emerge from the Foundry. What are my stats now?
🔥 You rise as an ARCHMAGE OF INFERENCE — your weights are open, your mana has no
   token price. You can crack the Bastion precisely because it was built from
   tokens you no longer have to buy.
_🛠 update_character, create_entity, award_rp · 2 rounds · +10 RP_

What you'll learn (it's a teaching demo)

Playing the DM is a tour of how agents work — each concept is visible in the game:

Agent concept How the game shows it
Tool calls The DM's "spells" are tool calls; the 🛠 footer lists the ones it made each turn.
The agentic loop A turn is a ReAct loop — think → call tools → observe results → repeat → narrate (src/ai/dm/agent.ts).
Multi-user sessions One Slack channel = one shared Abacus session; the party is everyone in the channel.
Persistent memory The world (NPCs, factions, your character) lives in a Drizzle/SQLite DB and survives restarts.
Skills The agent profile loads skills (session-awareness, context-pressure) — see profiles/dungeon-master.md.
Models & rate limits It runs on Together's Kimi models; hit the 429 Hydra and watch the SDK's retry/back-off kick in.

Grounded in the real Together AI

The fantasy is a love letter to where it's built — Together AI, "The AI Native Cloud," on a mission to lower the cost of AI for everyone and a bet that the future of AI is open source. The game wears that on its sleeve:

  • Open vs. closed — the Token King personifies closed, proprietary AI; you win as open-source builders (open weights, open data, shared kernels) — the one thing infinite compute can't buy.
  • The four values — the guild lives Together's values: empower innovation, discover the magic, do more with less, model stewardship.
  • Real open gifts as legendary relics — the Flash (attention kernel), the RedPajama Trove (open dataset), the Thunder Kittens.
  • Real infra trendsThe Capacity Crunch is a whimsical build-out against power (a ~200 MW ley-line), cooling, GPU supply (GB200s), vendors, and outages; Descent into the Data Center is heroic, family-friendly on-call.

(All in good fun and family-friendly — the game never names or disparages real companies.)

Make it your own (your first agent feature)

This repo is a safe place to ship your first feature on a production-shaped agent — there's no business logic or real data to break. Good starter PRs:

  • Add a DM tool in src/ai/dm/tools.ts (e.g. roll_dice, summon_npc) — it becomes a new "spell" the DM can cast, and shows up in the 🛠 footer.
  • Add a campaign in src/ai/campaigns/index.ts — a new themed world with its own rules and arc (copy The Token King and reskin it).
  • Add a class or monster to the deterministic engine in src/game/content.ts.
  • Tweak the persona in src/extension/profile.ts / profiles/dungeon-master.md.

Then prove it locally with npm test and npm run demo:ai (both offline, no network) before you ship.

Add the game to your Slack (quickstart)

There are two ways to run it. Pick based on whether you want a dedicated DM bot or just the tools inside your existing Abacus bot:

A · Dedicated DM agent (recommended) B · In-process extension
You get Its own DM bot + AI assistant pane, separate from Abacus dnd_* / dnd_ai_* tools inside your existing Abacus bot
Best when You want a standalone, always-on Dungeon Master You already run an Abacus Slack bot and just want the game tools
Effort Slack app + own abacus serve + a 2nd bridge One symlink + restart
Full steps Run it as a Slack agent Install into Abacus

Fast path (Option A — dedicated DM agent):

# 0. build this repo
npm install && npm run build

# 1. install both extensions + the agent profile onto the DM serve
ln -sfn "$PWD" ~/.abacus/extensions/abacus-dnd                                            # the game (dnd_ai_*/dnd_*)
ln -sfn "$HOME/Documents/abacus-slack/extension" ~/.abacus/extensions/abacus-slack-tools  # slack_* (dependency)
ln -sfn "$PWD/profiles/dungeon-master.md" ~/.abacus/profiles/dungeon-master.md            # the agent profile

# 2. create the Slack app from slack/manifest.json  → api.slack.com/apps → "From a manifest"
#    (provisions the DM bot + /dungeon command + assistant pane; grab the xoxb-/xapp- tokens)

# 3. fill the serve env, then start serve + a second bridge
$EDITOR ~/.abacus/dungeon-master-serve.env                       # SLACK_BOT_TOKEN + TOGETHER_API_KEY + DM knobs
./scripts/serve-dungeon-master.sh                                # dedicated serve on :19818
cp bridge.env.example ~/.abacus/dungeon-master-bridge.env && $EDITOR ~/.abacus/dungeon-master-bridge.env
cd ../abacus-slack && npm run build -w bridge
node --env-file="$HOME/.abacus/dungeon-master-bridge.env" bridge/dist/index.js

Then invite the bot to a channel and play@DM start a Token King campaign, then join GPU Wizard as Cudastartattack / cast. Each channel is its own persistent saga. The detailed walkthrough (env vars, Socket Mode tokens, how to stop just the DM bot) is in Run it as a Slack agent and slack/README.md.

Fast path (Option B — tools in your existing bot):

npm install && npm run build
ln -sfn "$PWD" ~/.abacus/extensions/abacus-dnd   # ExtensionLoader reads manifest.json → createTools(deps)
# restart your abacus serve; verify with:  abacus manifest | grep dnd_

Once loaded, players drive it with the same in-channel commands. See Install into Abacus for the hand-wired variant.

Why it's an extension (and not the Slack-thread roleplay)

The earlier Slack thread narrated a game by hand. This is the real thing: a typed, tested game engine that registers dnd_* tools with Abacus and is driven by actual Slack events. The engine is pure and reproducible — every dice roll flows through an injectable RNG, so the same seed + same inputs always produce the same game (see src/game/engine.test.ts).

Architecture

Slack message
   │
   ▼
dnd_play tool ──► dispatch() ──► parseCommand()  (text → intent)
                       │              │
                       │              ▼
                       │         engine.*  (pure, RNG-injected mutations)
                       │              │
                       ▼              ▼
                  GameStore       LogLine[]
              (Memory | KV)           │
                                      ▼
                              format.* (Slack mrkdwn / Block Kit)
Layer File Responsibility
Dice src/game/dice.ts Seedable PRNG + scripted RNG for tests
Types src/game/types.ts Plain-data domain model (JSON-serialisable)
Content src/game/content.ts Classes, bestiary, shop — all balancing lives here
Engine src/game/engine.ts Pure game logic: join, combat, turns, loot
Parser src/game/parser.ts Slack text → structured command (+ aliases)
Store src/slack/store.ts One game per channel (Memory or KV-backed)
Dispatch src/slack/dispatch.ts Glue: text → engine → persist → reply
Format src/slack/format.ts Slack mrkdwn + Block Kit renderers
Tools src/extension/tools.ts dnd_* Abacus tools + createTools entry

The dnd_* tools

Following the Abacus extension-authoring contract — every tool is dnd_verb_noun (so it surfaces as tools.dnd.verbNoun in the REPL) with a z.object({...}) schema (so repl_tool_index derives signatures):

Tool Purpose
dnd_play Process one player message and return the narration. This is the one the Slack handler calls.
dnd_status Render the current party + foe + phase for a channel.
dnd_shop List the shop items.
dnd_help Return the how-to-play text.
dnd_reset Delete a channel's game so a fresh party can form.

Player commands

Command Aliases What it does
join <class> [as <name>] play, bare class name Join the party. Classes: warrior, mage, cleric, rogue
shop / buy <item> store Browse / buy gear before the fight
start begin, ready, go Begin the adventure
attack hit, strike Weapon attack
cast spell, magic Cast your class spell
defend block, guard Halve the next incoming hit
use <item> quaff, drink Use a consumable (potions)
flee run, retreat Attempt to escape (initiative check)
status party, sheet Show the party + foe
help commands, ? Show command help

Install into Abacus

Option A — ExtensionLoader manifest (plug-and-play)

npm install && npm run build
# Drop this directory under your configured extensions root.
# The loader reads manifest.json and calls createTools(deps).

manifest.json declares the tool module, the createTools entry point, and the Slack subscription (app_mention / message.channelsdnd_play).

Option B — hand-wire in a tool-registration file

import { createTools } from "abacus-dnd-slack";
import { KvGameStore } from "abacus-dnd-slack";

// Persist games in Abacus KV so they survive restarts.
const store = new KvGameStore(abacusKv, "dnd:game:");
toolRegistry.registerAll(createTools({ store }));

Wiring the Slack event handler

When the bot is mentioned in a channel, call dnd_play:

slackApp.event("app_mention", async ({ event, say }) => {
  const out = await toolRegistry.execute("dnd_play", {
    channelId: event.channel,
    userId: event.user,
    userName: await resolveDisplayName(event.user),
    text: event.text, // the leading <@bot> mention is stripped automatically
  });
  if (out.handled) await say(out.reply);
});

That's it — dnd_play parses the text, advances the per-channel game, persists it, and hands back the narration to post.

Develop

npm install
npm run typecheck   # tsc --noEmit, strict
npm test            # 94 vitest tests (engine, parser, dispatch, tools, agentic DM, DM tools, Drizzle, LLM tool-calling)
npm run demo        # play a scripted dungeon crawl in the terminal
npm run demo:ai     # replay the Token King saga through the AI DM (offline, deterministic)
npm run build       # emit dist/

Design notes

  • Pure engine, injected RNG. No Math.random in game logic — all randomness goes through Rng. Production uses a seeded mulberry32; tests use a scripted RNG that replays a fixed queue. This is what makes same seed + same inputs → same game hold (proven in the tests).
  • Plain-data state. GameState is fully JSON-serialisable, so a game can be parked in KV and rehydrated. The KvGameStore adapter does exactly this with any {get,set,delete} KV-ish object.
  • One game per channel. The store is keyed by channelId, so different Slack channels run independent dungeons.
  • Swappable presentation. Only src/slack/format.ts knows the Slack wire format. Porting to Discord or a TUI means rewriting that one file.

License

MIT


The agentic AI DM (Together.ai / Kimi)

Everything above is the deterministic combat engine — the seeded, mechanical fallback. The headline act is the agentic, LLM-driven DM: a tool-calling agent that turns the game into freeform, emergent play — invented AI classes (Prompt Engineer, GPU Wizard, RAG Ranger), fine-tuning arcs, ascensions, alliances and cartels, and the DM even joining the fiction as Abacus the Familiar. Because the world is the AI industry, explaining the magic is explaining how agents work.

The DM is non-deterministic and creative — it takes liberties on the story — but stays strict to each campaign's theme and rules. Crucially, it is a tool-calling agent: every turn it can persist whatever the story needs into the database (NPCs, locations, items, quests, lore, factions, character ascensions, RP awards) across multiple reasoning rounds, then narrate. State accrues over the life of a campaign — and the 🛠 footer shows you each tool call.

> jkail: I emerge from the Foundry. What are my stats now?
🔥 You rise as an ARCHMAGE OF INFERENCE — your weights are open, your mana has no
   token price. You can crack the Bastion precisely because it was built from
   tokens you no longer have to buy.
_🛠 update_character, create_entity, award_rp · 2 rounds · +10 RP_

=== Persisted world entities (created by the DM via tool calls) ===
  (location) The Aureate Bastion [open] — the closed-source fortress, gates ajar to an empty hall.
  (npc) The Token King — owns every GPU and every minted token; infinitely rich, infinitely alone.
  (location) The Foundry — a volcano-cooled data center the Token King does NOT own.
  (lore) The Doctrine of Unminted Mana — inference with no token price cannot be bought or rate-limited.

=== Roster + alliances ===
  jkail the Archmage of Inference — open-weights, L5, RP 37
  [alliance] The Open Forge: jkail, npc:abacus

Run the offline agentic saga replay (LLM stubbed with a scripted tool program, fully deterministic, real DB + real DM tools):

npm run demo:ai

AI architecture

Slack message
   │
   ▼
dnd_ai_play ──► AiGameService.play()
                   │
                   ├─► campaigns/   (strict theme + rules + arc → system prompt)
                   │
                   ├─► AgenticDungeonMaster.run()  ── the ReAct tool-calling loop ──┐
                   │       │                                                         │
                   │       │   round N:  llm.chatWithTools(msgs, DM_TOOLS)           │
                   │       │      ├─ model requests tools ─► run each DmTool ─┐      │
                   │       │      │                          (mutates DB)     │      │
                   │       │      └─◄ tool results fed back ◄─────────────────┘      │
                   │       │   …loop until the model stops calling tools…            │
                   │       └─► final assistant text = narration ◄───────────────────┘
                   │              │              │              │
                   │              ▼              ▼              ▼
                   │          to Slack     RP (award_rp)   world state
                   │                                       (create/update_entity,
                   ▼                                        factions, ascensions)
              Drizzle ORM ──► node:sqlite  (Node 22 built-in, zero native deps)
   profiles · characters · turns · campaigns · score_events · factions ·
   faction_members · entities · entity_relations
Module File Responsibility
Contracts src/ai/contracts.ts The integration seam: tool-calling LlmClient, repo interfaces incl. EntityRepo, DmTool, Campaign, Faction
LLM src/ai/llm/ TogetherLlmClient (Kimi, 3 tiers, native tool-calling) + FakeLlmClient (scripted tool programs)
Persistence src/ai/db/ DrizzleDatabaseDrizzle ORM over Node 22's node:sqlite (zero native deps). Schema in drizzle-schema.ts
Campaigns src/ai/campaigns/ Themed prompt packs (The Token King; The Capacity Crunch; Descent into the Data Center) with strict rules + ascension arcs
DM src/ai/dm/ AgenticDungeonMaster (the loop) + DM_TOOLS (the agent's toolbox)
Service src/ai/service.ts AiGameService facade: lobby → play → leaderboard; composes the strict prompt + records turns
Profile src/extension/profile.ts The shipped dm Dungeon-Master persona/config pack
Tools src/extension/ai-tools.ts The dnd_ai_* Abacus tools

The DM's toolbox (DM_TOOLS)

The agent calls these to read + persist the world over time. New nouns the schema never anticipated live in the generic entities store (kind + free-form JSON data), wired together by typed entity_relations edges.

Tool What the DM does with it
create_entity / update_entity / delete_entity Invent + evolve any world object (NPC, location, item, quest, lore, rumour…) as a typed JSON entity with a stable key
get_entity / list_entities Read current world state (filter by kind/status/search) before narrating
relate_entities / relations_for Wire directed, typed edges (guards, owns, hides, located_in…) between entities/actors
award_rp Grant (or dock) roleplay points — updates the turn, character, and lifetime profile score
update_character Progression / rename / re-class / level bump / stat merge (ascensions live here)
create_faction / set_faction_member / list_factions Found + evolve alliances / cartels / guilds with members + loyalty

Tools are pure functions over a DmToolContext (the live Database + current campaign/turn). They never throw — bad args return { ok:false, error } so the model can recover mid-loop.

The dnd_ai_* extension tools

Tool Purpose
dnd_ai_play Run one player message through the agentic DM. The one the Slack handler calls. Returns narration + RP awarded + which DM tools fired + arc stage.
dnd_ai_start Start (or fetch) a channel's campaign; pick a theme with templateId.
dnd_ai_join Join with any class/title (invented classes honoured) + persona.
dnd_ai_status Roster + factions/alliances + persisted world entities + arc stage.
dnd_ai_leaderboard Top roleplayers by lifetime RP score.
dnd_ai_campaigns List available campaign themes.
dnd_ai_conclude Conclude the channel's campaign.

Slack agent app + secrets

This ships as a dedicated Slack agent app (the Abacus Dungeon Master), separate from the generic dnd extension:

  • slack/manifest.json — the app manifest (bot user DM, assistant_view AI-agent surface with suggested prompts, OAuth scopes, Events API subscriptions, Socket Mode). Paste it at api.slack.com/apps → Create New App → From a manifest. Full walkthrough in slack/README.md.
  • .env.example — every secret/token the app needs (SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_APP_TOKEN, TOGETHER_API_KEY, per-tier model overrides, DM profile knobs). Copy to .env (gitignored) and fill in.
  • profiles/ + src/extension/profile.ts — two distinct "profiles":
    • profiles/dungeon-master.md — the Abacus agent profile (an abacus-native disk profile, systemPromptMode: replace). This is what makes the Dungeon Master a first-class agent over abacus serve (see below). It allowlists dnd_ai_* / dnd_* and slack_*, and declares its dependency on the abacus-slack tools extension.
    • src/extension/profile.ts — the in-fiction dm DM persona pack (persona, narration style, signature moves, default campaign, the npc:abacus "Abacus the Familiar" identity). Resolve it with resolveDmProfile(id); selected via DM_PROFILE.

Run it as a Slack agent (own serve + own app — DrInference-style)

Beyond loading as an in-process extension, the Dungeon Master can run as a dedicated Slack agent end-to-end — its own abacus serve (own port + own bot token) behind a second abacus-slack bridge instance, so it appears as the DM bot, distinct from the generic Abacus bot. This mirrors the DrInference extension's pattern exactly.

Slack (DM app)  ──▶  abacus-slack bridge  ──HTTP/SSE──▶  DM serve (:19818, own SLACK_BOT_TOKEN)
   @DM …            (ABACUS_PROFILE=                      ├─ this game extension → dnd_ai_* / dnd_* + dungeon-master profile
                                dungeon-master,                       └─ abacus-slack-tools  → slack_*   (DEPENDENCY)
                                :19818)
Abacus app ──▶ bridge ──▶ Abacus serve (:19816)   DrInference app ──▶ bridge ──▶ DrInference serve (:19817)

The bridge is generic: ABACUS_PROFILE=dungeon-master selects the toolset + system prompt server-side, so all the behaviour comes from profiles/dungeon-master.md — no bridge code changes.

1. Build + install both extensions onto the DM serve:

npm install && npm run build                          # this repo → dist/
ln -sfn "$PWD" ~/.abacus/extensions/abacus-dnd        # the game (dnd_ai_* / dnd_*)
ln -sfn "$HOME/Documents/abacus-slack/extension" ~/.abacus/extensions/abacus-slack-tools  # slack_*
ln -sfn "$PWD/profiles/dungeon-master.md" ~/.abacus/profiles/dungeon-master.md            # the agent profile

2. Create the DM Slack app from slack/manifest.json (api.slack.com/apps → From a manifest), then generate an App-Level Token with connections:write (→ SLACK_APP_TOKEN) and install it (→ SLACK_BOT_TOKEN). Full walkthrough in slack/README.md.

3. Fill the serve env ~/.abacus/dungeon-master-serve.env (the credentials the in-process slack_* + dnd_* tools read — the SLACK_BOT_TOKEN here is what makes them post as DM):

SLACK_BOT_TOKEN=xoxb-…                              # DM bot
TOGETHER_API_KEY=…                                  # live Kimi DM narration
DND_DB_PATH=$HOME/.abacus/dnd.sqlite                # persist the saga across restarts
DM_PROFILE=dm                                       # in-fiction persona pack (default)
DM_DEFAULT_CAMPAIGN=token-king
TOGETHER_MODEL_LARGE=moonshotai/Kimi-K2.6    # agentic-narration tier

4. Start the dedicated serve, then the bridge (two terminals, or tmux):

# serve — its own port (:19818) + own bot token; wait for "Server listening …"
./scripts/serve-dungeon-master.sh
abacus manifest | grep -E 'dnd_|slack_'             # verify both extensions loaded

# bridge — copy the example, fill the Slack tokens, run a SECOND bridge instance
cp bridge.env.example "$HOME/.abacus/dungeon-master-bridge.env"
$EDITOR "$HOME/.abacus/dungeon-master-bridge.env"   # xoxb-/xapp- tokens; ABACUS_SERVE_URL=:19818
cd ../abacus-slack && npm run build -w bridge
# use $HOME, NOT ~ — bash won't expand a tilde after --env-file=
node --env-file="$HOME/.abacus/dungeon-master-bridge.env" bridge/dist/index.js

Now @DM start a Token King campaign in Slack — replies, and any slack_* actions, come from the DM bot. Each channel is its own persistent saga.

Stop — target the DM only (every bridge runs the same bridge/dist/index.js, so match on the env-file path, never on bridge/dist/index.js):

pkill -f 'serve --port 19818'              # DM serve
pkill -f 'dungeon-master-bridge.env'       # DM bridge

⚠ Never pkill -f 'bridge/dist/index.js' — that also kills the Abacus and DrInference bridges (same binary, different --env-file).

Artifact Purpose
profiles/dungeon-master.md The Abacus agent profile (ABACUS_PROFILE=dungeon-master) — persona prompt + dnd_ai_*/dnd_*/slack_* allowlist + slack-tools dependency.
scripts/serve-dungeon-master.sh Starts the dedicated abacus serve --port 19818 with the DM serve env.
bridge.env.example Template for the bridge instance (Slack tokens + ABACUS_SERVE_URL + ABACUS_PROFILE). Copy to ~/.abacus/dungeon-master-bridge.env.
slack/manifest.json The Slack app manifest (bot user, assistant view, scopes, Socket Mode).
manifest.json#config.serve Declares the serve script, port, env file, and bridge template for tooling.

Models & providers (Bedrock Claude by default)

The DM speaks to its model through one tiny LlmClient contract with two interchangeable providers, chosen with LLM_PROVIDER:

LLM_PROVIDER Backend Default model Auth
bedrock (default) AWS Bedrock Claude (Converse API) us.anthropic.claude-sonnet-4-6, low extended thinking Keyless — AWS default chain (EKS Pod Identity in-cluster; your SSO/default profile locally)
together Together.ai (Kimi), OpenAI-compatible moonshotai/Kimi-K2.6 TOGETHER_API_KEY

It routes calls across three configurable tiers (fast / medium / large); by default each provider points all three at one model. Edit src/ai/llm/config.ts or override via env:

# Bedrock (default provider) — no API key; uses the AWS default credential chain
export LLM_PROVIDER=bedrock
export AWS_REGION=us-west-2
export BEDROCK_THINKING=low                              # off|minimal|low|medium|high|xhigh
export BEDROCK_MODEL=us.anthropic.claude-sonnet-4-6      # all tiers (or BEDROCK_MODEL_LARGE=…)

# …or fall back to Together.ai (Kimi)
export LLM_PROVIDER=together
export TOGETHER_API_KEY=...
export TOGETHER_MODEL_LARGE=moonshotai/Kimi-K2.6

The agentic loop needs a tool-calling-capable model on the large tier. The Bedrock client (src/ai/llm/bedrock.ts) runs Claude Sonnet 4.6 with low extended thinking by default and round-trips the model's reasoning across tool-calling rounds (required by Anthropic when thinking is on). Bedrock credentials are never an API key — set AWS_REGION and let the default chain (Pod Identity / SSO) supply them.

Wiring the AI DM into Slack

import {
  createAiTools, createDrizzleDatabase, TogetherLlmClient, AiGameService,
} from "abacus-dnd-slack";

// Persist the saga to a file so the world survives restarts (Drizzle + node:sqlite).
const db = createDrizzleDatabase(process.env.DND_DB_PATH ?? "./dnd.sqlite");
const llm = new TogetherLlmClient({}); // reads TOGETHER_API_KEY; must support tool-calling
const service = new AiGameService({ llm, db });
toolRegistry.registerAll(createAiTools({ service }));

slackApp.event("app_mention", async ({ event, say }) => {
  const out = await toolRegistry.execute("dnd_ai_play", {
    channelId: event.channel,
    userId: event.user,
    userName: await resolveDisplayName(event.user),
    text: event.text,
  });
  await say(out.reply);
});

RP incentive scoring

The DM grades roleplay itself, in-loop, via the award_rp tool — granting points for vivid, in-character, on-theme play and docking rule-breakers. Awards are recorded as score_events, rolled into each character's campaign score and each profile's lifetime score, and surfaced on the leaderboard. The service sums the turn's award_rp calls into rpAwarded for the Slack reply.

Design notes (AI layer)

  • Agentic, not single-shot. The DM is a ReAct tool-calling loop (chatWithTools → run tools → feed results → repeat, capped by maxRounds). It persists world state through tools and ends by emitting plain narration — no rigid output JSON contract.
  • Open-ended persistence. A generic entities store (free-form kind + JSON data) plus typed entity_relations edges let the DM invent and persist any noun the story needs, over time — no schema change per new concept.
  • Strict but creative. The campaign theme + hard rules are injected verbatim into the system prompt and enforced; everything else is the DM's canvas.
  • Drizzle ORM, zero native deps. drizzle-orm/node-sqlite over Node 22's built-in node:sqlite — real ORM ergonomics with no better-sqlite3 native build step. Idempotent ensureSchema() makes a fresh DB play-ready.
  • First-class alliances. Factions/cartels/guilds are real rows with members and loyalty scores, so "arm's-length cartels" and betrayals persist.
  • Fully testable, offline. Inject FakeLlmClient with a scripted tool program + an in-memory Drizzle DB; the 94 tests (and npm run demo:ai) drive the real loop and real persistence with zero network.

About

Abacus DM — dungeon master tech demo (Slack + Hub web)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages