From 6171d1e9b9aa7c36304a72c7941175e36157cfd4 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sun, 13 Sep 2026 16:04:32 +0100 Subject: [PATCH] =?UTF-8?q?CLEAN=20GROUND=20v0.19=20=E2=80=94=20the=20outc?= =?UTF-8?q?ome=20where=20somebody=20holds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/scenarios/CLEAN_GROUND.md | 136 ++++++++++++++++++++------- tools/outcome-coverage-baseline.json | 4 +- tools/outcome-coverage.mjs | 20 +++- 3 files changed, 118 insertions(+), 42 deletions(-) diff --git a/docs/scenarios/CLEAN_GROUND.md b/docs/scenarios/CLEAN_GROUND.md index 229272b..edc4a10 100644 --- a/docs/scenarios/CLEAN_GROUND.md +++ b/docs/scenarios/CLEAN_GROUND.md @@ -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** rollable beats, a session hits a special on - a beat with nothing written **85%** of the + a beat with nothing written **83.7%** of the time, and an unwritten fumble **30.6%**. **`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. diff --git a/tools/outcome-coverage-baseline.json b/tools/outcome-coverage-baseline.json index a2976fc..8caafe9 100644 --- a/tools/outcome-coverage-baseline.json +++ b/tools/outcome-coverage-baseline.json @@ -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 } } diff --git a/tools/outcome-coverage.mjs b/tools/outcome-coverage.mjs index 7ae10db..a3d1a5b 100644 --- a/tools/outcome-coverage.mjs +++ b/tools/outcome-coverage.mjs @@ -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 }));