aa3ae24ef2850d22bbc71b35f174f3a55089e728
24
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
32ca982369 |
R-275: the harness had never read a creature's power
41 statblocks carry a POWER in their tactics and simulate.mjs read none of them, so a redcap that ignores the cumulative defence penalty has been measured as a creature that tires -- in check-lethality, in check-focus, and in every figure published about it. The defect was not that the powers were unimplemented, it was that nothing said they were not. powers.mjs classifies all 41: 2 wired, 14 notSimulable with a stated reason, 25 not fight rules. check-powers refuses an unclassified POWER and refuses a notSimulable without a reason -- and it does not test that the harness imports a power, it fights the creature with and without and requires the two to disagree. Moved: the courier 9.4% to 1.0% wiped (it attacks at half while carrying), the redcap 0.7% to 0.9% (small, because these fights rarely spend a second defence). ARGENT AND GULES was wired and then un-wired: it tripled the supporter's wipe rate to 75.2% because the harness has no ground and applied the borough-ground condition unconditionally. Same reason THE PULL is not wired. I had wired one and refused the other on identical facts. Lethality and focus re-recorded, bestiary regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9f3a63174f |
CLEAN GROUND v0.12: convention one-shot, the print pack, and the real opposed roll
Tim settled the last two questions: convention one-shot, and rules.mjs gains a real opposed roll (landed by a peer session at R-273). Both decisions change the document, and the one-shot unblocked the print pack. THE PRINT PACK — docs/scenarios/CLEAN_GROUND_HANDOUTS.html, guard 17. Four A4 sheets, self-contained, no external fonts or assets: H01 the coroner's note on white, H02 the 1962 committee minute on cream with photocopy grain, H03a and H03b the almanac on ruled paper. 11pt floor throughout. The binding constraint is that H03a and H03b must print identically or the almanac trick dies, so that is structural rather than careful: they share one `.almanac` class and every dimension comes from a variable defined once. There is no selector anywhere that names one page and not the other, and check-handouts fails the build if one appears, if their markup structures diverge, if their columns differ, if any row stops reading "41 mi", if the counts stop being forty-one now against fifty-three then, or if the pack and the scenario drift apart. It earned itself immediately: its first run failed my own pack for five rules at 10.5pt, under the house 11pt floor. Rendering was checked visually too, which caught two things no guard would have — the "TO CLEAN GROUND" header colliding with NOTES, and the writing crossing the red margin rule instead of starting right of it. THE ONE-SHOT. Countdown step 6 said "and this is a campaign", which the decision contradicts. Rewritten, and the Close's "leave it filed" ending now says how to land it tonight: do not end on "you'll be back", because the table never will and a hook they cannot take reads as an unfinished scenario. Name the next agent who gets sent, and have Registry thank them. THE OPPOSED ROLL. The scenario-local ruling is deleted; GM ESSENTIALS points at the game. Both beats were re-priced against the real rule by enumerating all 10,000 roll pairs -- exact, no seeds -- and two things fell out: THE OFFER IS PRICED. Ashcroft refusing is taken 21.9% of the time, the safest file at the table. Accepting: 48.7%, past Braithwaite's 37.6%. Accepting does not make him a bit more vulnerable, it makes him the easiest person in the room, and the countdown reaches for the easiest. STEP 5 CAN COME TO NOTHING, 14.5% of the time against an Ashcroft who accepted, and that row fires once and is marked permanent. Previously a silent gap in a climactic beat. It is now a written outcome with read-aloud text: the reach fails, it wears the wrong face for a moment, the party learns what it is and cannot prove it, and the clock still turns to step 6. Also: update-readme's count-word list ran out at sixteen, one guard after its own comment warned about hardcoded lists going stale. It failed loudly rather than silently, so it is an inconvenience and not a defect. Extended. npm run check: 17 guards, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8279cdfda8 |
R-272: guard the tail, and stop prose drifting from its artifacts
Three things Tim asked for together: measure the long end of a fight, make the swing check bite on stale prose, and record in rules.mjs the coincidence that hid an invented rule from two readers. GUARD 15 — check-fight-tail, on tools/fight-tail.mjs. Fourteen guards measured this scenario's combat and none could see a long fight, because every one of them averages. The cap is the measurement here, so the tool passes its own (400, not runFight's default 40) and --update refuses to record anything reaching it. Verified by lowering it back to 40: the cut loses 20 fights to the ceiling, the six-a-side 117, 107 of those ending neither won nor wiped, and the recorder stops. Two claims, both read from a fresh measurement rather than the baseline, so --update cannot silence them. "The long end is twice the median" was rejected as a claim because it is true of both encounters and so distinguishes nothing. Instead: a fight past 15 rounds wipes the party materially more often than a short one, and the SIX-A-SIDE fight is the longer one (median 14 vs 10.7) -- R-270 showing up as duration, since a disabled fighter keeps fighting 30 points down. The cut is shorter because it is decisive, not safer. GUARD 16 — check-cited, on tools/check-cited.mjs. check-firstblood and check-attackers catch the game changing; neither reads the document. Re-record after a re-cast and the artifact updates, the guard goes green, and the prose keeps printing the old number under a citation saying where the new one lives. So citations are now machine-readable -- **40**<!-- cite: first-blood cut.swing --> -- and resolved on every build. 25 of them. It failed three times on its first runs, all real: a config keyed "column" that the prose called "line", two figures rounded 32.3 -> 32, and a vacuous pass on zero citations, now fatal in its own right. It also refuses citation of unstable fields. fight-tail.longest may not reach prose: same party, same seeds, same runs, and renaming a config moved it 71 -> 90 rounds, because seedFor derives the stream from the id. Across seven labels -- median spread 0, p95 1, p99 3, longest 21. A sample maximum reads like a bound and is a property of the label. EXPOSURE states p99 instead. Same discipline on the deadlier ratio: 2.42 with seed spread 1.1, so the document gives its direction and declines to quote its size. RULES.MJS — one comment, no rule change. Over resolveLocationHit: its two thresholds are unrelated and usually agree. disabled is a fraction of the pool per location; majorWound is ceil(hp/2) and feeds only dyingLimitFor; neither removes anyone from a fight, which is conditionFor at 2 hit points or a destroyed head. At 10 hp, leg/abdomen/chest capacity is 5 and majorWoundFor is 5, and those locations take 12 of 20 melee results and 15 of 20 ranged -- so two readers reconstructed a rule that does not exist, checked it against the log, and were confirmed by it. The note says to test an arm, the only place the difference shows. Guards verified to bite, not assumed: drift, re-cast, censoring, the longest refusal, the rounding catch and the vacuous-pass catch were each forced and each failed the build with the right guidance, then restored. npm run check: 16 guards, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0b86c5ffc3 |
R-268: guard the sentence that names a player's character
CLEAN GROUND prints that the four-player cut is three effective attackers and that Ashcroft is not one. Nothing checked it, and a GM reads it aloud to decide who a real player spends four hours being. effective-attackers.mjs measures per attack, not per fight: counted per fight Braithwaite leads on disables, but only because his armour buys him a third more swings -- per attack he is the weakest of the three. Both rates are recorded and only the per-attack one is reasoned from. Asserts exact drift, then the sentence: three clear 5% of attacks disabling, one does not, and that one is Ashcroft. Re-recording does not silence the claim check; verified in a worktree. declared-cast.mjs holds the cast marker reading both scenario guards need, rather than a copy in each. It was briefly named scenario-cast.mjs, which check-scenarios sweeps into the scenario corpus -- its own example marker was read as a real cast. update-readme dropped any guard that exited non-zero, so check-rollable vanished from the README and the count word fell to thirteen while fourteen guards ran. A missing line is now fatal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ce1103ec0d |
R-267: a guarded home for the first disabling blow
Another session verified R-266's measurement, agreed the frame was better than its own, and declined to publish it because the figure had no guarded lineage a doubter could re-derive. It was right, and the fix costs 0.7s of build time -- which is the number I should have checked before calling it a reading tool and not a guard. first-blood.mjs gains --update/--check and is now both the reader and the measurement of record; check-firstblood.mjs is a thin wrapper over it, the same shape as check-bestiary. Compares the recorded figures exactly, then asserts only what a page would claim: first blood lands within 5 points of even in the cut, and its swing exceeds the six-a-side line's by more than both noises. Recording it moved the swing from the scratch run's 43 to 40 against a seed-to-seed spread of 2.1 -- high by more than its own noise, which is the argument in miniature. update-readme's COUNT_WORD could not reach thirteen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5dcaaf519a |
The simulator can be read now, not just totalled (R-263)
Every guard here reports a number per creature, and a number cannot say why a fight went the way it did. That gap is what produced R-258 through R-260: each written from a side harness built to watch a fight, each modelling something slightly different from the game, two of the three wrong in ways no guard could catch because no guard was involved. runFight takes an optional `say` sink. It reports what the simulator already decided — the attack roll and its target, a defence and the penalty it was made at, the landing level after a dodge downgrades it, damage against armour before and after armourAgainst, the location, major wounds, disablement, death. It never touches the generator, so a narrated fight and a silent one are the same fight; check-lethality and check-focus both still match their baselines exactly with the hook in place. tools/playthrough.mjs (npm run play) is its consumer, in the same commit deliberately: a hook with no reader is the exact defect this project keeps finding in its own rules, and adding one to the measurement pipeline with only a scratchpad file calling it would have been committing the thing I have spent the session removing. Pack size comes from focus-baseline.json so the fight you read is the fight check-focus measures; creatures recorded as pinned are played solo. Three redcaps, seed 20260913, same seed both ways. Spread fire: wiped in 10 rounds, 3 dead, and all three redcaps still standing — 39 hit points spread three ways so that none of it finished anything. Focus fire: same opening, diverging at one target choice in round 1, party wins in 20 with two up. The +13.2 points check-focus records, seen once instead of averaged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1d915c55cf |
check-focus: the focus-fire claim is an artifact, not a sentence (R-261)
R-258 put a tactic on a GM-facing page from an unguarded harness, R-259 found it worth
nothing, R-260 found the replacement right for the wrong reason. Three corrections, each
landing as prose with nothing checking it — the arrangement that let
|
||
|
|
555fa761c0 |
check-rollable: guard eleven, because a real skill is not the same as a route
check-scenarios resolves every skill a scenario NAMES against the catalogue, which is a different question from whether anybody present can roll it. CLEAN GROUND shipped four commits with three clues gated on Track (base 10), Navigate (base 10) and Science (Botany) — base 1, so the 1% floor was the whole of it — and every guard passed, because all three are perfectly real skills. A desk playtest found it by auditing the acts against the sheets the scenario casts. That audit is mechanical, so it belongs in the build. Two tiers. Corpus-wide, some roster agent must reach VIABLE for every skill any scenario names. Per scenario, a document that DECLARES its cast is held to that cast instead, and that is the tier that catches this defect class. The cast is declared rather than inferred, and that is the interesting part. The first version scraped pc_ keys out of the prose and swept up the substitutes named in CLEAN GROUND's player-count scaling — a cast of nine instead of six, which put Sandoval and his Track 35 in scope and made the guard pass the very bug it was written for. A guard that guesses the cast is worse than none, because it reports success. CLEAN GROUND now carries a cast comment and the guard reads it off the raw text, since scenarioText strips HTML comments. Verified load-bearing rather than assumed: re-injecting the original Track tag into the real CLEAN_GROUND.md fails the guard, naming the skill, the base chance and the declared cast. Worth recording that the corpus-wide tier would never have caught it — Lindqvist trains Botany at 40, so it is rollable by the roster and simply not by the six who were cast. The first fixture test passed for that reason and misled me; only the declared-cast tier finds this. VIABLE is 25 and is justified, not picked: it is the commonest base chance in the catalogue, what an untrained agent brings to Spot, Listen or Brawl, so it is the level the game itself treats as worth attempting. Below it a clue is not gated, it is buried. check-scenarios exports its tag parser rather than growing a second copy, behind the invokedDirectly pattern simulate.mjs already uses; a duplicated parser is exactly what this repository's one standing law forbids. update-readme then caught me fairly — it cross-checks the advertised guard list against npm run check — so the guard is registered there too and the README advertises eleven in all three places. No REVIEW_LOG entry: the log is clean at R-260 and is being appended to every few minutes by concurrent work, so the end of that file is the likeliest place to collide. Left for whoever next touches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f932a79121 |
The firing order, in the bestiary, generated (R-258)
DEFENCE_STEP is -30, so a creature's Dodge is not a percentage it has for a round — it is one it has once or twice. The median dodge in the book is 45% and goes 45 -> 15 -> 1 inside a single round. No player-facing document said so, which hid a real decision: what order the party shoots in. The bestiary now has a section on it. Only a landing hit spends a defence, so a miss strips nothing; each landing hit makes the next attack of that round easier; therefore the cheapest reliable weapon fires first and the one you most need to land fires last — against the supporter, the weapon the borough does not own. Everything it states is generated from DEFENCE_STEP, resolveBands and the statblocks: the step, the ladder, the median, the spread (13 of 47 creatures stripped by one landing hit, 25 by two, 9 by three) and the list of the hardest to strip. Negative-tested by setting the step to -15 and -20; the page rewrites itself both times. No win rates are published: the ordering measurements come from a table-side harness that ignores wound penalties, bleeding and dying, so the page states the mechanism and quotes no percentages. My first draft used the dodgiest creature in the book as the worked example — 80%, three landing hits to strip — directly beneath a claim that the first attack of the round eats the dodge. True of a modest dodge, false of a good one, and both were on the same page. tools/bestiary.mjs --check has existed since the document did, with the comment "(for the guards)", and no guard ever ran it: a check written and wired to nothing, the same shape as every rule found computed and never read. That was tolerable while the page restated statblocks and is not now that it derives from rules — change DEFENCE_STEP and the document lies to a GM. check-bestiary.mjs is the tenth guard, negative-tested both ways it can fail: a rule change that staler the page, and a hand-edit of the page. update-readme flagged it as "run by the build, not advertised" before I listed it, and the README's count word went from nine to ten by itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ddc4f99ff4 |
Hit locations reach ordinary combat, and creatures get a body
THE DEFECT. locationFor had ONE caller in the engine, hitLocationRoll, and hitLocationRoll was called from fall, burn, detonate and burstAttack. Falling, fire, explosions and automatic fire. Nothing else. rollWeaponDamage rolled dice, printed a number and returned it, so the chat Damage button, the character sheet's damage button and the item sheet's all stopped there. A grenade knew which arm it took off. A sword did not. Every individual piece was already correct - the tables, per-location armour and coverage, disable and destroy thresholds, Major Wound, dying, death, the figure on the sheet. They simply were not joined up, which is why this reads as a wiring change rather than a new subsystem. - rollWeaponDamage now carries through to hitLocationRoll: it rolls the location, applies that location's armour, applies the wound, and handles the rest through the machinery that already owned it. Nothing here re-implements any of that - locationModeFor picks the melee or ranged column from the weapon's class. The two columns have always differed and nothing was choosing between them - with no target it does exactly what it did before and SAYS SO on a card, rather than failing quietly - burstAttack opts out with locate:false - it rolls its own location per round, and letting both fire would have applied every round of a burst twice BODY PLANS. Two new tables: quadruped (four legs, fore/hindquarters, neck, a head that is hard to reach) and winged (that, plus wings). Forequarters are the vital. DISABLE EITHER WING AND IT CANNOT FLY, which is the fight a party can win. Barghest, Kelpie and Church grim were all species "baseline" - a black dog, a horse and a grave-hound, each hit-located with two arms and a chest. Fixed. Added "The supporter", a winged heraldic beast, so the new table is exercised by real content instead of existing unused. Body plan is a NEW FIELD, not a species. A bear is not a playable people with characteristic dice and talents, so creature specs take bodyPlan and the playable species list is untouched. THE NPC SHEET had no hit locations at all - 102 lines against the character sheet's 627 - while every NPC in the game had them derived and wired to consequences. It now carries the body-plan selector, the figure, the per-location table, the condition and a GROUNDED flag. The sheet class already extended the character sheet, so it already HAD the context; the template never used it. FOUND IN PASSING, by the new guard: kind "body" - the abdomen, the trunk, the hindquarters - did nothing whatever. Its printed effect has always promised "-30% to all Physical actions and bleeding 1 hit point per round until First Aid" and the code delivered none of it, on every body plan including the humanoid default. It now bleeds 1, or 2 destroyed, exactly as bodyX says. NEW GUARD: check-anatomy. Every body plan must cover 1-20 exactly once in BOTH modes, every location must be drawn and every drawn shape must be a location, and every kind must be one locationEffectsFor acts on, has effect text for, and has a destruction outcome. Verified it fails on a d20 gap, a shapeless limb and an unhandled kind before wiring it in. Ninth guard. VERIFIED IN A RUNNING FOUNDRY, not just unit-tested: melee bar reads the Melee column and the rifle reads Ranged on the same creature; forequarters graded as a vital hit; a maxed wing set flightLost and the sheet said GROUNDED; the real chat Damage button rolled 1D20 15, found the right foreleg, applied 12 through armour, disabled the leg and took the dog from 16 to 4; and with nothing targeted nothing was touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
339d5dd8dc |
test runs the guards through bun, not through npm
`test` was "npm run check", so reaching bun meant shelling out to npm first. It is "bun run check" now — one runner, and npm is out of the path. Not the eight-guard chain copied into `test`: that is the same shape of mistake the rule formulas made before rules.mjs existed, a list written twice and changed once. `check` stays the only place the guards are named. Verified: npm test exit 0, bun run test exit 0, bun run check exit 0. Worth knowing: bare `bun test` does NOT run this. It is bun's own test runner, it matches no files here, and it exits with an error. The ways in are `npm test`, `bun run test` and `bun run check` — which is what the README already tells you to use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
54a759e057 |
Run the guards under bun, including the two new ones
Rebased onto the creature-forge work, which had added check-creatures and check-lethality to the chain and had `check` back on node. Taking either side whole would have been wrong: the node side loses bun, the bun side silently drops the two new guards. So the runner is bun and the guard list is upstream's, all eight of them. Only `check` and `readme` move. build, icons, portraits, bestiary, mj and simulate stay on node — they were never part of this change, and build writes LevelDB packs through classic-level. Verified before committing: each guard run individually under bun, then `npm run check` end to end, exit 0. check-rules 103 rules, 33 files check-kits OK check-lang 995 keys, no collisions check-templates 19 templates compile check-behaviour 67 behavioural tests check-scenarios 9 files, 62 mechanic tags check-creatures 101 actor specs across 8 sources check-lethality 46 creatures, none drifted more than 6 points Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
322389bc8a |
A bestiary a GM can actually read, and the defect it found immediately
Forty-six creatures and no way to look at them. They live in content.mjs, which is source code, and in the compendium, which shows one actor at a time behind two clicks. A GM prepping a case can browse neither, so in practice the bestiary was whatever the GM happened to remember writing. docs/BESTIARY.md is generated, never hand-written, for the reason the rules journal is generated from rules.mjs: a reference that can disagree with the thing it references is worse than no reference. It opens with a one-page table sorted by how much of a party each creature puts on the floor - the part that gets printed - then an entry each with characteristics, attack, the POWER line pulled out of the tactics, and the measured figure read straight from the lethality baseline. Every number is derived, so the document cannot drift, and check-lethality already refuses the build if the measurements move. IT FOUND SOMETHING IN ITS FIRST RUN. The generic heavy natural attack is keyed `grasp` and was NAMED "Vesh grasp limb". Twenty-two creatures carry it and five are Vesh, so the at-a-glance table cheerfully printed "Vesh grasp limb" against a black dog, a kelpie, a church grim and an apple tree. Renamed to "Grasping limb"; the key is untouched, so every kit reference and every built pack still resolves. That defect was invisible for as long as nobody could see the bestiary in one place, which is the argument for the document. The caveats are given more room than the numbers, deliberately. The measured column is SOLO against ONE frozen party, and the document says so three times: encounter lethality moves with how many there are and, much harder, with who the players brought - the same fight runs 0% to 100% across party choice. It points at --spread rather than pretending a single figure is an answer. Also records the real remaining gap: 21 creatures fight with nothing but their own bodies and still carry human anatomy, so the barghest can be shot in the left arm. The machinery to fix that is built and measured; what is missing is anatomy tables for the shapes they actually are - a quadruped, an immobile object, a swarm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4b718595fe |
check-lethality: guard eight, and the README stops advertising a subset
Seven guards checked that content is WELL FORMED. None checked what it DOES, so a change to a damage modifier, a hit-point formula or the armour value on a service vest could double a creature's lethality without touching one line of that creature - and nothing in the build would notice, because the creature did not change. Built as a regression test rather than the hand-declared bands the creature-forge plan described. Bands are the wrong shape here: declaring "keepers: dangerous" across forty-six creatures means inventing forty-six judgements, and after --spread it is clear the interesting question is not "is this dangerous" - that has no single answer - but "is this the same as it was". A recorded baseline answers exactly that, needs no authoring, and cannot be argued with. Every creature is measured solo against a FROZEN party of four, two armed postings and two trades, at a fixed seed, so the only thing that can move a number is a change to the rules or to the creature. Tolerances are 6 points of wipe rate and 0.35 agents, which is outside the noise floor of 200 runs - a guard that cries wolf gets deleted. Verified by tampering: told the baseline the nuckelavee was harmless and the guard caught it at +16.5 points and +1.67 agents down, exit 1. Runs in half a second, so it joins the pre-build checks rather than being something to remember to run. Also: the README's guard block was a hardcoded list of four while the suite was seven. It had silently stopped mentioning every guard added after it was written, including check-creatures and check-scenarios. Now all eight, and the prose says eight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9ddea43720 |
make-portraits: every creature gets art it cannot disagree with
Sixty of the seventy-six actors had no portrait. The sixteen that did are the hand-made duty roster; the whole bestiary and every scenario NPC fell back to a generic silhouette. The point is not that this is free. A portrait composed from anatomy.mjs cannot contradict the statblock, because it is drawn from the numbers the hit-location roll uses: silhouette from BODY_ART, height from SIZ, build from STR against SIZ, plating from naturalArmour, a mark for the primary weapon's class. A Cadence would get five bodies because its table has five. Nothing has to be kept in step. Colour carries what a GM wants off a thumbnail — whether the thing is a person, folklore, recovered machinery, or the far side. That reads the shouted category the role lines already carry (FOLKLORE, HORROR, FORWARD), which nothing was using, and falls back to structure for the ones with no tag. build-packs resolves portraits by stem and extension rather than an exact .webp filename, so a creature gains art without an edit to content.mjs and a hand-made raster always beats the generated diagram. A spec that NAMES a portrait and has none is still a hard build failure. These read as diagrammatic field-guide plates and will not match the Midjourney agent portraits. That split is deliberate: the agents are the cast, the bestiary is case material. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2c547efe72 |
simulate: a lethality harness that uses the real rules
THROUGH TRAIN publishes "MEASURED, 400 runs each" and a 24% wipe rate; the starter publishes its own table. Nothing in this repository could produce either number, so both were claims rather than results and neither could be re-checked after a rule moved. The harness knows no rules of its own. Everything comes out of rules.mjs, because a harness with its own copy of the combat loop measures a game nobody is playing and drifts silently the first time a rule changes. Two things had to move for that to be possible: applyDifficulty, resolveBands and gradeRoll -> rules.mjs. The whole of d100 resolution lived in ringbrp.mjs, which node cannot import. All three are pure, so they moved and the engine re-exports them; no caller changes. check-rules then refused them for having no spot-check, so they now have fifteen, covering the 1% floor, the 96-99 rule and 00-always-fumbles. They had none in their entire life inside the engine. expandFromRegister -> tools/expand-spec.mjs. A roster agent is written as a job and a rank and carries no skills or kit; everything it can do is derived at build time by a function locked inside build-packs.mjs, which cannot be imported because importing it runs the build. A simulated agent expanded by a second copy of that logic is not the agent that gets packed. Moved verbatim; both sides import one copy. Deterministic throughout: same creature, party and seed, same numbers, which is what lets a published table be verified rather than remembered. What it finds: for the Act Three standoff the seed barely matters (0.5-2.8% wipe across five seeds, eight rounds throughout) and party composition dominates everything. Four support agents against the six miners wipe 92.8% of the time; swap in the heavy and the warden and it is 0.3%. The published 24% is not a property of the encounter, and no single figure can be. Models Reaction re-rolled each round, banding, graded defences, the stacking defence penalty, damage by band, major wounds and the dying clock. Does NOT yet model hit locations, bleeding, panic, cover, range bands or fire modes, all of which make a losing fight worse. Read the output as a floor, never a ceiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c3002e93c9 |
check-creatures: validate every actor spec, and fix the vesh talents it found
Nothing validated creature specs. check-kits validates postings and check-scenarios validates prose; buildActor throws on an unknown skill or item key and is silent about everything else, so species, marksmanship style, characteristic blocks, species-locked talents and misspelt field names could all be wrong and still build, pack and play. Every packed actor once shipped as species "hominid" and the whole cast was hit-located as something the rules do not contain. tools/creature-schema.mjs derives every allowed value from the catalogues rather than retyping them: skills from SKILL_CATALOGUE, kit from the item catalogues, species from SPECIES, stats from CHARACTERISTIC_DICE, and the two marksmanship styles out of the language file. It reports every problem with a spec rather than dying on the first, and names the nearest real key. It also reports fields buildActor never reads, because "armours:" is not an error, it is an actor that silently equips nothing. On its first run it found six baseline humans carrying vesh biology. "unblinking" is cat: species, species: vesh, and its own rule text reads "A Vesh has no face to read". Out of Step is reclassified as anomalous with a Coherence cost, which is what every talent in that category has and what the Anchor who is three years older than his birthday says always wanted to be; the other five are swapped to agency and field talents that fit them. Not fixed here: the root cause is upstream in postings.mjs, where twelve posting talent pools still offer unblinking, distributed or quorum to any agent. Picking replacements across those is a design decision, not a mechanical one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
973b4a53aa |
THROUGH TRAIN rewritten into five acts
Act One is now an induction at the Bureau, Act Two puts the players through the crossing and makes them find the train, Act Three is a town-side obstacle, Act Four is the boarding, Act Five is the four minutes. 4h00 in five acts, was 3h30 in three. The restructure earns one thing the old shape could not: THE CROSSING OPENS WHERE THE RECORD SAYS THE LINE IS, which is an empty bench of scrub on the wrong side of the wash. The agents arrive where the paperwork put the railway, not where the railway is, so the fraud is the ground under their feet and finding the train is real work. Registry's file has this wrong and Bewley believes the file. - Act Three: Rance Doyle and five unpaid miners take the agents for company detectives, and they are not wrong to. Resolves on what the players SAY - no roll makes it go away, no roll makes it a fight. Losing costs them the 1881 issue, not their lives. His brother Tom is aboard and dies at minute two of every pass, which is the wiring between the acts - new cast: Bewley, Prosser, Rance Doyle, Shard, Tom Doyle, Cordera man - new handouts: the company notice, and the six names in Ambrose's hand - 8 GM pages, 10 handouts, 15 actors, 13 plates MEASURED, 400 runs each: the Act Three standoff is 0.4/4 hurt from cover and 1.25/4 in the open, both ending early - but FOUGHT TO A FINISH it puts 1.75 of 4 down and wipes the party in 24% of runs. The early stop is what makes the act survivable, not the miners being weak. My first draft of that table was guessed and wrong in all three rows. FOUND AND FIXED: there is no Ride skill in this game and never has been. The boarding set-piece has asked for "[CUS: Ride, then Athletics]" since it was written, through a playtest, which is an instruction to roll a thing nobody has. Horses are now handled by a local and the agent rolls Athletics. NEW GUARD: check-scenarios resolves every [CUS: Skill] tag in every scenario against SKILL_CATALOGUE and fails the build on a skill that does not exist. Verified it fails on the real bug before wiring it in. Sixth guard. NOTE: the three new plates (Bureau, north bench, standoff) are placeholders in this commit - Midjourney is still rendering them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab619f4b73 |
Remove the dice textures and the whole Dice So Nice path
Deleted: the seven PNGs in dice/, tools/make-dice-textures.mjs, the `npm run dice` script, the RB_DICE colourset and texture tables, diceSetFor, the diceSoNiceReady hook, and rbDressDice — which was tagging ten roll sites with coloursets that no longer existed. 99 lines of module, plus the assets. README and DEPLOY updated; they still advertised all of it. Recoverable with `git revert` if it is ever wanted back. This also explains the 01-that-was-12. The RESULT was never wrong: skillRoll rolls one 1d100 and grades on roll.total, and the chat card derives the tens and units it prints from that same total. Checked all 100 values of percentileFaces and 60 live rolls — the printed faces reconstruct the graded number every time, with no mismatches. What disagreed was the ANIMATION. The custom addDicePreset labels registered for d100 and d10 remapped which physical face means what, so Dice So Nice could land showing 01 while the roll behind it was 12. Nothing downstream ever read the animated faces, so no roll was ever adjudicated on the wrong number — but a table watching the dice had every reason to believe otherwise, which is its own kind of wrong. Disabling the skins in 1.0.6 already removed it; deleting them makes it permanent. Verified in a running Foundry after removal: skill roll, weapon damage, hit location, major wound and Coherence all resolve and post their cards, no errors, and game.ringbrp.diceSetFor is gone from the API. Version 1.0.7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
02ec47a351 |
Fix every NPC sheet, which has never opened
npc-sheet.hbs line 43 read
{{#if (lookup ../skillsByCategory key).length}}
and Handlebars cannot read a property off a subexpression. That is a PARSE
error, so the template never compiled, so every NPC sheet in the system threw
on click — Natives & Threats, the starter's enemy roster, and the whole LAST
ADMISSION cast. #if already treats an empty array as falsy, so the .length was
never doing anything in the first place.
It was invisible to all four existing guards, and the reason is worth writing
down: check-rules reads .mjs, check-behaviour says in its own header that it
cannot reach sheets, and every play-through in this project has driven the
system through its API rather than by clicking. Nobody had ever opened an NPC
sheet, including me, in any of the sessions that "played through" scenarios
full of NPCs.
So: check-templates.mjs, a fifth guard, compiling every .hbs in about a tenth
of a second, wired into the build ahead of check-behaviour. It also names the
specific shape — a property read off a subexpression — because the raw parse
error says only where. Verified it bites by reintroducing the bug. Handlebars
is a devDependency now.
Opening the sheet then showed a second thing nobody had seen: the NPC skill
rows carry four children and were using the character sheet's seven-column
grid, so the name landed in a 13px column and the whole list overlapped
itself. The NPC row now has its own four-column template.
Verified by opening all six LAST ADMISSION cast sheets with an error hook
attached: six rendered, no errors, and Old Marrow reads HP 20/20, major wound
10, armour 2, Brawl 60%, Melee Weapon (Polearm) 60%.
Version 1.0.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7f8355f0d0 |
Add the player primer and cheat sheet, and brand every journal
Ten pages a player reads before session one, in their own compendium at OBSERVER ownership: nine of setting, one cheat sheet. The draft called the organisation "the Bureau"; everything else in the game calls it the department or the agency, so that is what it is called. Thin areas filled rather than padded: the seven character principles as a list, the real Coherence band table instead of a paragraph describing one, and "what continuity is not" pulled out of the middle of a page because it is the most important sentence for a new player. The cheat sheet's numbers are computed from rules.mjs and lang/en.json at build time. A hand-typed one would have been the sixth time this project shipped a number that disagreed with the code, and the one the players were holding. R-250: the design tokens were never on :root. Journals rendered as default parchment, and four things were wrong in sequence — v14's hook is renderJournalEntrySheet; its `element` is the node, so `[0]` returns the first CHILD and put the class on a header button; there is no .journal-page-content in v14; and then the real one: with the selector matching, font-size, line-height and padding all applied while every colour did not, because the palette is declared on .ringbrp-sheet/.ringbrp-chat/.ringbrp-window and never on :root. A journal inherited no tokens, so every var() silently became nothing. A rule that HALF applies is not a specificity problem — I spent three attempts treating it as one, including a version bump on a cache theory that was wrong. Branded: primer, rulebook, starter GM text and handouts. Scoped by flag, so other modules' journals are untouched. Version 0.9.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
43508259ac |
Consolidation pass after a Codex review
Four real defects, each verified against the source before acting on it. Trade expertise was coupled to agency rank. A probationary burglar came out at 35-50% and a veteran at 65-80%, as though fifteen years of picking locks were something the department conferred. TRADE_BANDS is now fixed at 50-65/30-50 at every rank, and INDUCTION scales with service instead (20-32 probationary to 45-60 veteran) because that half genuinely is the department's. The two axes were the wrong way round. This is the thing I flagged myself after PAPER HARBOUR and then left alone — flagging a defect is not the same as fixing it. Bonus points were sprayed across every skill in the game. grantFullSkillList runs immediately before the loop and puts all fifty-nine skills on the sheet, and the loop picked from actor.items, so a veteran's ninety points landed anywhere. The comment two hundred lines above has always said "over the core skills"; the code never did. "Random species" passed "baseline" into generators that support random perfectly well. No behavioural tests. The strongest point in the review: the three guards are static and none of them can tell whether a rule is READ, which is the only kind of defect this project has ever shipped. tools/check-behaviour.mjs now runs 28 deterministic tests, gated into the build, covering R-231/232/243/245/246, graded defences and the lamp rule. Proved it bites by re-breaking R-246. The register moved to postings.mjs, a Foundry-neutral module the engine, rulebook, guards and tests all import normally — they were previously pulled out of the engine with a regex and eval, which worked and was a trap. My first version of that used `export … from`, which re-exports without local bindings, so ROLES was undefined and the system threw on init: node --check passes that, loading it in Foundry does not. Also: the contact doc comment still said "only OFFER moves standing"; check-kits accepted mas/app, which are the DISPLAY names of siz/cha, so a posting declaring one would have passed and produced an undefined characteristic; package.json disagreed with the manifest; README counts were three revisions stale and are now generated at build. Deferred with reasons: splitting the 6,700-line engine into modules. Right, and a multi-session refactor whose risk is exactly the silent breakage demonstrated above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1734612c06 |
v0.7.0 — vehicles, Borrowed Authority, and a door for the case generator
Since 0.6.1: - VEHICLES as their own Item type: crew, seats, cargo, hull, armour, range, terrain and the skill that works them, with speed as a band. 14 of them, from the pool car and the Custodian van to a sealed crawler and the carriage that is already parked outside wherever you were going to go. - STORES roughly doubled to 118 gear items across four eras — standing issue and paperwork, transport stores, and the forward stores Registry will not hand a probationary officer. Catalogue now 175 items, 231 icons. - Agents leave creation properly equipped: standing issue, the posting's kit, what seniority adds, and one personal effect that is theirs and not the department's. - All 25 postings now describe themselves in the creation dialog. - BORROWED AUTHORITY, the access subsystem. No hacking skill and nothing to break into: access is a named credential with a scope, a Trust that erodes, and red flags describing how its real owner behaves. Inside scope there is no roll. Two tiers beyond is impossible at any skill. Coherence widens a record, and pushing one past what its Trust will carry merges your file with theirs. - Defences graded by level, and damage now rolls at the level a blow landed at. - Dodge's DEX x 2 base is finally evaluated; it read 0% for most postings. - Brawl is a fallback rather than a peer of the weapon in hand. - check-lang guard: one key that was both a value and a branch silently un-localised the entire system. - The case generator is reachable from the Actor Directory, and no longer puts every character in the world on a new case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d0144f744e |
The Custodians v0.6.1 — full system
A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that deals with crossings, where reality keeps a record of what belongs where and can be made to get it wrong. Contents: - rules.mjs as the single authority for every rule formula, with 40 rules and 132 spot-checked values. Nothing may restate a formula it defines. - 25 postings, each with a key characteristic, trained skills, talents and its own kit; a 175-item catalogue across four eras; 231 generated icons. - Reaction Lanes initiative, re-rolled every round including the first. - Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong. - Graded defences: only a critical stops a critical. - Borrowed Authority — access as a record rather than a hacking roll. - A case generator, and a rules journal generated from rules.mjs so the book cannot drift from the code. Built compendia are committed so a remote Foundry needs only the files. node_modules is not; `npm install` restores it for rebuilding content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |