Lean on the tools harder for 5e bookkeeping
The dm-system prompt has been claiming "this is a real 5e game with real
rules" for a while, but a lot of the mechanical enforcement was still
living in the LLM's head. This pulls the parts that are genuinely
unambiguous arithmetic down into the tool layer, and leaves the judgment
calls where they belong.
Highlights from the character-side work:
- `damage()` now applies resistance / vulnerability / immunity before
temp HP soaks. Same with the routing through `_sync_player_hp`.
- Exhaustion folds directly into displayed skill and save modifiers;
Poisoned/Frightened show a disadvantage marker on checks; Paralyzed/
Stunned/etc show ✗ auto-fail on Str/Dex saves. Passive perception
drops 5 when the character is disadvantaged on checks. Inspiration
and exhaustion get their own lines on the sheet.
- `add_effect(concentration=True)` enforces "one concentration at a
time" per 5e 2024, and `damage()` emits a "Concentration save DC X"
hint whenever the character takes damage while concentrating.
`break_concentration()` for failed saves.
- `level_up(class_name, new_level, hp_gain, features)` is an atomic
replacement for the five-step update_character dance at level-up.
It only appears in the DM's toolset when `advancement_ready` is set
on the sheet — the evaluator agent grants access, the DM picks the
narrative moment. The prompt spells out that the *player* still
makes the mechanical choices (ASI/feat, subclass, spells, etc).
- Spell slots turn out to be shape-compatible with ResourcePool, so
no schema change. The prompt just documents the `resources.slot_N`
convention so `use_resource("slot_3")` and `rest("long")` handle
casting and refreshing for free.
World-state side:
- `establish()` refuses to create an NPC entity that matches the
player character's name. The PC lives on the sheet, not as an NPC,
and the prompts have always said so — this makes it actually true.
- `mark()` grew a `when:` parameter so the world-tick agent can
backdate off-screen events without manually prefixing the event
text. Kills the `#dX-HHMM | #dY-HHMM | ...` double-anchor pattern.
- `_auto_mark_present` is rate-limited (20 in-game-minute cooldown
per entity) and skips session-lifecycle events. Scene-dense NPCs
like a constant travel companion will stop accumulating one Was
entry per set_scene.
- `amend_mark` replaces the most recent Was entry instead of
appending, so "Retcon:" lines stop leaking into the fiction.
- `maps` is now in the EstablishType enum — the seed and DM prompts
had been telling agents to use it, but the tool was rejecting it.
Small things I tripped over on the way:
- `Duration.parse`'s rounds-to-minutes math was `rounds // 10`, so a
3-round fight advanced the clock by 1 minute instead of ~0, and a
15-round fight also advanced by 1. Now uses `round(rounds * 6 / 60)`
with a 1-minute floor, so 10 rounds is 1 minute and 20 rounds is 2.
- `/save` and `/note` were puppeteering the DM via `[System: ... call
add_note ...]` injected into the user message. Both are operational,
not in-fiction events, so they now call the ops layer directly.
- `/context` was missing the MCP tool-schema surface from its estimate
entirely — those definitions are non-trivial and flow into the model
every turn. Added a `Tool Schemas` section that walks the composed
server's list_tools() and sums up name + description + serialized
parameters.
- Deleted a `lore/Search Test.md` stray I found while reviewing the
campaign and purged it from the vector index.
- Cleaned up the `TYPE_CHECKING → _FastMCP` alias pattern in both
combat.py and character.py after noticing it was cargo-culted from
an earlier refactor — the runtime FastMCP import is already there.
- Suppressed ruff B008 project-wide. It fires on every FastMCP tool's
`root: Path = StorageRoot()` default, but those are uncalled_for
Dependency markers, not mutable defaults. Was adding 49 pre-existing
errors to the baseline and masking real regressions.
710 tests passing, coverage at 95.78%. A few files pushed past the loq
500-line cap as I added features — character/operations.py, tools/
entities.py, tools/character.py, and the matching test files. They're
on the list to split when we pick up the architectural refactors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>