AGENTS.md defines the session loop; docs/oracle.md and docs/games.md cover table mechanics and the game package contract.
3.7 KiB
Game packages
Each game is a self-contained package under games/<id>/. It owns everything
that is true about the game regardless of who is playing it: the rules, the
spark tables, the character options, and any tooling the game needs. Campaigns
own the play state and reference a game by id.
Layout
games/<id>/
package.json @solo/game-<id>, type: module
index.js the manifest (default export)
game.md authoritative rules the agent reads each session
rules/ longer rulebook chapters, read on demand
tables/*.csv spark tables and complication tables
characters/ backgrounds, playbooks, starting options
scripts/ tool scripts and CLI commands
package.json should depend on @solo/oracle if the game's scripts need dice
or table parsing:
{
"name": "@solo/game-my-game",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "index.js",
"exports": { ".": "./index.js" },
"dependencies": { "@solo/oracle": "workspace:*" }
}
The manifest
index.js default-exports the manifest. Every field is optional; the id always
comes from the directory name.
export default {
id: 'my-game',
title: 'My Game',
description: 'One line about the game.',
rules: 'game.md',
commands: {
'my-command': {
description: 'What it does.',
run(args, ctx) { /* ... */ },
},
},
};
Commands
Each entry in commands becomes a journal <name> subcommand, available
whenever a campaign running this game is in context. The handler receives:
args—{ positional, flags }, already parsed.ctx—{ root, campaignsDir, gamesDir, campaignDir, campaign, game }plusctx.findTable(name)andctx.helpers(rollDie,rollExpr,oracle,parseTable,rollTable,tableRange,findTable).
Handlers may be async. Print to console.log; throwing exits non-zero with
journal: <message>.
Commands are the recommended way to ship game tooling, because the agent can
reach them through the one journal entry point. Standalone executables go in
scripts/ for one-off jobs and are run with node.
What lives where
| Content | Location | Reached by |
|---|---|---|
| Rules summary | game.md |
the agent, every session |
| Rulebook chapters | rules/*.md |
the agent, on demand |
| Spark/complication tables | tables/*.csv |
journal roll <name> |
| Character backgrounds | characters/ |
game commands, or the agent |
| Game tooling | index.js, scripts/ |
journal <command>, node |
Table lookup
journal roll <name> searches the campaign's tables/ first, then the game's.
That lets a campaign override a game table without copying the rest. Tables the
game ships are not copied into campaigns.
Validating
journal check --game <id> validates the package as well as its tables:
- errors — overlapping table ranges, CSV parse failures, invalid or missing
package.json, missing game. - warnings — range gaps, package name that is not
@solo/game-<id>, missing"type": "module", missingindex.js/tables//characters/, missing rules file.
It exits non-zero on errors. journal games additionally surfaces a manifest
that fails to load as ERROR. Test each table with
journal roll <table> --game <id> and each command with
journal <command> --game <id>.
journal check --game my-game
journal game my-game
Creating a game
- Copy
games/generic/togames/<id>/. - Rename the package to
@solo/game-<id>and setidinindex.js. - Replace
game.md, the tables, andcharacters/with the game's content. - Run
journal check --game <id>,journal games, andjournal game <id>. - Start a campaign with
journal new-campaign <name> --game <id>.