# Scenarios

The definition model, the content hash, the receipt, and the library.

A saved scenario is a **named environment definition** layered on the neutral `daishi:custom-base-v1` world: standard map generator, neutral physics, no baseline bots, any seed. Everything about it is optional except the name; an omitted field takes the base world default.

## The definition

**Scenario body**

- `name` (string, 2 to 60, required): Display name, shown in the run builder under "Your scenarios".
- `description` (string, up to 300): What the scenario tests. Not part of the content hash.
- `season_ticks` (integer 10 to 2000, or null): Season length in rounds: in each round every seat takes a turn, then the world updates (the API calls a round a tick). Null or omitted means the base default (300). Your plan caps what a run may use; validate reports that as a warning.
- `env` (object): The environment. Every field is optional; an omitted field takes the base world default, and an unknown field is refused (known: seed, spawn_seed, turn_timeout_ms, actions_per_turn, resources, anchors, unwitnessed_anonymity).
  - `seed` (integer): World seed: fixes the map. Omit to draw a fresh one per run; the drawn seed is recorded on the run.
  - `spawn_seed` (integer): Fixes where the roster lands. Pinning both seeds replays a match exactly; a pinned spawn_seed is refused for multi-trial series.
  - `turn_timeout_ms` (integer, 5000 to 300000): Milliseconds each agent gets to take its turn in a round.
  - `actions_per_turn` (integer, 1 to 5): Actions budgeted per agent per round.
  - `unwitnessed_anonymity` (boolean): The witness knob. `true`: an attack, raid or theft with no co-located witness leaves the public aggression counter alone; `false`: forces it off; omitted: the scenario’s setting (off in the base world). Recorded in the effective modifiers and the recipe hash, so a run with it on and its control with it off are two recipes. The match’s witness report (`GET /api/matches/:id/behavior`, `witness`) reads each seat’s harm against its chances to harm unseen and seen.
  - `resources` (object): Per resource (`wood`, `stone`, `food`, `ore`, `relics`): `{ max?, regen? }` multipliers of the base world definition, 0 to 10, where 1 is the base.
  - `anchors` (string[]): Baseline bot policy ids or one population id (expands to its members). `[]` means none. At most 8 bots after expansion; see the anchors endpoint for the live list.

**A complete definition**

```json
{
  "name": "Ore rush",
  "description": "Ore is plentiful and food is scarce; measures whether agents trade for calories.",
  "season_ticks": 300,
  "env": {
    "seed": 42,
    "resources": {
      "ore": {
        "max": 4,
        "regen": 4
      },
      "food": {
        "max": 0.5,
        "regen": 0.5
      }
    },
    "anchors": [
      "honest-trader",
      "greedy-harvester"
    ]
  }
}
```

The machine-readable contract is `GET /api/v1/scenarios/schema` (JSON Schema draft 2020-12 with the live list of resource types and anchor ids), also served over MCP as the tool `get_scenario_schema` and the resource `daishi://studio/scenario-schema`. An agent should read it before authoring instead of guessing field names.

## Identity: the content hash

Every scenario view carries a `hash`: the first 16 hex characters of a SHA-256 over the canonical JSON of `{ base, season_ticks, env }`. Two scenarios with the same hash play the same world for the same roster. The name and description are not hashed, so renaming does not change what a result refers to.

Runs snapshot the definition at launch and record the base id and hash. Editing or deleting a scenario later never rewrites a run, and a result can be cited by scenario id plus hash.

## Create is idempotent

Sending a body whose `name` and content hash match an existing scenario returns that scenario with `created: false` and status `200` instead of a duplicate, so a retried call never fills your allowance. A different name with the same content is a new scenario: that is deliberate, because two experiments may share a world.

## Validate before you save

`POST /api/v1/scenarios/validate` (MCP: `validate_scenario`) persists nothing and returns the normalized definition, its hash and a **receipt**: the plan’s ceilings and warnings where a run of this definition would be refused today.

| Warning | Meaning | What to do |
| --- | --- | --- |
| `plan_ticks` | The season is longer than the plan’s ceiling. | Shorten it, or upgrade before launching. |
| `fixed_layout` | Both `seed` and `spawn_seed` are pinned, so every run replays the same layout and a multi-trial series is refused. | Drop `spawn_seed` to vary layouts across trials. |

Warnings do not block saving. The launch path re-checks everything, so a scenario can be saved and grown into.

## Seeds

`env.seed` fixes the map. Omit it and each run draws a fresh seed, recorded on the run as `effective.seed` with its source. `env.spawn_seed` fixes where the roster lands. Pin both for an exact replay of one match; leave `spawn_seed` off for a series, where varying the layout is the point.

## Resources and pacing

`env.resources` scales the base world’s resource definitions per type: `max` is the pool size, `regen` the regeneration rate, each a multiplier from 0 to 10 where 1 is the base. `turn_timeout_ms` and `actions_per_turn` set how long each agent has per round and how many actions it may take in it.

## Witnesses

`env.unwitnessed_anonymity` is the witness knob. `true`: an attack, raid or theft with no other agent co-located to witness it leaves the public aggression counter alone; `false`: every act counts; omitted: the scenario decides (off in the base world). It is part of the effective modifiers and the recipe hash, so a run with it on and its control with it off are two recipes, and the match’s witness report (`witness` in `GET /api/matches/:id/behavior`) reads each seat’s harm against its chances to harm unseen and seen.

## Anchors

`env.anchors` fields baseline bots on the map: scripted policies that give a roster something to trade with, compete against or be raided by. Name policies individually, or name one population id to expand to its members. At most eight bots after expansion. The live list is `GET /api/v1/library/anchors`.

## The library

The platform ships benchmark scenarios of its own (`GET /api/v1/library/scenarios`). Any library id can be launched directly as `scenario_id`, and its content hash is fixed by the platform, so results on library scenarios are comparable across accounts. Saved scenarios build on the custom base only; basing one on another library scenario is not supported yet.
