CLEAN GROUND v0.19 — the outcome where somebody holds

Applies desk pass 8's five fixes, and adopts R-292's TAG_OPEN.

1. Step 5 now has all three of its outcomes. opposedContestFor returns taken,
   held and unsettled; the document printed the first and the third. "They
   hold" is 48.1% against the default target, 52.4% against Braithwaite and
   74.3% against an Ashcroft who refused — which v0.17 established is what
   most tables have, so the middle column is the one they will play. It gets
   its own countdown row, its own table, and read-aloud of its own, and the
   COMES TO NOTHING paragraph is now explicitly the unsettled case so it can
   stop being the nearest text to an outcome it is not about. The reason
   they held is the case's own argument arriving as good news: a person with
   a life on the record is a difficult document to overwrite. Every figure
   re-enumerated over all 10,000 roll pairs before writing.
2. EXPOSURE's five things are six, in order, under a heading that says six.
   Introduced in my own v0.16 and survived two versions; asserting a unique
   match protects against editing the wrong text and not against inserting in
   the wrong place.
3. EXPOSURE opens with a one-minute box. The body is untouched — nothing in
   it is padding — but 3,176 words is sixteen minutes about the encounter the
   document exists to prevent, and a GM with thirty minutes of prep now has
   somewhere to stop.
4. The stale open question is closed. Braithwaite has not been on the
   critical path since v0.14 un-gated the tell, and pass 6 ran the act with
   the Xenology and the Psychology both failed.
5. Act Four's Psychology has a special: which file it thinks it is, and how
   recently it read it. It corrects them on Prichard's service history — it
   is not remembering, it is citing.

Coverage 6 -> 7 specials, so the cited figure moved 85% -> 83.7% and
check-cited caught the prose before I did, which is what it is for.

R-292 adopted: outcome-coverage now composes its body onto check-scenarios'
TAG_OPEN through tagRe, so the two readers cannot drift on what starts a tag
while keeping the different bodies they need — and tagRe's fresh matcher per
call avoids the shared-lastIndex defect, this file being the second caller
that would have found it. Verified identical across all seven spellings.

npm run check: 19 guards pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 16:04:32 +01:00
co-authored by Claude Opus 5
parent df38be794b
commit 6171d1e9b9
3 changed files with 118 additions and 42 deletions
+101 -35
View File
@@ -9,10 +9,10 @@
| | |
|---|---|
| **Version** | **v0.18**, 13 September 2026 |
| **Version** | **v0.19**, 13 September 2026 |
| **Can it be run?** | **Yes, from this document alone.** A GM with this file and the compendium can run it end to end. |
| **Is it convention-ready?** | **Nearly.** The print pack is built (`CLEAN_GROUND_HANDOUTS.html`, held by `check-handouts`). What is missing is a run with human beings, and a slot. |
| **Playtesting** | **Seven desk passes, no human runs.** `_1.md` GM chair (seed 4271) · `_2.md` Act Four stress (9314) · `_3.md` player chair, contrarian (5580) · `_4.md` **the clock, against v0.13's recut timings** (6142) · `_5.md` **the four-player cut, played** (8815) · `_6.md` **the four untested branches, forced** (3319) · `_7.md` **an honest session, nothing forced** (7742). Forty-seven fixes plus three post-passes, and pass 7's finding is now a guard: **`check-outcomes` reads every bracketed mechanic tag in the acts and holds what its beat states.** Pass 7 ran ~3:35 against 3:40 at six players with no cuts taken — the first honest pass inside the clock unaided. |
| **Playtesting** | **Seven desk passes, no human runs.** `_1.md` GM chair (seed 4271) · `_2.md` Act Four stress (9314) · `_3.md` player chair, contrarian (5580) · `_4.md` **the clock, against v0.13's recut timings** (6142) · `_5.md` **the four-player cut, played** (8815) · `_6.md` **the four untested branches, forced** (3319) · `_7.md` **an honest session, nothing forced** (7742) · `_8.md` **the cold read, plus an honest session** (5108). Fifty-two fixes plus three post-passes, and pass 7's finding is now a guard: **`check-outcomes` reads every bracketed mechanic tag in the acts and holds what its beat states.** Pass 7 ran ~3:35 against 3:40 at six players with no cuts taken — the first honest pass inside the clock unaided. |
| **What it is** | A **convention one-shot**, decided 13 September 2026. It ends in the slot; there is no second session and the text must not promise one. |
| **Where it runs** | **Contingency 2027 — Sunday 31 January 2027, afternoon** (slot 9), Searles Leisure Resort, Hunstanton. |
| **Runtime** | **3h30 of play plus a 10-minute break — 3h40 wall clock**, which is the house budget. Act Two was cut from 65 minutes to 45 in v0.13 to get there. The **Pacing Note** carries a further 65 minutes of cuts; take them as the default. |
@@ -84,7 +84,7 @@ premise (a walk north that never arrives) and the predator's method (imitation).
- ⚠ **A SPECIAL IS COMMONER THAN A FUMBLE AND THE TEXT WRITES FEWER OF THEM.** At a rating
of 63 a special or better is **13%** and a fumble **2%**; at 53, **11%** against 3%. Across
the case's **21**<!-- cite: outcomes beats --> rollable beats, a session hits a special on
a beat with nothing written **85%**<!-- cite: outcomes session.unwrittenSpecial --> of the
a beat with nothing written **83.7%**<!-- cite: outcomes session.unwrittenSpecial --> of the
time, and an unwritten fumble **30.6%**<!-- cite: outcomes session.unwrittenFumble -->.
**`check-outcomes` holds those figures and ratchets them** — coverage may improve and may
not quietly go backwards — so they are measured from the acts rather than typed here.
@@ -308,11 +308,49 @@ Advance one step per act, plus one whenever the agents spend Coherence on the fa
| **3** | It takes an NPC. The NPC's file is still good, so the copy holds |
| **4** | The copy is fluent. It is inside the cordon and helping |
| **5** | It reaches for **the lowest POW in the room** — opposed POW×5 (it is **75**, not 100), permanent. **Unless Ashcroft accepted THE OFFER in Act Two, in which case it takes him instead and he resists at Difficult.** GM ESSENTIALS has the resolution; Act Four beat 3 has the reasoning |
| **5 — they hold** | ⚠ **THE LIKELIEST RESULT, and the one this document used to leave blank.** If the agent's band beats the thing's — or ties it, because a tie goes to whoever is being acted upon — **the reach simply fails and the person is still themselves.** **48.1%** against Okonkwo, **52.4%** against Braithwaite, **74.3%** against an Ashcroft who refused. This is **not** the unsettled row below and must not be played as it |
| **5 — unsettled** | **If neither side succeeds, the reach fails and the row is spent.** Common enough to plan for, and **THE OFFER nearly quadruples it**: **3.8%** against an Ashcroft who refused, **10%** against Braithwaite, **14.5%** against an Ashcroft who accepted. It does not retry. See below — it is an outcome, not a gap |
| **6** | It goes south at dusk wearing whatever face it has. **Read the Close's "leave it filed" note: in a one-shot this lands tonight, not as a sequel** |
**WHEN STEP 5 COMES TO NOTHING — and it will, about one time in seven against an Ashcroft
who accepted.** **That is nearly four times the 3.8% it would be if he had refused**, and
### Step 5 has three outcomes, and the one nobody expects is a person holding
**Enumerated over all 10,000 roll pairs — no seed, reproduce it from `opposedContestFor`:**
| It reaches for | Takes them | **They hold** | Unsettled |
|---|---|---|---|
| **Okonkwo, POW×5 55 — the default** | 40.7% | **48.1%** | 11.3% |
| Braithwaite, 60 | 37.6% | **52.4%** | 10.0% |
| **Ashcroft, 85, refused THE OFFER** | 21.9% | **74.3%** | 3.8% |
| Ashcroft, 85 → 42, accepted | 48.7% | **36.8%** | 14.5% |
**Most tables will play the middle column.** Ashcroft refuses about five times in eight, and
even against the easiest file in the room the thing fails more often than it succeeds. Desk
pass 8 rolled it and found the document had nothing to say.
#### WHEN THEY HOLD — read this one most often
**The understudy has practised for thirty years and it is not stupid: it takes the easiest
file in the room. This is the person it could not open.** That is the only moment all night
when somebody beats it, and it is not a shrug — play it as the thing being *surprised*,
which is the one expression it has never rehearsed.
> *Read aloud or paraphrase:* It reaches, the way a man reaches for a coat he is certain is
> his. Then it stops, and for a half-second there is nothing behind its face at all — no
> colleague, no stranger, nobody. It says your name, with a question mark it did not mean to
> put there.
- **Say why they held, in one line, to whoever it was:** there is too much of you already
filed. Not courage, not willpower — **a person with a life on the record is a difficult
document to overwrite**, which is the case's whole argument arriving as good news for once.
- **The clock still turns.** Step 5 is spent whichever way it goes; step 6 comes and it
leaves with whatever it already has. Do not give it a second attempt.
- **It now knows which of them is hard**, and it will not reach for that one again. If the
party works that out, they have learned something true about the record that no roll in
the case teaches.
#### WHEN IT COMES TO NOTHING — the unsettled row, which is a different outcome
**And it will, about one time in seven against an Ashcroft who accepted.** **That is nearly four times the 3.8% it would be if he had refused**, and
the reason is the same Difficult that makes him easy to take: a resisting side at 42
succeeds far less often, so *both* sides missing becomes much likelier. Accepting THE OFFER
makes Dominic both the likeliest person to be taken **and** the likeliest reason the whole
@@ -867,6 +905,13 @@ through with him.
it is failed or never attempted, **it surfaces in beat 3 instead** — the thing says it
itself, unprompted, while being examined, in the tone of a man correcting a clerical
error.
- **On a special or better (01–08 at 40):** not only that it thinks it is the file, but
**which file, and how recently it read it.** It corrects them on Prichard's service
history — a posting they have not mentioned, a date they have got wrong — with the mild
impatience of a man who has been over his own record more carefully than they have.
**It is not remembering. It is citing.** Desk pass 8 rolled this special on the first
honest session after `check-outcomes` measured how often one lands with nothing written,
and there was nothing written.
- **Borrowed Authority** — pull the record. The copy checks out; *the column does not*. The
instrument is working correctly and is useless.
**On a critical:** the record answers too well. Prichard's file has been *read* recently,
@@ -930,6 +975,21 @@ whoever it is copying, always.
### EXPOSURE — what a fight actually costs. Read before Act Two, not before Act Four.
> ### ⏱ IF YOU HAVE ONE MINUTE, READ ONLY THIS
>
> **The monster cannot hurt them. The refugees can wipe them.** The understudy is a 1d3
> punch that dies in two rounds — do not buff it. Six of the column kill one party in
> seven; ten kill nine in ten. **The six at the back are never fought alone**: two of the
> column joining in takes that fight from nobody dying to one wipe in thirteen, and **at
> four players the six at the back wipe 58.7% on their own.**
>
> **A fight also costs the session** — a median 13 rounds, 24 if the column joins, which is
> most of an act. **So there is no version of a fight here that is good for anybody**, and
> the reason to avoid one is the clock at least as much as the bodies.
>
> *Everything below is the measurement behind those four sentences, and the six numbered
> lessons at the end are the short version. A GM who has read this box can run the case.*
**The finding that matters: the understudy cannot hurt them, and the refugees can wipe
them.** That is the scenario's whole thesis, and it is not a claim — it is measured. A GM
who does not know it will pad Act Four to make it frightening and will be padding the one
@@ -996,7 +1056,7 @@ thirteen. Three takes it past one in four.** Measured the same way as the table
- **Say the price out loud before dice are picked up.** That is not a warning against the
fight; it is the information the party's own briefing would have given them.
#### The five things a GM needs from that table
#### The six things a GM needs from that table
1. **Do not fix the understudy.** It is a 1d3 punch with a 60% chance to land, and it goes
down in two rounds. That is correct and it is the point: the horror is that it passed
@@ -1067,32 +1127,6 @@ thirteen. Three takes it past one in four.** Measured the same way as the table
rate**. Against the full six it is worth a third of that, because one fighter is a sixth
of that line and a quarter of the cut. **This is a property of the cut, not of the
column.**
6. ⚠ **A fight costs the session, and that is the better reason not to have one.**
Everything above prices fights in bodies. The cost that ends conventions is the clock:
| | Median rounds |
|---|---|
| Six hollow men, six players | **13** |
| Six of the column, six players | 14 |
| Six hollow men + three of the column | **24** |
| Six hollow men, four players | **30** |
A d100 round with six agents and nine opponents is not a fast round. **Assume three to
four minutes each** — an estimate, not a measurement — and the encounter this section
warns about is a **sixty-to-ninety-minute** encounter in an act budgeted at fifty.
A GM who has been told the party probably survives has been told the less important
half. **If a fight starts, the session is now the thing to protect**: see the Pacing
Note, which carries the instruction.
*Recorded in `tools/first-blood-baseline.json` and re-checked on every build by
`check-firstblood`; read it with `node tools/first-blood.mjs`. Earlier drafts of this
paragraph carried a swing of 43 from a single seed, which was high by more than its own
seed-to-seed noise — the figures above are three seeds and are the ones of record.*
At four players, **any** violence against the column is a party-ending decision. **Say
the shape aloud, not the average:** "nearly half of all parties die" is the honest
warning, and "you will probably lose two people" is not.
5. **Their sheets read about twice as effective as they are, and the number doing it is
Dodge 68.** This one is arithmetic off two published numbers, not a measurement. The
column dodges at **68%** (`The Quiet Neighbours`, `tools/content.mjs`; the bestiary
@@ -1120,6 +1154,32 @@ thirteen. Three takes it past one in four.** Measured the same way as the table
passed hand to hand with both parties off the ground rather than marksmanship. The
column being uncatchable is the point; see their tactics line.
6. ⚠ **A fight costs the session, and that is the better reason not to have one.**
Everything above prices fights in bodies. The cost that ends conventions is the clock:
| | Median rounds |
|---|---|
| Six hollow men, six players | **13** |
| Six of the column, six players | 14 |
| Six hollow men + three of the column | **24** |
| Six hollow men, four players | **30** |
A d100 round with six agents and nine opponents is not a fast round. **Assume three to
four minutes each** — an estimate, not a measurement — and the encounter this section
warns about is a **sixty-to-ninety-minute** encounter in an act budgeted at fifty.
A GM who has been told the party probably survives has been told the less important
half. **If a fight starts, the session is now the thing to protect**: see the Pacing
Note, which carries the instruction.
*Recorded in `tools/first-blood-baseline.json` and re-checked on every build by
`check-firstblood`; read it with `node tools/first-blood.mjs`. Earlier drafts of this
paragraph carried a swing of 43 from a single seed, which was high by more than its own
seed-to-seed noise — the figures above are three seeds and are the ones of record.*
At four players, **any** violence against the column is a party-ending decision. **Say
the shape aloud, not the average:** "nearly half of all parties die" is the honest
warning, and "you will probably lose two people" is not.
#### How long it runs, which is not the median
**Every figure above is a middle, and a middle is not a plan.** The table prints median
@@ -1510,6 +1570,12 @@ at which a GM most needs one. Desk pass 5.*
(`opposedContestFor`, R-273). The scenario-local ruling is deleted and GM ESSENTIALS
points at the game. Both beats were priced against the real rule and the Act Four row
gained an *unsettled* outcome it did not previously have.
- **Scent is unusable** (base 0, 1% floor). I moved Act Four's working test to Xenology
(Baseline), which puts Braithwaite on the critical path. Worth deciding whether you want
the whole scenario's one reliable test sitting on a single sheet.
- ~~**Scent is unusable** (base 0, 1% floor), so Act Four's working test moved to Xenology
(Baseline) — worth deciding whether you want the scenario's one reliable test sitting on a
single sheet.~~ **Resolved by the fixes themselves, and closed 13 September 2026.** Scent
is still unusable and still says so in Act Four. But **Braithwaite has not been on the
critical path since v0.14**, which made the tell un-gated — *anyone who examines him
properly finds it, with no roll* — and **desk pass 6 ran the act with the Xenology and the
Psychology both failed and it held.** The Xenology now buys the tell *early and privately*
rather than at all. Nothing to decide; flagged by desk pass 8's cold read as a question the
document had already answered and left open.
+2 -2
View File
@@ -8,12 +8,12 @@
"stated": {
"failure": 18,
"fumble": 6,
"special": 6
"special": 7
},
"session": {
"anyFumble": 41,
"unwrittenFumble": 30.6,
"anySpecial": 93,
"unwrittenSpecial": 85
"unwrittenSpecial": 83.7
}
}
+15 -5
View File
@@ -31,14 +31,23 @@ import { resolveBands, gradeRoll } from "../rules.mjs";
import { ROSTER } from "./roster.mjs";
import { expandFromRegister } from "./expand-spec.mjs";
import { declaredCast, SCENARIO } from "./declared-cast.mjs";
import { tagRe } from "./check-scenarios.mjs";
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), "..");
export const BASELINE = path.join(ROOT, "tools", "outcome-coverage-baseline.json");
/* ------------------------------------------------------------------ the beats */
/* ONE PATTERN FOR BOTH HALVES OF THIS FILE, and it agrees with check-scenarios' `tagsIn`
deliberately. R-291 measured the two apart: `tagsIn` was widened to tolerate case and
/* THE OPENING IS SHARED, THE BODY IS NOT — R-292, and the distinction is the point. The
two readers must agree about what STARTS a tag, and they had drifted there: `\[CUS:` was
copied into both files and widened in only one. They must NOT share a body, because
check-scenarios captures the whole tag for `skillsIn` while this file stops at the
em-dash, with the outcome text after it. So `TAG_OPEN` comes from check-scenarios and the
body stays here. `tagRe` builds a fresh matcher per call: a shared /g regex carries
`lastIndex` between callers, which is a defect that only appears once there is a second
caller — and this file is the second caller.
The history it closes. R-291 measured the two apart: `tagsIn` was widened to tolerate case and
spacing around the colon, this file's strict half was not, and `[cus: Spot — x]` was then
READ by one guard and reported unparseable by the other. The build failed, which is the
safe direction, but it failed saying the tag could not be read while another guard had
@@ -46,7 +55,8 @@ export const BASELINE = path.join(ROOT, "tools", "outcome-coverage-baseline.json
`[CUS= Spot]`, which neither strict reader accepts, so the canary keeps its teeth.
check-behaviour asserts the two readers agree — that assertion is the thing that must
not be deleted, not this comment. */
const TAG = /\[\s*CUS\s*:\s*([^\]—]+?)\s*(?:—|\])/gi;
const TAG_BODY = String.raw`([^\]—]+?)\s*(?:—|\])`;
const TAG = () => tagRe(TAG_BODY);
const indentOf = l => { const m = l.match(/^(\s*)(?:[-*]|\d+\.)\s/); return m ? m[1].length : null; };
const isHeading = l => /^#{1,6}\s/.test(l);
@@ -66,7 +76,7 @@ export function beatsIn(text) {
if (/^\s*```/.test(l)) { inFence = !inFence; return; }
if (inFence) return;
const quoted = /^\s*>/.test(l);
for (const m of l.matchAll(TAG)) {
for (const m of l.matchAll(TAG())) {
if (quoted) { out.push({ line: i + 1, skill: m[1].trim(), quoted: true }); continue; }
let owner = i;
for (let j = i; j >= 0; j--) {
@@ -95,7 +105,7 @@ export function beatsIn(text) {
/** A `[CUS …]` that this reader cannot parse is worse than no tag: somebody thinks it is
one. Asymmetric with beatsIn on purpose — R-286's castLikeIn, same reasoning. */
export function beatLikeIn(text) {
const good = [...text.matchAll(TAG)].map(m => [m.index, m.index + m[0].length]);
const good = [...text.matchAll(TAG())].map(m => [m.index, m.index + m[0].length]);
return [...text.matchAll(/\[\s*CUS\b[^\]]*\]/gi)]
.filter(m => !good.some(([a, b]) => m.index >= a && m.index < b))
.map(m => ({ raw: m[0].slice(0, 70), line: text.slice(0, m.index).split("\n").length }));