Imported from spainion/Hammerborn- (
AGENTS.md). Install upstream withnpx skills add spainion/Hammerborn-. Copyright stays with the author.
Agent and Tool Integration
CLAUDE.md is the operating protocol for a model running the game. This file covers
wiring the engine into agent frameworks, tool-calling APIs, and other runtimes.
Contract
The package and runtime both require Python 3.11 or newer. Keep the version in
pyproject.toml synchronized with wandering_age.__version__; the regression suite
checks both pieces of integration metadata.
Every machine command follows the same shape:
python3 wa api <subcommand> [--flags] -> one JSON object on stdout
exit 0 = success, payload has "ok": true
exit 1 = failure, payload has "ok": false, "error", and usually "hint"
Nothing is written to stderr on a handled error, and no traceback ever reaches the caller. Parse stdout unconditionally.
Argument parsing and unexpected runtime failures also use this JSON envelope. Unexpected database errors are rolled back before the error is returned. Numeric physical inputs such as distance, elapsed time, combatant counts, and fall height reject negative values.
Subcommands as tool definitions
| Subcommand | Arguments | Returns |
|---|---|---|
state |
— | full character and world state |
schema |
— | table names, columns, row counts |
lookup |
query |
matches across terms, cities, lore, materials, engineering, tech, npcs, history |
roll |
--attr, --dc |
die, modifier, total, outcome |
skill |
--skill, --dc |
die, rank, condition penalties, total, outcome, example DCs |
attack |
--zone, --skill |
hit/miss, damage multiplier, rider effect, inflicted condition |
act |
--act, --victim, --commit |
resolved conscience weight, every factor, sanity change |
wound |
--location, --mechanism, --commit |
wound, bleed rate, treatment, prognosis, shock state |
treat |
--id, --note, --commit |
medicine roll against an active wound; successful treatment stops bleeding |
clock |
actions... |
per-action seconds, total, what an opponent achieves meanwhile |
turn |
--action, --skill, --dc, --commit |
first-person narrative packet, resolution, per-skill XP, rank changes and state |
progression |
--limit |
every skill's uncapped rank/XP/use record and recent action history |
map |
--place |
moving-world route graph, regions, city types, courses, and cultures |
city |
city |
city machinery, current course, districts, services, and cultural context |
society |
--place |
property, households, shops, politics, subgroups, developing NPCs, and recent world motion |
property |
--id, --transfer, --owner-type, --why, --commit |
inspect or transfer a claim with provenance |
rig |
--acquire, --fuel, --commit |
preview or acquire the owned vehicle, fuel state, damage systems, and upgrades |
travel |
--region, --origin, --destination, --commit |
one real travel day, route, supplies, fuel, navigation, weather, and encounter |
--commit is the write flag. Omit it for a preview; nothing is persisted.
For natural LLM play, call turn once per player action. Its narrative object is a
provider-neutral first-person-present packet that can be passed directly to this model or
any other model without API keys, endpoints, or adapter configuration. Every committed
consequential skill action earns individual experience; thresholds scale algorithmically
and ranks have no ceiling. An unknown skill returns a ready Forge definition workflow so
the narrative can expand the skill catalogue without silently canonising a typo.
For explicit zero-friction adoption, repeat the unknown-skill turn with --learn --commit.
The engine infers a governing attribute from canonical attributes (or validates
--attribute), writes Forge provenance, creates rank and experience rows, and resolves the
same action. Never use --learn to conceal a typo; without it, unknown names remain previews.
All actions may be attempted; outcomes are not guaranteed. For an action crossing a hard
boundary, pass turn --limit ability, --limit knowledge, or --limit physics, together
with --risk routine|dangerous|severe|lethal and an honest --consequence domain.
Physical impossibility remains impossible even on a natural 20. The returned
attempt_assessment preserves the attempt and scales the world's response. Serious bodily
consequences require a real wound or shoot
follow-up; never turn a consequence prompt into invented anatomy or arbitrary death.
Use map before route travel: this world is mobile, so its canonical map is a route graph
plus each city's current course rather than false fixed coordinates. Committed travel
calls consume carried rations, water, filters, and owned-rig fuel. shoot returns an
immutable first-person sensory packet for both hits and misses; narrators must not add a
wound to a miss or revise the resolved anatomy.
Combat narration must consume shoot.damage_roll, shoot.trauma, shoot.medical_reality,
and shoot.aftermath as immutable facts. Dismemberment is not a style choice: the engine
only returns it when a limb is hit by a cutting or cleaving weapon with enough post-armour
energy. Ballistic destruction of an extremity is distinct from clean severance. Preserve
lost function, bleeding, treatment requirements, witnesses, and aftermath across later turns.
turn and resolve compose one explained modifier stack: governing attribute, primary
skill rank, action-relevant trained skills, inferred scene conditions, identity/context
effects, and automatic physiological penalties. Pass related skills with --support; the
engine also discovers support from action language. Support is half-rank, weighted by
evidence, capped at +2 per skill and +4 overall, and can never add the primary skill twice.
Repeated modifier ids are deduplicated, while mutually exclusive aim, cover, and target
states never stack. Treat modifier_breakdown as the authoritative roll arithmetic.
For long-cycle regression, run python3 tools/campaign_soak.py --campaigns 4 --cycles 12.
It creates independent disposable campaign databases, drives different origins concurrently
through committed turns, travel, injury, treatment, sleep, moral acts, bleeding, and possible
succession, then runs SQLite integrity checks and the complete world audit on every campaign.
Parallel writers never share a database. Use --keep <directory> only when failed campaign
files are needed for diagnosis.
For multi-port stress, run python3 tools/stress.py --ports 8 --cycles 6. Each port is a
real isolated campaign database driven concurrently through skill learning, committed turns,
society reads, elapsed world motion, ownership transfer, SQLite integrity, and the complete
audit. Increase both values for deep release testing; ports must never share a writer database.
Committed elapse calls also advance the living society. Households respond to material
pressure, political bodies gain or lose legitimacy, and NPCs pursue their own stated aims
and record new insights. These changes appear in society_events and world_pulses; do not
erase them because the player was elsewhere. Use society --place before scenes involving
ownership, families, shops, children, factions, or local politics. Property changes require
property --why and remain previews until --commit.
Do not commit generated binary artifacts. book/*.pdf, book/*.epub, and repository ZIP
archives are ignored so pull requests remain text-reviewable. Regenerate local book exports
with python3 wa book; keep the generator and HTML output as the reviewable sources.
The HTML is the canonical complete edition with searchable full-registry and alphabetical
indexes; PDF and EPUB must be generated from that same document rather than maintained by hand.
Forge subcommands (world growth)
| Subcommand | Arguments | Returns |
|---|---|---|
forge describe |
--table (optional) |
table catalogue, or field types, requirements and examples |
forge similar |
--table, --text |
ranked near-duplicates with shared terms and a verdict |
forge define |
--table, --data (JSON), --why, --update |
write result, snapshot path, integrity status, derived rows |
forge extend |
--table, --fields (JSON), --why |
new table created |
forge suggest |
--limit |
prioritised gaps with a ready-to-run fix for each |
forge history |
--limit |
every runtime change with provenance, plus snapshots |
forge revert |
--to (optional) |
undo the last change, or restore a snapshot |
A model should call describe then similar before define. Writes to save,
attributes, identity, conscience_axes and habituation are refused: those are live
character state with their own commands.
Example: OpenAI-style function schema
{
"name": "wandering_age_act",
"description": "Resolve a morally weighted act against the character's culture, bloodline, upbringing and history. Returns the resolved weight and sanity change. Never judge morality yourself; call this.",
"parameters": {
"type": "object",
"properties": {
"act": {"type": "string", "description": "act id, e.g. kill_stranger"},
"victim": {"type": "string"},
"commit": {"type": "boolean", "default": false}
},
"required": ["act"]
}
}
Implementation:
import json, subprocess
def wandering_age_act(act, victim="", commit=False):
cmd = ["python3", "wa", "api", "act", "--act", act, "--victim", victim]
if commit:
cmd.append("--commit")
return json.loads(subprocess.run(cmd, capture_output=True, text=True, cwd=REPO).stdout)
Example: MCP server
Each subcommand maps to one tool. Register state, lookup, roll, skill, attack,
act, wound, clock and turn; treat --commit as a boolean parameter and require
user confirmation before passing it, since committed calls mutate the campaign.
Concurrency
SQLite allows one writer. Do not hold an open write connection while shelling out to
another command — this causes disk I/O errors. Open, write, commit, close. Read-only
callers can use connect(readonly=True) from wandering_age.paths.
Choosing a database
Priority order: --db <path> on the command line, then WANDERING_AGE_DB in the
environment, then db/wandering_age.db in the repo. Useful for running several campaigns:
python3 wa --db campaigns/vell.db api state
WANDERING_AGE_DB=/srv/games/ashfall.db python3 wa status
Context management
The complete encyclopedia is roughly 800,000 characters and will not fit alongside a conversation. Generate the briefing pack instead:
python3 wa context --budget 24000 # docs/CONTEXT_PACK.md (~4k tokens) and .json
Load the markdown into the system prompt or project knowledge and call wa api state at
the start of each session for live values. The JSON sidecar carries acts, teachings, hit
zones, action costs and open hooks in structured form.
Runtimes without code execution
If the model cannot run commands, upload db/wandering_age.sql, docs/CONTEXT_PACK.md,
docs/SYSTEM.md and CLAUDE.md. The model can then apply rules by hand, but cannot
persist state — so it must end each reply with the current state block and the player must
paste it back next session.
Verification in CI
python3 db/build.py --from-dump
python3 wa audit # comprehensive relational and domain integrity checks
python3 wa scan # supernatural language and contradictions
python3 tests/test_world.py
python3 tools/campaign_soak.py --campaigns 4 --cycles 12
All five must pass before a campaign state is committed.