docs: add agent workflow and reference docs
AGENTS.md defines the session loop; docs/oracle.md and docs/games.md cover table mechanics and the game package contract.
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
# 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
```js
|
||||
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 }` plus
|
||||
`ctx.findTable(name)` and `ctx.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"`, missing `index.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
|
||||
|
||||
1. Copy `games/generic/` to `games/<id>/`.
|
||||
2. Rename the package to `@solo/game-<id>` and set `id` in `index.js`.
|
||||
3. Replace `game.md`, the tables, and `characters/` with the game's content.
|
||||
4. Run `journal check --game <id>`, `journal games`, and `journal game <id>`.
|
||||
5. Start a campaign with `journal new-campaign <name> --game <id>`.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Oracle and tables
|
||||
|
||||
## The d6 oracle
|
||||
|
||||
A single d6 answers yes/no questions about the world.
|
||||
|
||||
| d6 | Result | Meaning |
|
||||
|----|--------|---------|
|
||||
| 6 | **Yes, and…** | It works, and there is a fortunate extra. |
|
||||
| 5 | **Yes** | It works, plainly. |
|
||||
| 4 | **Yes, but…** | It works, at a cost or with a complication. |
|
||||
| 3 | **No, but…** | It fails, but there is a consolation or opening. |
|
||||
| 2 | **No** | It fails, plainly. |
|
||||
| 1 | **No, and…** | It fails, and there is an additional setback. |
|
||||
|
||||
Ask one question at a time, and phrase it so both answers are interesting. The
|
||||
oracle answers the world; it does not decide what the PC wants.
|
||||
|
||||
```
|
||||
journal oracle "Is the gate locked?" --campaign ashfall
|
||||
```
|
||||
|
||||
## Complications
|
||||
|
||||
The complication table supplies a twist. Roll it on `and`/`but` results, or
|
||||
whenever the fiction needs one:
|
||||
|
||||
```
|
||||
journal oracle "Is the gate locked?" --complicate --campaign ashfall
|
||||
journal roll complication-themes --campaign ashfall
|
||||
```
|
||||
|
||||
The table is resolved like any other (see below), so a campaign without its own
|
||||
`complication-themes.csv` falls back to the one the game ships.
|
||||
|
||||
## Spark tables
|
||||
|
||||
Any CSV in a campaign's or game's `tables/` directory is a spark table. The
|
||||
header must contain `min`, `max`, and `text`; `tags` is optional.
|
||||
|
||||
```csv
|
||||
min,max,text,tags
|
||||
1,1,"A sign of recent passage",exploration
|
||||
2,2,"Something out of place",mystery
|
||||
```
|
||||
|
||||
- `min`/`max` are inclusive integers and define the die range. `1..6` is a d6;
|
||||
`1..66` reads as a d66; any contiguous range works.
|
||||
- `text` is the result, quoted if it contains a comma.
|
||||
- `tags` holds one or more tags separated by `;` or `|`.
|
||||
|
||||
### Lookup order
|
||||
|
||||
`journal roll <name>` searches:
|
||||
|
||||
1. `campaigns/<campaign>/tables/<name>.csv`
|
||||
2. `games/<game>/tables/<name>.csv`
|
||||
|
||||
So a campaign can override a single game table without copying the rest. Use
|
||||
`journal tables` to see both sources, labelled.
|
||||
|
||||
```
|
||||
journal roll sparks --campaign ashfall
|
||||
journal roll sparks --tag mystery --campaign ashfall
|
||||
journal roll sparks --count 3 --campaign ashfall
|
||||
journal roll sparks --game generic
|
||||
```
|
||||
|
||||
A tagged roll picks uniformly among matching entries rather than rolling on the
|
||||
range, since ranges stop being meaningful once the table is filtered.
|
||||
|
||||
## Validation
|
||||
|
||||
`journal check` parses every table for a campaign and its game, reporting
|
||||
overlapping ranges and parse failures as errors, and range gaps and missing
|
||||
directories as warnings.
|
||||
|
||||
```
|
||||
journal check --campaign ashfall
|
||||
journal check --game generic
|
||||
```
|
||||
Reference in New Issue
Block a user