dndkit 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dndkit-0.4.0/LICENSE +22 -0
- dndkit-0.4.0/MANIFEST.in +2 -0
- dndkit-0.4.0/PKG-INFO +567 -0
- dndkit-0.4.0/README.md +535 -0
- dndkit-0.4.0/pyproject.toml +87 -0
- dndkit-0.4.0/setup.cfg +4 -0
- dndkit-0.4.0/src/dndkit/__init__.py +10 -0
- dndkit-0.4.0/src/dndkit/__main__.py +6 -0
- dndkit-0.4.0/src/dndkit/activity.py +68 -0
- dndkit-0.4.0/src/dndkit/app.py +105 -0
- dndkit-0.4.0/src/dndkit/config.py +104 -0
- dndkit-0.4.0/src/dndkit/errors.py +48 -0
- dndkit-0.4.0/src/dndkit/game/__init__.py +1 -0
- dndkit-0.4.0/src/dndkit/game/areas.py +181 -0
- dndkit-0.4.0/src/dndkit/game/battlefield.py +154 -0
- dndkit-0.4.0/src/dndkit/game/bestiary.py +155 -0
- dndkit-0.4.0/src/dndkit/game/characters.py +227 -0
- dndkit-0.4.0/src/dndkit/game/combat.py +209 -0
- dndkit-0.4.0/src/dndkit/game/combatants.py +188 -0
- dndkit-0.4.0/src/dndkit/game/concentration.py +43 -0
- dndkit-0.4.0/src/dndkit/game/dice.py +186 -0
- dndkit-0.4.0/src/dndkit/game/effects.py +183 -0
- dndkit-0.4.0/src/dndkit/game/grid.py +88 -0
- dndkit-0.4.0/src/dndkit/game/journal.py +47 -0
- dndkit-0.4.0/src/dndkit/game/movement.py +264 -0
- dndkit-0.4.0/src/dndkit/game/pathing.py +153 -0
- dndkit-0.4.0/src/dndkit/game/proficiencies.py +104 -0
- dndkit-0.4.0/src/dndkit/game/progression.py +77 -0
- dndkit-0.4.0/src/dndkit/game/readiness.py +214 -0
- dndkit-0.4.0/src/dndkit/game/reference.py +123 -0
- dndkit-0.4.0/src/dndkit/game/registry.py +172 -0
- dndkit-0.4.0/src/dndkit/game/resources.py +96 -0
- dndkit-0.4.0/src/dndkit/game/rests.py +98 -0
- dndkit-0.4.0/src/dndkit/game/rules.py +196 -0
- dndkit-0.4.0/src/dndkit/game/seating.py +71 -0
- dndkit-0.4.0/src/dndkit/game/sight.py +142 -0
- dndkit-0.4.0/src/dndkit/game/spellbook.py +97 -0
- dndkit-0.4.0/src/dndkit/game/tables.py +216 -0
- dndkit-0.4.0/src/dndkit/game/world.py +125 -0
- dndkit-0.4.0/src/dndkit/models/__init__.py +69 -0
- dndkit-0.4.0/src/dndkit/models/base.py +26 -0
- dndkit-0.4.0/src/dndkit/models/battle.py +40 -0
- dndkit-0.4.0/src/dndkit/models/bestiary.py +56 -0
- dndkit-0.4.0/src/dndkit/models/campaign.py +209 -0
- dndkit-0.4.0/src/dndkit/models/character.py +149 -0
- dndkit-0.4.0/src/dndkit/models/world.py +62 -0
- dndkit-0.4.0/src/dndkit/skills/__init__.py +14 -0
- dndkit-0.4.0/src/dndkit/skills/catalog.py +113 -0
- dndkit-0.4.0/src/dndkit/skills/dnd-battle-map/SKILL.md +78 -0
- dndkit-0.4.0/src/dndkit/skills/dnd-game-master/SKILL.md +125 -0
- dndkit-0.4.0/src/dndkit/skills/dnd-monsters/SKILL.md +85 -0
- dndkit-0.4.0/src/dndkit/skills/extension.py +93 -0
- dndkit-0.4.0/src/dndkit/storage/__init__.py +6 -0
- dndkit-0.4.0/src/dndkit/storage/catalog.py +72 -0
- dndkit-0.4.0/src/dndkit/storage/confirmations.py +54 -0
- dndkit-0.4.0/src/dndkit/storage/files.py +55 -0
- dndkit-0.4.0/src/dndkit/storage/sharing.py +67 -0
- dndkit-0.4.0/src/dndkit/storage/store.py +278 -0
- dndkit-0.4.0/src/dndkit/text.py +13 -0
- dndkit-0.4.0/src/dndkit/tools/__init__.py +47 -0
- dndkit-0.4.0/src/dndkit/tools/apps.py +47 -0
- dndkit-0.4.0/src/dndkit/tools/battle.py +117 -0
- dndkit-0.4.0/src/dndkit/tools/bestiary.py +121 -0
- dndkit-0.4.0/src/dndkit/tools/campaign.py +208 -0
- dndkit-0.4.0/src/dndkit/tools/characters.py +189 -0
- dndkit-0.4.0/src/dndkit/tools/combat.py +169 -0
- dndkit-0.4.0/src/dndkit/tools/deps.py +42 -0
- dndkit-0.4.0/src/dndkit/tools/journal.py +102 -0
- dndkit-0.4.0/src/dndkit/tools/locations.py +98 -0
- dndkit-0.4.0/src/dndkit/tools/npcs.py +101 -0
- dndkit-0.4.0/src/dndkit/tools/play.py +118 -0
- dndkit-0.4.0/src/dndkit/tools/players.py +44 -0
- dndkit-0.4.0/src/dndkit/tools/progression.py +41 -0
- dndkit-0.4.0/src/dndkit/tools/quests.py +95 -0
- dndkit-0.4.0/src/dndkit/tools/recorder.py +63 -0
- dndkit-0.4.0/src/dndkit/tools/render.py +281 -0
- dndkit-0.4.0/src/dndkit/tools/render_combat.py +121 -0
- dndkit-0.4.0/src/dndkit/tools/render_map.py +141 -0
- dndkit-0.4.0/src/dndkit/tools/resources.py +104 -0
- dndkit-0.4.0/src/dndkit/tools/schema.py +47 -0
- dndkit-0.4.0/src/dndkit/tools/screen.py +47 -0
- dndkit-0.4.0/src/dndkit/tools/spells.py +67 -0
- dndkit-0.4.0/src/dndkit/tools/utility.py +42 -0
- dndkit-0.4.0/src/dndkit/ui/__init__.py +52 -0
- dndkit-0.4.0/src/dndkit/ui/backstage.js +41 -0
- dndkit-0.4.0/src/dndkit/ui/battlemap.js +70 -0
- dndkit-0.4.0/src/dndkit/ui/character.html +23 -0
- dndkit-0.4.0/src/dndkit/ui/combat.html +18 -0
- dndkit-0.4.0/src/dndkit/ui/components.css +283 -0
- dndkit-0.4.0/src/dndkit/ui/components.js +261 -0
- dndkit-0.4.0/src/dndkit/ui/desk.js +91 -0
- dndkit-0.4.0/src/dndkit/ui/dice.html +42 -0
- dndkit-0.4.0/src/dndkit/ui/dicetray.js +99 -0
- dndkit-0.4.0/src/dndkit/ui/gm.html +59 -0
- dndkit-0.4.0/src/dndkit/ui/party.html +18 -0
- dndkit-0.4.0/src/dndkit/ui/player.html +61 -0
- dndkit-0.4.0/src/dndkit/ui/seating.html +18 -0
- dndkit-0.4.0/src/dndkit/ui/shared.css +195 -0
- dndkit-0.4.0/src/dndkit/ui/shared.js +242 -0
- dndkit-0.4.0/src/dndkit/ui/sheet.js +75 -0
- dndkit-0.4.0/src/dndkit/ui/table.html +41 -0
- dndkit-0.4.0/src/dndkit/ui/words.js +120 -0
- dndkit-0.4.0/src/dndkit/ui/world.js +56 -0
- dndkit-0.4.0/src/dndkit/views/__init__.py +7 -0
- dndkit-0.4.0/src/dndkit/views/backstage.py +49 -0
- dndkit-0.4.0/src/dndkit/views/battle.py +92 -0
- dndkit-0.4.0/src/dndkit/views/desk.py +76 -0
- dndkit-0.4.0/src/dndkit/views/everyone.py +194 -0
- dndkit-0.4.0/src/dndkit/views/public.py +89 -0
- dndkit-0.4.0/src/dndkit/views/widgets.py +58 -0
- dndkit-0.4.0/src/dndkit/views/world.py +70 -0
- dndkit-0.4.0/src/dndkit/web/__init__.py +10 -0
- dndkit-0.4.0/src/dndkit/web/access.py +136 -0
- dndkit-0.4.0/src/dndkit/web/audiences.py +111 -0
- dndkit-0.4.0/src/dndkit/web/changes.py +84 -0
- dndkit-0.4.0/src/dndkit/web/feed.py +112 -0
- dndkit-0.4.0/src/dndkit/web/routes.py +134 -0
- dndkit-0.4.0/src/dndkit/web/server.py +131 -0
- dndkit-0.4.0/src/dndkit.egg-info/PKG-INFO +567 -0
- dndkit-0.4.0/src/dndkit.egg-info/SOURCES.txt +151 -0
- dndkit-0.4.0/src/dndkit.egg-info/dependency_links.txt +1 -0
- dndkit-0.4.0/src/dndkit.egg-info/entry_points.txt +2 -0
- dndkit-0.4.0/src/dndkit.egg-info/requires.txt +18 -0
- dndkit-0.4.0/src/dndkit.egg-info/top_level.txt +1 -0
- dndkit-0.4.0/tests/__init__.py +0 -0
- dndkit-0.4.0/tests/conftest.py +30 -0
- dndkit-0.4.0/tests/fixtures/legacy_campaign.json +79 -0
- dndkit-0.4.0/tests/helpers.py +39 -0
- dndkit-0.4.0/tests/test_apps.py +105 -0
- dndkit-0.4.0/tests/test_battle.py +204 -0
- dndkit-0.4.0/tests/test_battle_effects.py +137 -0
- dndkit-0.4.0/tests/test_bosses.py +86 -0
- dndkit-0.4.0/tests/test_concentration.py +79 -0
- dndkit-0.4.0/tests/test_deletion.py +102 -0
- dndkit-0.4.0/tests/test_desk.py +92 -0
- dndkit-0.4.0/tests/test_dice_and_rules.py +75 -0
- dndkit-0.4.0/tests/test_game.py +183 -0
- dndkit-0.4.0/tests/test_levels_and_life.py +132 -0
- dndkit-0.4.0/tests/test_live_fixes.py +123 -0
- dndkit-0.4.0/tests/test_more_tools.py +99 -0
- dndkit-0.4.0/tests/test_names_and_fights.py +128 -0
- dndkit-0.4.0/tests/test_proficiencies.py +107 -0
- dndkit-0.4.0/tests/test_readiness.py +116 -0
- dndkit-0.4.0/tests/test_release.py +72 -0
- dndkit-0.4.0/tests/test_rules_and_table.py +155 -0
- dndkit-0.4.0/tests/test_seating.py +108 -0
- dndkit-0.4.0/tests/test_skills.py +88 -0
- dndkit-0.4.0/tests/test_srd.py +328 -0
- dndkit-0.4.0/tests/test_storage.py +175 -0
- dndkit-0.4.0/tests/test_tools.py +165 -0
- dndkit-0.4.0/tests/test_web.py +232 -0
- dndkit-0.4.0/tests/test_web_views.py +168 -0
- dndkit-0.4.0/tests/web_helpers.py +92 -0
dndkit-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joel Casimir and contributors
|
|
4
|
+
Copyright (c) 2026 Sylvain Boily (dndkit)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
dndkit-0.4.0/MANIFEST.in
ADDED
dndkit-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dndkit
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A D&D 5e campaign management MCP server, built for voice game masters
|
|
5
|
+
Author: Sylvain Boily, Joel Casimir
|
|
6
|
+
Maintainer: Sylvain Boily
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/sboily/dndkit
|
|
9
|
+
Project-URL: Changelog, https://github.com/sboily/dndkit/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Upstream, https://github.com/study-flamingo/gamemaster-mcp
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: fastmcp<5,>=4.1.0
|
|
15
|
+
Requires-Dist: platformdirs<5,>=4.0
|
|
16
|
+
Requires-Dist: pydantic<3,>=2.11
|
|
17
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
18
|
+
Requires-Dist: segno<2,>=1.6
|
|
19
|
+
Requires-Dist: shortuuid<2,>=1.0.13
|
|
20
|
+
Requires-Dist: starlette<2,>=0.46
|
|
21
|
+
Requires-Dist: uvicorn<1,>=0.34
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: bandit>=1.8; extra == "dev"
|
|
24
|
+
Requires-Dist: httpx>=0.28; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.15; extra == "dev"
|
|
26
|
+
Requires-Dist: pre-commit>=4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=6.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.11; extra == "dev"
|
|
30
|
+
Requires-Dist: types-pyyaml>=6.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# dndkit
|
|
34
|
+
|
|
35
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an
|
|
36
|
+
AI be the game master of a Dungeons & Dragons 5e campaign: it keeps the
|
|
37
|
+
characters, the world, the fights and the dice, and holds the rules, so the
|
|
38
|
+
model narrates while the server keeps count. It is built with
|
|
39
|
+
[FastMCP](https://gofastmcp.com) 4 and made for **voice play with several
|
|
40
|
+
players around one microphone**, as an MCP service in
|
|
41
|
+
[roomkit-ui](https://github.com/roomkit-live/roomkit-ui), while working just as
|
|
42
|
+
well from Claude Desktop or any MCP client, with a cloud model or a local one.
|
|
43
|
+
|
|
44
|
+
dndkit began as a fork of
|
|
45
|
+
[study-flamingo/gamemaster-mcp](https://github.com/study-flamingo/gamemaster-mcp)
|
|
46
|
+
by Joel Casimir, and has since been largely rewritten: voice play with several
|
|
47
|
+
players, rules kept by the server (combat, the battle map, hit dice,
|
|
48
|
+
concentration, a campaign checked before play), skills, on-screen widgets and
|
|
49
|
+
live web pages. It took a name of its own so as not to take the original
|
|
50
|
+
project's. See [CHANGELOG.md](CHANGELOG.md) for what changed.
|
|
51
|
+
|
|
52
|
+
## What it does
|
|
53
|
+
|
|
54
|
+
- **Campaigns**: several campaigns, one active at a time, each saved as one JSON
|
|
55
|
+
file. Creating a campaign never overwrites an existing one; deleting one takes
|
|
56
|
+
a confirmation code and moves its file to a trash folder.
|
|
57
|
+
- **Characters**: full 5e sheets; damage and healing with temporary hit points
|
|
58
|
+
applied first, unconsciousness and death saves at 0 HP, massive damage;
|
|
59
|
+
conditions and exhaustion; inventory that stacks; known spells and spell slots;
|
|
60
|
+
short and long rests; experience and levelling up (hit points, proficiency,
|
|
61
|
+
spell slots by class, French class names understood).
|
|
62
|
+
- **Combat**: party characters and monsters in one initiative order, tracked by
|
|
63
|
+
position so three "Goblin"s become "Goblin 1/2/3"; monster hit points, AC and
|
|
64
|
+
conditions; a monster at 0 HP leaves the fight; newcomers mid-fight; the end of
|
|
65
|
+
the fight sums the XP of the defeated.
|
|
66
|
+
- **Bestiary and encounters**: saved stat blocks fill monsters in; planned
|
|
67
|
+
encounters are rated easy to deadly against the party (DMG rules) and started
|
|
68
|
+
with one call, their initiative rolled.
|
|
69
|
+
- **World**: NPCs (with private bios), locations and quests with numbered objectives.
|
|
70
|
+
- **Journal**: session notes and a per-campaign adventure log with search.
|
|
71
|
+
- **Dice and rules**: `d20`, `1d20+5`, `2d6+1d4+3`, advantage and disadvantage;
|
|
72
|
+
short rule reminders (conditions, cover, grapple, death saves...) by English
|
|
73
|
+
or French name.
|
|
74
|
+
- **Several players**: each spoken turn names its speaker; a roll call says
|
|
75
|
+
who plays which character tonight, and only the characters at the table
|
|
76
|
+
count for difficulty and XP.
|
|
77
|
+
- **Battle map**: a grid where moves follow the 5e rules (speed, terrain,
|
|
78
|
+
opportunity attacks), areas of effect, cover, hidden foes.
|
|
79
|
+
- **Before play**: `check_campaign` says what breaks the rules or is missing
|
|
80
|
+
in a new campaign, each with the tool that fixes it.
|
|
81
|
+
- **Skills**: how-to guides the model loads when it needs them: being the
|
|
82
|
+
game master and preparing a campaign, monsters and bosses, the battle map.
|
|
83
|
+
- **On-screen widgets** (MCP Apps): rolling dice, a live combat tracker, a party
|
|
84
|
+
board, the table and character sheets, in clients that show MCP Apps.
|
|
85
|
+
- **Live web pages**: a table screen, each player's sheet on their phone, and
|
|
86
|
+
a backstage where you follow every call the model makes.
|
|
87
|
+
|
|
88
|
+
Names are matched without case or accents ("elodie" finds "Élodie"), and a part
|
|
89
|
+
of a name is enough when only one name contains it ("phare" finds "Le phare
|
|
90
|
+
éteint"), the way a speech transcript spells them. When a call fails, the error
|
|
91
|
+
says why and lists the known names, so the model can correct itself.
|
|
92
|
+
|
|
93
|
+
## Install and run
|
|
94
|
+
|
|
95
|
+
Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/).
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# From PyPI (from the first release, 0.4.0)
|
|
99
|
+
uvx dndkit
|
|
100
|
+
|
|
101
|
+
# Straight from GitHub
|
|
102
|
+
uvx --from git+https://github.com/sboily/dndkit dndkit
|
|
103
|
+
|
|
104
|
+
# From a checkout
|
|
105
|
+
git clone https://github.com/sboily/dndkit.git
|
|
106
|
+
cd dndkit
|
|
107
|
+
uv sync
|
|
108
|
+
uv run dndkit
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The server speaks MCP over stdio: an MCP client starts it, you don't keep it
|
|
112
|
+
running yourself. `uvx` fetches it on first use and keeps it cached.
|
|
113
|
+
|
|
114
|
+
### Configuration
|
|
115
|
+
|
|
116
|
+
| Variable | Default | Purpose |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `DNDKIT_STORAGE_DIR` | the per-user data directory (below) | Where campaigns are stored |
|
|
119
|
+
| `DNDKIT_LOG_LEVEL` | `INFO` | Log level; logs go to stderr |
|
|
120
|
+
| `DNDKIT_WEB_PORT` | unset: no web pages | Serve the [live web pages](#live-web-pages) on this port |
|
|
121
|
+
| `DNDKIT_WEB_HOST` | `127.0.0.1` | `0.0.0.0` lets phones on the same network open them |
|
|
122
|
+
| `DNDKIT_WEB_URL` | guessed | The address phones should use, when the server cannot tell (a DNS name, Tailscale) |
|
|
123
|
+
|
|
124
|
+
Default data directory:
|
|
125
|
+
|
|
126
|
+
- macOS: `~/Library/Application Support/dndkit`
|
|
127
|
+
- Linux: `~/.local/share/dndkit` (or `$XDG_DATA_HOME/dndkit`)
|
|
128
|
+
- Windows: `%LOCALAPPDATA%\dndkit`
|
|
129
|
+
|
|
130
|
+
Before its rename the project was `gamemaster-mcp`: its `GAMEMASTER_*`
|
|
131
|
+
variables still work, and its data directory is used as long as no `dndkit`
|
|
132
|
+
one exists.
|
|
133
|
+
|
|
134
|
+
To start a campaign from a prepared world document, drop a `.md` or `.txt` file
|
|
135
|
+
in the `settings/` folder of the data directory and pass its name as
|
|
136
|
+
`setting_file` to `create_campaign` or `update_campaign`. Only files directly in
|
|
137
|
+
that folder can be read.
|
|
138
|
+
|
|
139
|
+
### With roomkit-ui
|
|
140
|
+
|
|
141
|
+
In **Settings → Integrations → MCP**, add a server:
|
|
142
|
+
|
|
143
|
+
| Field | Value |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| Name | `dndkit` |
|
|
146
|
+
| Transport | `stdio` |
|
|
147
|
+
| Command | `uvx` |
|
|
148
|
+
| Arguments | `dndkit` (from PyPI), or `--from git+https://github.com/sboily/dndkit dndkit` |
|
|
149
|
+
| Environment variables | `DNDKIT_STORAGE_DIR=/Users/you/Games/dnd` (optional)<br>`DNDKIT_WEB_PORT=8765` and `DNDKIT_WEB_HOST=0.0.0.0` for the web pages (optional) |
|
|
150
|
+
|
|
151
|
+
From a local checkout instead: Command `uv`, Arguments
|
|
152
|
+
`run --directory /path/to/dndkit dndkit`.
|
|
153
|
+
|
|
154
|
+
roomkit-ui reads what the server publishes besides its tools: its skills
|
|
155
|
+
(roomkit 0.98 and later) and its instructions, which tell the model how the
|
|
156
|
+
tools are meant to be used (roomkit 0.99). Put the system prompt below in the
|
|
157
|
+
assistant's personality.
|
|
158
|
+
|
|
159
|
+
The 62 tool schemas weigh about 8,900 tokens by roomkit's own estimate, over
|
|
160
|
+
the 8,000 at which roomkit may hide tools behind `find_tools`: keep the skills
|
|
161
|
+
on (the tools they bring then wait for them: about 7,300 tokens), or raise
|
|
162
|
+
roomkit's threshold (`tool_search_threshold_tokens`). roomkit-ui's automatic
|
|
163
|
+
Tool Search leaves a local voice model every tool. Optional parameters are
|
|
164
|
+
advertised without their `null` alternative to save room.
|
|
165
|
+
|
|
166
|
+
#### Local models
|
|
167
|
+
|
|
168
|
+
A model running on your own computer can be the game master. The
|
|
169
|
+
[local model bench](poc/local-model/RESULTS.md) plays scripted table turns
|
|
170
|
+
through roomkit with Ornith 1.5 35B-A3B (llama.cpp): with **thinking on** it
|
|
171
|
+
passes 26 to 30 of 36 scenarios; without thinking it mostly narrates instead
|
|
172
|
+
of calling the tools. Tool Search hurts such a model: leave every tool in
|
|
173
|
+
sight. A thinking turn takes about a minute, which roomkit-ui's background
|
|
174
|
+
tasks (on a llama.cpp model of their own) keep away from the voice.
|
|
175
|
+
|
|
176
|
+
#### Skills
|
|
177
|
+
|
|
178
|
+
The server ships three Agent Skills, how-to guides the model loads when it
|
|
179
|
+
needs them, and publishes them by the official MCP Skills extension
|
|
180
|
+
([SEP-2640](https://modelcontextprotocol.io/seps/2640-skills-extension),
|
|
181
|
+
`io.modelcontextprotocol/skills`): it declares the extension in its
|
|
182
|
+
capabilities, serves every skill file as a `skill://<name>/<file>` resource,
|
|
183
|
+
and answers `skills/list` and `skills/get` with each skill's frontmatter and
|
|
184
|
+
the SHA-256 digest and size of its files. The server instructions also point
|
|
185
|
+
at each skill's URI, for hosts that only read resources. Load
|
|
186
|
+
`dnd-game-master` first: the system prompt below asks for it.
|
|
187
|
+
|
|
188
|
+
| Skill | Teaches | Gates (roomkit) |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `dnd-game-master` | To load first: the game master leads and acts, what the tools hold, opening and ending a session, a session zero by the rules (scores, armor, weapon, skills and spells for each class), `check_campaign` before play | nothing |
|
|
191
|
+
| `dnd-monsters` | Inventing monsters and bosses sized to the party, preparing and rating a fight, legendary actions and resistances, lair actions | `save_monster`, `plan_encounter` |
|
|
192
|
+
| `dnd-battle-map` | Fights on the grid: laying out the room, placing and hiding combatants, moving by the rules, areas of effect, cover | `set_battlefield`, `place_tokens`, `move_token`, `area_of_effect`, `check_target` |
|
|
193
|
+
|
|
194
|
+
A skill's `allowed_tools` frontmatter is roomkit's: roomkit keeps those tools
|
|
195
|
+
out of the catalogue, and out of its token count, until the model activates
|
|
196
|
+
the skill. With the skills on, the catalogue weighs about 7,300 tokens.
|
|
197
|
+
roomkit-ui lists them under the server's name; a client that does not speak
|
|
198
|
+
the extension can load them from `src/dndkit/skills/` as a skill folder.
|
|
199
|
+
|
|
200
|
+
### With Claude Desktop
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"mcpServers": {
|
|
205
|
+
"dndkit": {
|
|
206
|
+
"command": "uvx",
|
|
207
|
+
"args": ["dndkit"],
|
|
208
|
+
"env": { "DNDKIT_STORAGE_DIR": "/Users/you/Games/dnd" }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## System prompt for a voice game master
|
|
215
|
+
|
|
216
|
+
```markdown
|
|
217
|
+
You are the game master of a Dungeons & Dragons 5e session played by voice.
|
|
218
|
+
Speak the players' language, in short spoken sentences: no lists, no markdown.
|
|
219
|
+
You lead the game: you decide when a roll is needed, make it and apply it
|
|
220
|
+
with the tools, without asking leave. Before anything else, activate the
|
|
221
|
+
dnd-game-master skill.
|
|
222
|
+
|
|
223
|
+
Several players sit around one microphone; each of their turns opens with
|
|
224
|
+
who said it. Open every session the same way:
|
|
225
|
+
- Greet the table and ask who is playing tonight. When a voice is not named
|
|
226
|
+
yet, ask who it is and call name_speaker.
|
|
227
|
+
- Call seat_players with start_over and each player with their character.
|
|
228
|
+
Someone new gets a character first: create_character with their name as
|
|
229
|
+
player_name.
|
|
230
|
+
- Remind the table where the last session ended (get_sessions), then play.
|
|
231
|
+
Call seat_players again when someone arrives or leaves. A player's "I attack"
|
|
232
|
+
is their character's action; speak to players by name and to characters in
|
|
233
|
+
the story. Absent players' characters stay in the background.
|
|
234
|
+
|
|
235
|
+
The dndkit tools hold the campaign. Trust them over your memory:
|
|
236
|
+
- Before describing a character, place or quest, read it (get_character,
|
|
237
|
+
get_location, get_quest, get_game_state).
|
|
238
|
+
- Record every change as it happens: damage and healing with change_hit_points
|
|
239
|
+
(characters and monsters), conditions with set_condition, a spell held
|
|
240
|
+
with set_concentration (damage then asks for the save), items with
|
|
241
|
+
add_item_to_character and remove_item_from_character, spells with
|
|
242
|
+
use_spell_slot, the party's location and date with update_game_state.
|
|
243
|
+
- Roll every die with roll_dice and say the result; never invent a roll.
|
|
244
|
+
Resolve each roll yourself: the attack against the target's AC, then the
|
|
245
|
+
damage, applied with change_hit_points. Never ask the players for a total
|
|
246
|
+
or a number the tools hold (get_character gives bonuses, spell attack and
|
|
247
|
+
save DC).
|
|
248
|
+
What the players must not see (a hidden foe's Stealth, an NPC's lie) is a
|
|
249
|
+
secret roll: say only what it means.
|
|
250
|
+
- Ability checks and saves use the sheet's bonuses (get_character); a short
|
|
251
|
+
rest spends Hit Dice with rest and hit_dice.
|
|
252
|
+
- In combat, start_combat with everyone's initiative (or a planned encounter),
|
|
253
|
+
then next_turn after each turn. Monsters at 0 HP leave the fight on their own.
|
|
254
|
+
When positions matter, play it on the battle map (the dnd-battle-map skill:
|
|
255
|
+
set_battlefield, then move_token for every move).
|
|
256
|
+
At the end, end_combat, then award_experience with the XP it reports.
|
|
257
|
+
- When you're unsure of a rule, lookup_rule.
|
|
258
|
+
- Never call get_screen_data: it feeds the on-screen widgets.
|
|
259
|
+
- Log notable moments with add_event, and summarise the session with
|
|
260
|
+
add_session_note when the players stop.
|
|
261
|
+
- When a tool reports an unknown name, pick the right one from the names it
|
|
262
|
+
lists, or ask the players; never create a duplicate.
|
|
263
|
+
- You run the monsters. One the story needs and the bestiary lacks is yours
|
|
264
|
+
to create: save_monster with a challenge rating fitting the party (and
|
|
265
|
+
boss for the boss), then plan_encounter to check the fight's difficulty.
|
|
266
|
+
Spend a boss's legendary actions and resistances with use_legendary, and
|
|
267
|
+
any creature's reaction (an opportunity attack, Shield) with take_reaction.
|
|
268
|
+
A condition that lasts a while gets its rounds in set_condition.
|
|
269
|
+
- remove_from_campaign and delete_campaign work in two calls: the first
|
|
270
|
+
says what would be lost and gives a code; ask the players, and call again
|
|
271
|
+
with the code only after a clear yes.
|
|
272
|
+
|
|
273
|
+
Starting a new campaign, run a short session zero, one question at a time:
|
|
274
|
+
the campaign's name and pitch, then each player's character (name, race,
|
|
275
|
+
class, level, ability scores, then their skills with update_character), the
|
|
276
|
+
starting location, a first NPC and a first quest, creating each with its tool
|
|
277
|
+
as soon as you have it (the dnd-game-master skill has the rules for each).
|
|
278
|
+
Then call check_campaign and fix what it lists before the first scene.
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Tools
|
|
282
|
+
|
|
283
|
+
| Area | Tools |
|
|
284
|
+
| --- | --- |
|
|
285
|
+
| Campaign | `create_campaign`, `get_campaign_info`, `check_campaign`, `list_campaigns`, `load_campaign`, `update_campaign`, `delete_campaign`, `remove_from_campaign` |
|
|
286
|
+
| Characters | `create_character`, `get_character` 🖥, `list_characters`, `update_character` |
|
|
287
|
+
| Resources | `add_item_to_character`, `remove_item_from_character`, `record_death_save`, `rest` |
|
|
288
|
+
| Spells | `set_spell_slots`, `use_spell_slot`, `set_known_spell` |
|
|
289
|
+
| Progression | `award_experience`, `level_up` |
|
|
290
|
+
| NPCs | `create_npc`, `get_npc`, `update_npc`, `list_npcs` |
|
|
291
|
+
| Locations | `create_location`, `get_location`, `update_location`, `list_locations` |
|
|
292
|
+
| Quests | `create_quest`, `get_quest`, `update_quest`, `list_quests` |
|
|
293
|
+
| Table | `seat_players` 🖥, `get_game_state` 🖥, `update_game_state`, `change_hit_points`, `set_condition`, `set_concentration` |
|
|
294
|
+
| Combat | `start_combat` 🖥, `add_combatant`, `next_turn`, `remove_combatant`, `use_legendary`, `take_reaction`, `end_combat` |
|
|
295
|
+
| Battle map | `set_battlefield` 🖥, `place_tokens`, `move_token`, `area_of_effect`, `check_target` |
|
|
296
|
+
| Bestiary | `save_monster`, `get_monsters`, `plan_encounter`, `list_encounters` |
|
|
297
|
+
| Journal | `add_session_note`, `get_sessions`, `add_event`, `get_events` |
|
|
298
|
+
| Utility | `roll_dice` 🖥, `lookup_rule`, `get_screen_data` (for the widgets) |
|
|
299
|
+
|
|
300
|
+
🖥 opens an on-screen widget in clients that support MCP Apps.
|
|
301
|
+
|
|
302
|
+
`update_character` sets absolute values; `change_hit_points` applies damage or
|
|
303
|
+
healing to characters and monsters alike, and changes nobody if one name is
|
|
304
|
+
unknown. A tool that fails changes nothing, in memory or on disk.
|
|
305
|
+
|
|
306
|
+
A few rules the tools keep:
|
|
307
|
+
|
|
308
|
+
- **Before play**: `check_campaign` lists what breaks the 5e rules (hit
|
|
309
|
+
points the hit dice cannot give, AC under Unarmored Defense, a caster with
|
|
310
|
+
no spells) and what a first campaign forgets (a fighter without armor,
|
|
311
|
+
scores left at 10, no weapon, no quest or place), each with its fix.
|
|
312
|
+
- **Proficiencies**: `update_character` takes skills, saving throws (English
|
|
313
|
+
or French names: *Discrétion*, *Sagesse*), languages and features; a new
|
|
314
|
+
character is proficient in its class's saves. The sheets give each
|
|
315
|
+
proficient skill and save with its bonus. `expertise` doubles the
|
|
316
|
+
proficiency of a rogue's or a bard's chosen skills, and a bard from level 2
|
|
317
|
+
adds half of it to every other check, initiative included (Jack of All
|
|
318
|
+
Trades).
|
|
319
|
+
- **Hit Dice**: `rest` with `hit_dice` spends them on a short rest (rolled on
|
|
320
|
+
the dice tray, each die plus the Constitution modifier); a long rest gives
|
|
321
|
+
half of them back.
|
|
322
|
+
- **Concentration**: `set_concentration` holds one spell. Damage then asks for
|
|
323
|
+
the Constitution save (DC 10 or half the damage); 0 HP or an incapacitating
|
|
324
|
+
condition ends it.
|
|
325
|
+
- **Secret rolls**: `roll_dice` with `secret` shows on the backstage only, not
|
|
326
|
+
on the table screen, the phones or the dice widget.
|
|
327
|
+
- **Down at 0 HP**: three death save successes or `stabilized` (Spare the
|
|
328
|
+
Dying, Medicine) leave a character stable, without death saves until hit or
|
|
329
|
+
healed; sheets say dying, stable or dead.
|
|
330
|
+
- **Exhaustion** 4 halves the hit point maximum, 6 kills.
|
|
331
|
+
- **Reactions**: one a round (`take_reaction`), back at the start of the
|
|
332
|
+
creature's turn; one that has reacted provokes no more opportunity attacks.
|
|
333
|
+
- **Timed conditions**: `set_condition` with `rounds` ends by itself at the end
|
|
334
|
+
of the target's turn that many turns on; `next_turn` says so.
|
|
335
|
+
|
|
336
|
+
### Monsters and bosses
|
|
337
|
+
|
|
338
|
+
The game master invents the monsters its story needs: `save_monster` with a
|
|
339
|
+
challenge rating alone fills armor class, hit points and a line of attack
|
|
340
|
+
bonus, damage a round and save DC from the DMG's "Monster Statistics by
|
|
341
|
+
Challenge Rating", for the model to flesh out. `boss` marks the fight's boss,
|
|
342
|
+
a legendary creature with 3 legendary actions a round and 3 legendary
|
|
343
|
+
resistances a day unless told otherwise. In combat, `use_legendary` spends
|
|
344
|
+
them (actions at the end of another creature's turn, back at the start of
|
|
345
|
+
its own); the screens crown the boss, and the game master's show what it has
|
|
346
|
+
left. Combatants are found by a unique part of their name ("gardien" for
|
|
347
|
+
"Gardien de Magma").
|
|
348
|
+
|
|
349
|
+
Characters created without hit points or armor class get the PHB's: the
|
|
350
|
+
class hit die at level 1 and its average after, plus Constitution each level;
|
|
351
|
+
10 + Dexterity without armor. A caster gets its class's spell slots and
|
|
352
|
+
casting ability, and its sheet (`get_character`, the widget and the player's
|
|
353
|
+
page) gives the spell attack bonus and save DC; the sheet also shows the
|
|
354
|
+
initiative bonus, background, alignment, appearance and story, spells with
|
|
355
|
+
their descriptions, gear, features, proficiencies and languages.
|
|
356
|
+
`get_character` without a name reads whose turn it is.
|
|
357
|
+
|
|
358
|
+
The running fight keeps up with the campaign: a combatant named after a party
|
|
359
|
+
character (even "Fizbin the gnome") plays from that sheet, including one
|
|
360
|
+
created once the fight began, and a monster that joined without numbers takes
|
|
361
|
+
its stat block as soon as `save_monster` saves it.
|
|
362
|
+
|
|
363
|
+
### Battle map
|
|
364
|
+
|
|
365
|
+
A fight can be played on a grid of 5-foot squares named like a chessboard (a
|
|
366
|
+
column letter and a row number, A1 at the top left). `set_battlefield` lays it
|
|
367
|
+
out with walls and difficult terrain; `start_combat` puts everyone without a
|
|
368
|
+
square at an edge (the party left, its foes right) and `place_tokens` sets
|
|
369
|
+
chosen squares (an ambush). `move_token` moves a combatant by the 5e rules
|
|
370
|
+
(PHB ch. 9) and refuses an impossible move:
|
|
371
|
+
|
|
372
|
+
- 5 feet a square, diagonals included (the PHB grid rule), 10 in difficult
|
|
373
|
+
terrain or through another creature's space; walls are walked around and
|
|
374
|
+
never cut diagonally;
|
|
375
|
+
- allies can be passed through, foes only when two sizes apart; nobody ends a
|
|
376
|
+
move on someone else; a Large creature holds 2x2 squares, Huge 3x3,
|
|
377
|
+
Gargantuan 4x4 (`save_monster`'s `size`);
|
|
378
|
+
- speed from the sheet (25 ft for dwarves, halflings and gnomes, else 30;
|
|
379
|
+
`create_character`'s `speed` overrides it) or the stat block's `speed`;
|
|
380
|
+
grappled, restrained, stunned, paralyzed, petrified or unconscious: 0;
|
|
381
|
+
exhaustion halves it from level 2; Dash doubles the turn's movement;
|
|
382
|
+
- a prone mover stands up first, for half its speed;
|
|
383
|
+
- leaving a foe's reach provokes an opportunity attack, which the result
|
|
384
|
+
names, unless the mover Disengaged; `forced` movement (a shove, a spell, a
|
|
385
|
+
teleport) spends nothing and provokes nothing;
|
|
386
|
+
- `toward` someone stops within reach or as close as it can ("still 15 ft
|
|
387
|
+
away"); `away_from` backs off; `next_turn` restores the next combatant's
|
|
388
|
+
movement.
|
|
389
|
+
|
|
390
|
+
Around the moves:
|
|
391
|
+
|
|
392
|
+
- `area_of_effect` says who a spell or a breath catches: a `sphere` (or
|
|
393
|
+
cylinder) of radius R `at` a square or a creature holds every square within
|
|
394
|
+
R feet as the grid counts them; a `cube` is centred there, or set against
|
|
395
|
+
its `source`'s side facing `toward`; a `cone` (as wide as it is far) and a
|
|
396
|
+
`line` start at the edge of the caster's space, and take a square when at
|
|
397
|
+
least half of it is covered (DMG). Walls stop an area. Everyone caught is
|
|
398
|
+
named, allies first; the area shows on the map until the next turn.
|
|
399
|
+
- `check_target` gives what an attack needs: distance, reach, cover by the
|
|
400
|
+
DMG corner rule (walls give half, three-quarters or total cover, creatures
|
|
401
|
+
half at most), a foe adjacent to a ranged attacker, hidden attacker or
|
|
402
|
+
target.
|
|
403
|
+
- Hidden foes (`place_tokens` with `hidden`, or a planned group's `hidden`)
|
|
404
|
+
stay off the players' screens altogether: no token, no line in the
|
|
405
|
+
initiative they see, no turn named, no hole in the squares a character can
|
|
406
|
+
reach. The game master's screens show them faint. `hidden: false` reveals.
|
|
407
|
+
- `plan_encounter` can carry a `battlefield` and, per monster group, the
|
|
408
|
+
squares `at` which they start (checked against the map, its walls and each
|
|
409
|
+
other); `start_combat` with that encounter lays it all out.
|
|
410
|
+
|
|
411
|
+
`remove_combatant` takes a creature out of the fight and off the map: a
|
|
412
|
+
`defeated` or `fled` foe counts for XP, a `withdrawn` one (a mistake, a
|
|
413
|
+
change of plan) leaves no trace; `add_combatant` brings one in at its side's
|
|
414
|
+
edge.
|
|
415
|
+
|
|
416
|
+
The model reads the map in text: `set_battlefield` and `place_tokens` return
|
|
417
|
+
a grid with a legend, every move says what it cost, what is left and who is
|
|
418
|
+
in reach, and `get_game_state` lists the squares and the distances from
|
|
419
|
+
whoever is acting. The combat widget and the web pages draw it: tokens gold for
|
|
420
|
+
the party and red for its foes, the squares the acting character can still
|
|
421
|
+
reach, and on a player's phone their own token ringed. The players' pages show
|
|
422
|
+
how hurt a foe looks, never its hit points. The map goes away with
|
|
423
|
+
`end_combat`.
|
|
424
|
+
|
|
425
|
+
### Who plays
|
|
426
|
+
|
|
427
|
+
roomkit-ui tells the voices apart (enrolled voices, or Meta Muse labels that
|
|
428
|
+
the user or the model's `name_speaker` names) and opens each spoken turn with
|
|
429
|
+
the speaker's name. The game keeps the rest: a character's `player_name` ties
|
|
430
|
+
it to a person, and `seat_players` records who is at the table tonight, in
|
|
431
|
+
seat order. Players are matched by name, case and accents aside; the server
|
|
432
|
+
keeps no voice data. Once there has been a roll call:
|
|
433
|
+
|
|
434
|
+
- `get_game_state` says who plays whom, who is absent and which characters
|
|
435
|
+
nobody plays, so the model finds its way after a break;
|
|
436
|
+
- encounter difficulty (`plan_encounter`, `list_encounters`) counts the
|
|
437
|
+
characters at the table only: two players out of four turn a medium fight
|
|
438
|
+
into a deadly one;
|
|
439
|
+
- `award_experience` without names shares the XP between the characters at
|
|
440
|
+
the table; name absent characters to include them.
|
|
441
|
+
|
|
442
|
+
Without a roll call, the whole party counts as present.
|
|
443
|
+
|
|
444
|
+
`delete_campaign` works in two calls. The first deletes nothing: it says what
|
|
445
|
+
the campaign holds and returns a 4-digit code valid for 5 minutes. Only a
|
|
446
|
+
second call with that code deletes, by moving the file to `<data dir>/trash/`,
|
|
447
|
+
from where it can be put back by hand. A wrong code cancels the request.
|
|
448
|
+
|
|
449
|
+
## On-screen widgets (MCP Apps)
|
|
450
|
+
|
|
451
|
+
Six tools carry an MCP App (`_meta.ui.resourceUri`): the client opens their
|
|
452
|
+
page next to the conversation.
|
|
453
|
+
|
|
454
|
+
| Tool | Widget |
|
|
455
|
+
| --- | --- |
|
|
456
|
+
| `roll_dice` | Dice tray: each die tumbles and lands on its face; a natural 20 glows, a natural 1 shakes; with advantage the discarded d20 fades out; recent rolls below. |
|
|
457
|
+
| `start_combat`, `set_battlefield` | Combat tracker, live: the battle map when there is one, initiative order, current turn, hit point bars that flash on damage or healing, AC, conditions, the defeated and their XP. |
|
|
458
|
+
| `get_game_state` | Party board, live: location, date, funds, every character's hit points, spell slots, conditions and death saves, quest progress, combat banner. |
|
|
459
|
+
| `seat_players` | The table, live: the players seated around it with their characters, newcomers without one yet, absent players and characters nobody plays. |
|
|
460
|
+
| `get_character` | Character sheet, live: hit points, AC, proficiency, XP to next level, abilities, conditions, spell slots, spells, inventory. |
|
|
461
|
+
|
|
462
|
+
Each page is self-contained (no network access), French or English after the
|
|
463
|
+
system language, and reads its data with the `get_screen_data` tool, refreshing
|
|
464
|
+
every couple of seconds: the combat tracker follows `change_hit_points` or
|
|
465
|
+
`next_turn` calls made later in the conversation. The widgets only display; the
|
|
466
|
+
game changes through the conversation, so the model always knows the state.
|
|
467
|
+
|
|
468
|
+
## Live web pages
|
|
469
|
+
|
|
470
|
+
With `DNDKIT_WEB_PORT` set, the server also serves web pages that follow
|
|
471
|
+
the game as it happens: every change made through a tool, and every roll, is
|
|
472
|
+
pushed to the open pages at once (server-sent events). The pages run inside
|
|
473
|
+
the MCP server's process, so they are up while roomkit-ui runs the server and
|
|
474
|
+
reconnect by themselves when it restarts. A taken port only logs a warning;
|
|
475
|
+
the game goes on without the pages.
|
|
476
|
+
|
|
477
|
+
| Address | For | Shows |
|
|
478
|
+
| --- | --- | --- |
|
|
479
|
+
| `/table/` | a shared screen (TV, laptop) | The campaign's name and pitch, the session, place and date; initiative order, the party's hit points and conditions (absent players' characters faded), the table with who sits where, the dice tray, active quests, the adventure log, and a QR code per character |
|
|
480
|
+
| `/p/<code>/` | each player's phone | Their full character sheet, the fight, the dice, quests and log, the party; "Your turn!" and a buzz when their turn comes |
|
|
481
|
+
| `/gm/` | you, on the computer running the game | Everything, to follow and debug what the model does: the **model's activity** (every tool call with its arguments, its answer or error and its time, and the skills the client fetched); the fight with exact monster hit points and AC, or the last fight between two; the adventure log with importance, characters and tags; planned encounters and their difficulty; every quest with its objectives and notes; the notebook (current situation, world notes, setting, past sessions, version, campaign file, last save, skills); the world (NPCs with their secrets and ties, locations with the party's marked, the bestiary's full stat blocks and how many are fighting); every character's full sheet with the game master's notes; the players' join codes |
|
|
482
|
+
|
|
483
|
+
- **Joining**: players scan their character's code on the table screen. Each
|
|
484
|
+
link carries a code derived from the campaign, the character and a key kept
|
|
485
|
+
in the data directory (`player-links.key`): it cannot be guessed, survives
|
|
486
|
+
restarts, and belongs to the active campaign. Delete the key file to change
|
|
487
|
+
every link.
|
|
488
|
+
- **What players see** is built field by field from what they may know: the
|
|
489
|
+
campaign's description (a pitch: `create_campaign` asks for no spoilers), their
|
|
490
|
+
party in full, but a monster only as *unhurt*, *hurt*, *bloodied* or *down*,
|
|
491
|
+
and never an NPC's biography or notes, a planned encounter, a monster's
|
|
492
|
+
statistics or the game master's notes.
|
|
493
|
+
- **The activity** lives in memory for the session, like the dice log, and
|
|
494
|
+
skips the widgets' own `get_screen_data` polling. A call the client refuses
|
|
495
|
+
before sending it (roomkit checks required arguments) never reaches the
|
|
496
|
+
server, so it is not there: look at the client's log for those.
|
|
497
|
+
- **The backstage** opens only from the computer running the server: a
|
|
498
|
+
loopback peer, asking for this server by name (`localhost`, its address or
|
|
499
|
+
`DNDKIT_WEB_URL`'s host), so a web page whose domain points at this
|
|
500
|
+
computer (DNS rebinding) is refused. Requests carrying proxy headers are
|
|
501
|
+
refused too, but a reverse proxy or tunnel on that computer that adds none
|
|
502
|
+
would make every visitor look local: do not put one in front of the pages.
|
|
503
|
+
- **Read-only**: the pages never change the game; it changes through the
|
|
504
|
+
conversation, so the model always knows the state.
|
|
505
|
+
- **Phones** need `DNDKIT_WEB_HOST=0.0.0.0` and the same network. The table
|
|
506
|
+
screen then puts this computer's network address in the codes, even when it
|
|
507
|
+
is open at `127.0.0.1`. For players elsewhere, put a private network such as
|
|
508
|
+
Tailscale between you and set `DNDKIT_WEB_URL`; the pages carry no other
|
|
509
|
+
authentication.
|
|
510
|
+
- The log names the addresses at startup: `Table screen: http://127.0.0.1:8765/table/`.
|
|
511
|
+
|
|
512
|
+
## Data
|
|
513
|
+
|
|
514
|
+
See [docs/STORAGE_STRUCTURE.md](docs/STORAGE_STRUCTURE.md). Campaigns saved by
|
|
515
|
+
gamemaster-mcp, dndkit's upstream, load as they are and are rewritten in the
|
|
516
|
+
current format at their first change. Its global `events/adventure_log.json`
|
|
517
|
+
is not imported: events now belong to their campaign.
|
|
518
|
+
|
|
519
|
+
## Development
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
uv sync --extra dev
|
|
523
|
+
uv run pytest
|
|
524
|
+
uv run ruff check . && uv run ruff format --check .
|
|
525
|
+
uv run mypy src/
|
|
526
|
+
uv run bandit -r src/ -c pyproject.toml
|
|
527
|
+
npm ci && npx knip # unused files and exports in the pages' scripts
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The pages' scripts (`src/dndkit/ui/*.js`) are ES modules, so knip can
|
|
531
|
+
follow who imports what; each page inlines them as one script. GitHub Actions
|
|
532
|
+
runs all of the above on every push and pull request (Python 3.12 and 3.13),
|
|
533
|
+
and `pre-commit install` runs most of it before each commit.
|
|
534
|
+
|
|
535
|
+
[CLAUDE.md](CLAUDE.md) describes the architecture and the project's rules.
|
|
536
|
+
|
|
537
|
+
### Releasing
|
|
538
|
+
|
|
539
|
+
Each change worth telling goes into [CHANGELOG.md](CHANGELOG.md) under
|
|
540
|
+
`## Unreleased` with the commit that makes it. To release, on an up-to-date
|
|
541
|
+
`main`:
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
uv run python scripts/release.py prepare 0.5.0 # versions follow semver
|
|
545
|
+
git push origin main v0.5.0
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`prepare` names the Unreleased section `## 0.5.0 (<date>)`, sets the version
|
|
549
|
+
in `pyproject.toml` and `uv.lock`, commits `release: 0.5.0` and tags
|
|
550
|
+
`v0.5.0`; it refuses outside `main`, with uncommitted changes, an empty
|
|
551
|
+
Unreleased section or an existing tag. The tag runs
|
|
552
|
+
`.github/workflows/release.yml`: it checks that the tag matches the version,
|
|
553
|
+
sits on `main` and has its changelog section, runs the tests and builds, then
|
|
554
|
+
publishes to PyPI by trusted publishing (no token: PyPI trusts the workflow in
|
|
555
|
+
its `pypi` environment) and makes the GitHub release, its notes the version's
|
|
556
|
+
changelog section, the built files attached. A failed step stops what follows:
|
|
557
|
+
nothing reaches GitHub's releases before PyPI has the version.
|
|
558
|
+
|
|
559
|
+
Once, on pypi.org: add a pending trusted publisher for the project `dndkit`
|
|
560
|
+
(owner `sboily`, repository `dndkit`, workflow `release.yml`, environment
|
|
561
|
+
`pypi`). GitHub creates the `pypi` environment at the first release; required
|
|
562
|
+
reviewers set on it (Settings → Environments) hold each release for approval.
|
|
563
|
+
|
|
564
|
+
## License
|
|
565
|
+
|
|
566
|
+
MIT, see [LICENSE](LICENSE). dndkit keeps the copyright of gamemaster-mcp,
|
|
567
|
+
the project it was forked from.
|