# Clocktower Notebook: model and evidence contract

The phone app has two explicit search inputs: the legacy **first-night** Evidence /
Worlds search, and the **Trouble Brewing history** editor in Timeline. Both search
initial-role assignments and retain one supporting hidden-state witness per
assignment. Counts are not probabilities or counts of all possible histories.
Temporal mechanics cover all 22 characters under the explicit standard-rule scope below.

## Trouble Brewing history

`temporal.mjs` is the pure transition kernel. `temporal-search.mjs` searches role
assignments, shown Drunk tokens, fixed red herrings, unknown report nights and
omitted private actions. It runs locally in a separate Worker without a provider.
Initial setup is standard Trouble Brewing with 5–15 players. Travellers, Fabled,
Loric, other scripts, homebrew and Storyteller mistakes are excluded and unknown
fields are rejected. No social likelihood or recommendation to execute is inferred.

The state tracks actual character, shown token, alignment, living players,
Poisoner source and expiry, Monk protection, consumed Slayer/Virgin abilities,
Butler master, dead votes, executions, succession and game end. Poison ends at
next dusk or immediately when its source dies or changes character. Registration
is local to each ability check; it does not give the registered character's ability
or change alignment. The official first/later-night order governs information.

```js
import {solveTemporal} from './temporal-search.mjs';
const result = solveTemporal({
  schema: 'botc-temporal-v1',
  players, // [{id, name}, ...] in initial clockwise order; all start alive
  evidence: [
    {kind:'testimony', player:'alice', role:'slayer', mode:'assumption'},
    {kind:'alignment', player:'alice', alignment:'good', mode:'hard'},
  ],
  reports: [
    {player:'bob', role:'fortune_teller', night:null, targets:['bob','alice'],
     value:false, authenticity:'good', mode:'assumption'},
  ],
  phases: [
    {phase:'night', number:1, deaths:[]},
    {phase:'day', number:1, events:[], ended:false},
  ],
  limits: {maxResults:80, maxNodes:1000000, maxMs:4000},
});
```

Every evidence/report/day-event row requires `mode`: `claim` is reference only;
`assumption` and `hard` both constrain the model, with different provenance.
`enabled:false` disables a row. Optional `source` preserves uncertainty and attribution.
Private actions are actual constraints. Unsupported fields fail even on inactive rows.

Evidence supports `role`, `alignment`, `inPlay` as described below, plus `shown`
(actual shown token) and `testimony` (good truthfully report their shown token;
evil may lie). These statements concern the initial setup. A report uses
`authenticity:'authentic'` to require an actual received report, including an evil
Spy, or `'good'` for faithful good reports with evil permitted to lie. `night:null`
searches recorded eligible nights; an integer fixes the night. `reliability:'healthy'`
excludes impairment at that wake; it does not suppress target registration.

Information payloads below also apply to temporal reports. Additional abilities:

| Role | Payload and timing |
| --- | --- |
| Undertaker | `targetRole` or `null` for no information, later nights after execution death |
| Ravenkeeper | `target`, `targetRole`, only the night the speaker dies |
| Spy | `grimoire:{playerId:{...}}`, each night at the Spy's wake |

Partial Spy observations support `role`, `shown`, `alignment`, boolean `alive`,
`poisoned`, `redHerring`, `protected` (effective Monk protection), `usedSlayer`,
`usedVirgin`, and `master` (player ID or null). These describe mechanical state,
not arbitrary Storyteller reminder placement. Impaired Spy information may be false.

Authentic initial evil information uses evidence `kind:'setupInfo'`, `player`,
`team:'Minion'|'Demon'`, optional complete `minions` player-ID list, `demons` list
for Minions, and exactly three distinct good `bluffs` for Demons. Omitted lists
are unknown, while `[]` is an observed empty list. These game-rule observations
precede poison and require at least seven players. Spy/Recluse registration can
affect learned player lists. Entering bluff tokens explicitly assumes actual in-play exclusion. Rare rulings
that use registration to override bluff eligibility are outside this constraint;
leave bluff tokens unspecified for those games. One active record per recipient
and wake is allowed, so conflicting complete lists cannot be combined.

Phases are N1/D1/N2/D2 without gaps. A night may provide the complete `deaths`
list; omitted deaths are unknown. Optional `actions` constrain Poisoner (`poison`),
Monk (`monk`), Butler (`butler`) or Imp (`imp`), with `player` and `target`.
An Imp action may specify `redirect`, `successor` (including null), or boolean
`recluseDemon`. Omission searches legal choices; explicit false/null stays binding.

A day has ordered `events` and explicit `ended`. Events are `execution` (`player`),
`slayer` (`player`, `target`, boolean `killed`), `nomination` (`player`, `target`,
boolean `triggered` for Virgin execution), or `ballot` (`voters`, optional `nominee`).
Death-capable events also permit successor/registration choices. Day deaths and
executions must be completely recorded: the search does not invent omitted public
Slayer shots or executions. An open final day does not trigger Mayor prematurely.

Set a day's `completeVoting:true` only when all nominations and ballots are entered.
It enforces nomination eligibility, majority, ties, the unique highest vote and
execution. Otherwise execution is a supplied observation independent of vote totals.
A ballot may add `qualifying:{voterId:[playerIds]}`: a complete observation of which other players'
hands were up or whose vote had already counted at that voter's turn. This constrains
Butler support. Final counted votes alone do not establish the master's hand timing.
Dead votes are consumed once regardless of whether voting records are complete.

Results include `complete`, `stopReason`, `status`, `errors`, `stats`, `coverage`,
and candidates with `assignments`, `shownRoles`, `redHerring`, `reportNights`,
replayable `witness`, `finalRoles`, `alive`, and `winner`. Only an exhausted search
with zero candidates is `contradiction`; an empty bounded search is `inconclusive`.
Completeness concerns initial-role projections for these inputs, never all hidden
histories or unsupported rules. One witness's hidden choices are examples, not
individually proven facts. Search can be cancelled by editing the history.

The phone's optional `botc-history-v1` extension preserves existing notebook notes,
evidence and recorded events. The three input collections stay explicit: legacy
first-night evidence, recorded bookkeeping replay, and tested history. The copy
button imports first-night evidence only on request. The history editor never
silently treats a recorded death as proof of a legal ability history.

## Legacy first-night search

The remainder documents the separate `engine.mjs` API and its narrower scope.

## Provenance

The existing project was inspected at `~/botc-solver`, including
`src/data/troubleBrewing.ts`, `src/engine/z3solver.ts`, and
`src/engine/infoValidation.ts`. Its preserved copy is
[`docs/archaeology/games/botc-solver/sources`](https://github.com/ethancd/deevgames/tree/master/docs/archaeology/games/botc-solver/sources).
The role identifiers, role categories and standard setup-count table follow that
project. The browser search, validation and result contract here are newly written;
no original files or personal notes were changed. No character art, flavor text or
ability descriptions were copied. Blood on the Clocktower belongs to its creators;
this is an unofficial aid.

The original implementation returns capped assignments without a completeness
signal. Its separate `infoValidation.ts` Fortune Teller helper explicitly ignores
the red herring for a yes. Those behaviors are not carried into this engine.

## First-night supported scope

- Trouble Brewing only; 5–15 players, initial clockwise seating, everyone alive.
- Unique actual roles, standard Townsfolk/Outsider/Minion/Demon counts, exactly one
  Imp, and Baron replacing two Townsfolk with two Outsiders.
- Actual-role and actual-alignment positive/negative observations and assumptions;
  actual character in/out of play.
- One first-night report per player from Chef, Empath, Washerwoman, Librarian,
  Investigator or Fortune Teller.
- The Drunk is a good Outsider shown a Townsfolk token that is not actually in play.
  Their information may be correct or incorrect. Poison does not turn them into
  the Townsfolk whose token they hold.
- If a Poisoner is in play, one target is shared across every first-night report.
  A poisoned information source may receive correct or incorrect information.
  Poisoned Spy/Recluse targets cannot use their registration ability.
- Healthy Spy and Recluse registration can vary by ability check. Chef checks each
  adjacent pair separately, including the wraparound pair: a Recluse can count in
  one adjacent pair and not the other. Empath counts the two initial neighbors.
- Washerwoman/Librarian can see a Spy as the learned good character. Investigator
  can see a Recluse as the learned Minion. Librarian zero is possible when every
  actual Outsider is a healthy Recluse registering outside the Outsider type, or
  when there are no Outsiders; other actual Outsiders prevent a healthy zero.
- Fortune Teller checks the Imp, one fixed red herring, and optional healthy
  Recluse demon registration. The red herring is selected at setup from good
  players, also allowing a Spy registering good at setup before night poisoning.
  The Fortune Teller may select themself. A red herring remains a red herring when
  poisoned; it is an effect of the Fortune Teller's ability.

No deaths, executions, Slayer/Virgin events, Demon jumps, protection, resurrection,
night two onward, Travellers, Fabled, custom scripts, homebrew, character changes,
Storyteller mistakes, bluff-token restrictions, or social likelihood model are
implemented. Supported characters without first-night evidence mechanics still
appear in initial-role assignments. Unsupported active evidence returns `invalid`;
it is not silently treated as true. Alternative reports for one player must remain
claims or be disabled, rather than combined as if they happened on different nights.

The registration choices above follow the official character abilities, including
unusual legal uses; ask your Storyteller if their game deliberately restricts them.
This model does not infer those restrictions from conventional storytelling habits.

Rules checked against the official wiki on 2026-10-18:
[Chef](https://wiki.bloodontheclocktower.com/Chef),
[Spy](https://wiki.bloodontheclocktower.com/Spy),
[Recluse](https://wiki.bloodontheclocktower.com/Recluse),
[Drunk](https://wiki.bloodontheclocktower.com/Drunk),
[Poisoner](https://wiki.bloodontheclocktower.com/Poisoner),
[Librarian](https://wiki.bloodontheclocktower.com/Librarian), and
[Fortune Teller](https://wiki.bloodontheclocktower.com/Fortune_Teller).
The Spy-as-red-herring setup option and Recluse-hidden-from-zero option apply the
general registration rules to setup and detection respectively.

## Claims, assumptions and hard observations

`mode: "claim"` never constrains the search. A statement made by a player is not
automatically true or authentic. `enabled: false` disables any evidence row.
Enabling a row as `assumption` or `hard` makes its stated constraint binding;
the difference is provenance and user intent, not mathematical strength.

An actual-role assertion `role: "chef"` excludes the Drunk. Being shown a Chef
token alone does **not** establish that assertion. Actual-alignment observations
also differ from an ability's possibly misleading registration.

An active `info` row means: assume this is an authentic report of information
received while shown that Townsfolk role. It permits that actual role or the Drunk
shown that role, and permits poison unless `reliability: "healthy"`. It excludes
evil bluffing and good players intentionally lying about their information.
Use `claim` when authenticity is uncertain, or compare assumption sets.
`healthy` additionally asserts actual role, sober and unpoisoned at that moment;
it does not suppress other players' Spy/Recluse registration.

## JavaScript API

```js
import { solve, ROLES, SCOPE } from './engine.mjs';
const result = solve({
  script: 'trouble_brewing', // optional, no other scripts accepted
  players: [ // 5–15, initial clockwise order
    {id: 'p1', name: 'Alice'}, {id: 'p2', name: 'Bob'},
    {id: 'p3', name: 'Cora'}, {id: 'p4', name: 'Dev'},
    {id: 'p5', name: 'Eli'},
  ],
  evidence: [
    {id: 'e1', kind: 'role', player: 'p1', role: 'chef', mode: 'claim'},
    {id: 'e2', kind: 'info', player: 'p1', role: 'chef', value: 1,
      mode: 'assumption', reliability: 'uncertain'},
  ],
  limits: {maxResults: 80, maxNodes: 150000, maxMs: 1500},
});
```

Evidence kinds (all also accept `id`, `mode`, `enabled`):

| Kind | Fields |
| --- | --- |
| `role` | `player`, `role`, optional boolean `is` (default true) |
| `alignment` | `player`, `alignment: "good" / "evil"`, optional `is` |
| `inPlay` | `role`, optional `is` |
| `info` | `player`, source `role`, optional `reliability: "uncertain" / "healthy"` |

Info payloads:

| Source role | Additional fields |
| --- | --- |
| `chef` | integer `value`, 0 to player count |
| `empath` | integer `value`, 0–2 |
| `washerwoman` | `targets: [id, id]`, learned Townsfolk `targetRole` |
| `librarian` | `targets: [id, id]`, learned Outsider `targetRole`; OR `value: 0` with no pair |
| `investigator` | `targets: [id, id]`, learned Minion `targetRole` |
| `fortune_teller` | `targets: [id, id]`, boolean `value` |

Pairs require two distinct seated players. If `night` is supplied it must be 1.
Role IDs retain the original snake_case names, including `fortune_teller` and
`scarlet_woman`. The `ROLES` export supplies `id`, `name`, `team`, `alignment`.

The result includes:

- `candidates`: `{id, assignments: {[playerId]: roleId}, poisoned,
  drunkShownRole, redHerring}`. Hidden fields give **one supporting example**,
  not established facts. A null means that source is absent in that projection.
- `complete`: whether all initial-role assignments in this scoped model were
  searched. This is never a claim to have enumerated full game timelines.
- `status`: `invalid`, `candidates`, `contradiction`, or `inconclusive`.
  `contradiction` requires a complete search with zero candidates. Zero examples
  at a bound are `inconclusive`, not proof of impossible evidence.
- `stopReason`: `null`, `result_limit`, `node_limit` or `time_limit`.
- `errors`, `warnings`, `assumptions`, `scope`, and `stats`.
- `topCruxes`: questions about disputed actual roles, with partitions of retained
  projections (`answer`, `role`, `count`) and an explicit `basis` warning. Concrete
  `nextStep` prompts ask for role claims, corroboration of named information pairs,
  or initial Empath neighbors. Cruxes first favor distinctions between good,
  Minion and Imp, then the split among retained examples. Testimony remains a claim.

Search traversal is deterministic and interleaves potential Imp seats and Baron
setups. A wall-clock bound may stop at different points on different devices.
Results at a bound are biased examples, not uniform samples. Counts and crux order
are never odds, confidence, or a recommendation to execute anyone. Even agreement
among every retained example does not clear a player when search is incomplete.
The result cap checks for an additional candidate before declaring truncation;
finding exactly the cap can still be a complete search.

Run focused checks with `node --test botc/engine.test.mjs`.
