One generator, so a citation names one fight (R-265)
tools/playthrough.mjs exists to show a reader why a measured number is what it is. I cited it to another session — read seed 2, see the wipe the 45% row is made of — and the reproduction failed in front of them. They reported different outcomes at both seeds and guessed the cause correctly from outside: the two tools were not drawing from the same stream. They were not. simulate.mjs used a private mulberry32 makeRng; playthrough.mjs had its own LCG written to look like it. Both deterministic, both reproducible alone, and "seed 2" named a different fight in each — which breaks the only thing the tool is for. Its own comment claimed a seed here names the same fight there. check-focus carried a third copy of that LCG, so the two guards described the same game with different dice. makeRng is exported and both files use it. A playthrough seed is now exactly the first fight of simulate.mjs --seed <n>: --runs 1 --seed 2 and the playthrough give 9 rounds, 4 of 4 down, 1 dead, both. The other half was my citation rather than the code: the command I sent omitted --mode, so it plays both targeting arms and prints two fights. They read the last line, I quoted the first. The summary line now names the arm. check-focus re-recorded under the shared stream; figures move a point or two. What it buys is that the bimodality analysis now reproduces the published means exactly — 2.31, 3.03, 0.40, 0.42 against the four rows CLEAN GROUND publishes. Under the old LCG it agreed to within a decimal, which looked like corroboration and was two experiments landing near each other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
796bb7df73
commit
8627fa3536
@@ -31,7 +31,7 @@ import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { NPCS } from "./content.mjs";
|
||||
import { ROSTER } from "./roster.mjs";
|
||||
import { runFight, seedFor } from "./simulate.mjs";
|
||||
import { runFight, seedFor, makeRng } from "./simulate.mjs";
|
||||
|
||||
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), "..");
|
||||
const BASELINE = path.join(ROOT, "tools", "focus-baseline.json");
|
||||
@@ -67,7 +67,10 @@ const party = PARTY_KEYS.map(k => {
|
||||
|
||||
/* One generator per arm, derived from the creature's own key, so inserting a creature
|
||||
cannot perturb its neighbours — R-252, learned the hard way. */
|
||||
const rngFor = seed => { let s = seed >>> 0; return () => ((s = (1664525 * s + 1013904223) >>> 0) / 4294967296); };
|
||||
/* R-265: the harness's own generator, not a second one of the same shape. This file used
|
||||
to roll its own LCG, so the two guards described the same game with different dice and
|
||||
no figure here could be reproduced from simulate.mjs. */
|
||||
const rngFor = makeRng;
|
||||
const winRate = (foes, targets, seed) => {
|
||||
let won = 0;
|
||||
for (let i = 0; i < RUNS; i++) {
|
||||
|
||||
+132
-129
@@ -20,66 +20,66 @@
|
||||
"creatures": {
|
||||
"quiet_neighbours_npc": {
|
||||
"n": 6,
|
||||
"spread": 47.4,
|
||||
"focus": 55.7,
|
||||
"gain": 8.3,
|
||||
"noise": 2.5
|
||||
"spread": 47,
|
||||
"focus": 54.9,
|
||||
"gain": 7.9,
|
||||
"noise": 2.7
|
||||
},
|
||||
"keepers": {
|
||||
"n": 5,
|
||||
"spread": 59.9,
|
||||
"focus": 70,
|
||||
"gain": 10.1,
|
||||
"noise": 0.5
|
||||
"spread": 60,
|
||||
"focus": 69.8,
|
||||
"gain": 9.8,
|
||||
"noise": 2.7
|
||||
},
|
||||
"cleaner": {
|
||||
"n": 6,
|
||||
"spread": 57.8,
|
||||
"focus": 71,
|
||||
"gain": 13.2,
|
||||
"noise": 6.8
|
||||
"spread": 59.4,
|
||||
"focus": 70.1,
|
||||
"gain": 10.7,
|
||||
"noise": 1.4
|
||||
},
|
||||
"cleaner_marksman": {
|
||||
"n": 5,
|
||||
"spread": 51.2,
|
||||
"focus": 62.1,
|
||||
"gain": 10.9,
|
||||
"noise": 0.7
|
||||
"spread": 51.8,
|
||||
"focus": 61.9,
|
||||
"gain": 10.1,
|
||||
"noise": 0.5
|
||||
},
|
||||
"revenant": {
|
||||
"n": 3,
|
||||
"spread": 63.1,
|
||||
"focus": 73.3,
|
||||
"gain": 10.2,
|
||||
"noise": 1.6
|
||||
"spread": 61.9,
|
||||
"focus": 75.4,
|
||||
"gain": 13.5,
|
||||
"noise": 2.2
|
||||
},
|
||||
"duplicate": {
|
||||
"n": 5,
|
||||
"spread": 58.4,
|
||||
"focus": 69.3,
|
||||
"gain": 10.9,
|
||||
"noise": 3.2
|
||||
"spread": 60.5,
|
||||
"focus": 69.7,
|
||||
"gain": 9.2,
|
||||
"noise": 2.4
|
||||
},
|
||||
"barghest": {
|
||||
"n": 3,
|
||||
"spread": 26.2,
|
||||
"focus": 36.8,
|
||||
"gain": 10.5,
|
||||
"noise": 2.2
|
||||
"spread": 24.8,
|
||||
"focus": 36.5,
|
||||
"gain": 11.8,
|
||||
"noise": 3.5
|
||||
},
|
||||
"kelpie": {
|
||||
"n": 3,
|
||||
"spread": 54.6,
|
||||
"focus": 67.3,
|
||||
"gain": 12.8,
|
||||
"noise": 1.8
|
||||
"spread": 52,
|
||||
"focus": 67.1,
|
||||
"gain": 15.1,
|
||||
"noise": 2.8
|
||||
},
|
||||
"redcap": {
|
||||
"n": 3,
|
||||
"spread": 31.4,
|
||||
"focus": 44,
|
||||
"gain": 12.6,
|
||||
"noise": 2
|
||||
"spread": 31.1,
|
||||
"focus": 41.9,
|
||||
"gain": 10.9,
|
||||
"noise": 1.8
|
||||
},
|
||||
"boggart": {
|
||||
"n": 2,
|
||||
@@ -87,18 +87,18 @@
|
||||
},
|
||||
"churchgrim": {
|
||||
"n": 3,
|
||||
"spread": 42.1,
|
||||
"focus": 54.7,
|
||||
"gain": 12.6,
|
||||
"noise": 3.4
|
||||
"spread": 43.7,
|
||||
"focus": 55.7,
|
||||
"gain": 12,
|
||||
"noise": 2.1
|
||||
},
|
||||
"supporter": {
|
||||
"n": 2,
|
||||
"pinned": 3.8
|
||||
"pinned": 4
|
||||
},
|
||||
"hollow_man": {
|
||||
"n": 6,
|
||||
"pinned": 92.4
|
||||
"pinned": 91.8
|
||||
},
|
||||
"the_tenant": {
|
||||
"n": 2,
|
||||
@@ -106,57 +106,57 @@
|
||||
},
|
||||
"the_choir": {
|
||||
"n": 6,
|
||||
"pinned": 99.1
|
||||
"pinned": 99.4
|
||||
},
|
||||
"precedent": {
|
||||
"n": 3,
|
||||
"spread": 59.2,
|
||||
"focus": 71.1,
|
||||
"gain": 11.9,
|
||||
"spread": 61,
|
||||
"focus": 72.2,
|
||||
"gain": 11.2,
|
||||
"noise": 0.7
|
||||
},
|
||||
"carrion_file": {
|
||||
"n": 4,
|
||||
"spread": 45.8,
|
||||
"focus": 58.4,
|
||||
"gain": 12.7,
|
||||
"noise": 3.6
|
||||
"spread": 45.2,
|
||||
"focus": 57.3,
|
||||
"gain": 12.1,
|
||||
"noise": 1.3
|
||||
},
|
||||
"the_understudy": {
|
||||
"n": 5,
|
||||
"spread": 60.2,
|
||||
"focus": 71.8,
|
||||
"gain": 11.7,
|
||||
"noise": 0.8
|
||||
"spread": 61.5,
|
||||
"focus": 73,
|
||||
"gain": 11.5,
|
||||
"noise": 6.3
|
||||
},
|
||||
"surveyor": {
|
||||
"n": 2,
|
||||
"spread": 27.5,
|
||||
"focus": 34.5,
|
||||
"gain": 7,
|
||||
"noise": 3.7
|
||||
"spread": 28.8,
|
||||
"focus": 34.4,
|
||||
"gain": 5.6,
|
||||
"noise": 1.9
|
||||
},
|
||||
"quarantine_unit": {
|
||||
"n": 2,
|
||||
"pinned": 0.1
|
||||
"pinned": 0.2
|
||||
},
|
||||
"cuckoo_array": {
|
||||
"n": 2,
|
||||
"spread": 58.3,
|
||||
"spread": 58.6,
|
||||
"focus": 66.2,
|
||||
"gain": 7.9,
|
||||
"noise": 2.1
|
||||
"gain": 7.7,
|
||||
"noise": 3.8
|
||||
},
|
||||
"long_walker": {
|
||||
"n": 2,
|
||||
"pinned": 0.6
|
||||
"pinned": 0.1
|
||||
},
|
||||
"auditor": {
|
||||
"n": 4,
|
||||
"spread": 52,
|
||||
"focus": 64.5,
|
||||
"gain": 12.5,
|
||||
"noise": 1.6
|
||||
"spread": 49.4,
|
||||
"focus": 62.3,
|
||||
"gain": 12.9,
|
||||
"noise": 2.5
|
||||
},
|
||||
"washer_ford": {
|
||||
"n": 2,
|
||||
@@ -167,37 +167,37 @@
|
||||
"pinned": 100
|
||||
},
|
||||
"grindylow": {
|
||||
"n": 3,
|
||||
"spread": 70.6,
|
||||
"focus": 80.1,
|
||||
"gain": 9.4,
|
||||
"noise": 2.9
|
||||
"n": 4,
|
||||
"spread": 30.5,
|
||||
"focus": 42.1,
|
||||
"gain": 11.6,
|
||||
"noise": 1.5
|
||||
},
|
||||
"knockers": {
|
||||
"n": 2,
|
||||
"pinned": 100
|
||||
"n": 6,
|
||||
"pinned": 99.7
|
||||
},
|
||||
"spriggan": {
|
||||
"n": 3,
|
||||
"spread": 38.9,
|
||||
"focus": 55.5,
|
||||
"gain": 16.6,
|
||||
"noise": 3.4
|
||||
"spread": 42.5,
|
||||
"focus": 55.4,
|
||||
"gain": 12.9,
|
||||
"noise": 5.7
|
||||
},
|
||||
"nuckelavee": {
|
||||
"n": 2,
|
||||
"pinned": 8.5
|
||||
"pinned": 8.2
|
||||
},
|
||||
"hedley_kow": {
|
||||
"n": 6,
|
||||
"pinned": 97.6
|
||||
"pinned": 97.8
|
||||
},
|
||||
"apple_man": {
|
||||
"n": 2,
|
||||
"spread": 36.8,
|
||||
"focus": 42.5,
|
||||
"gain": 5.7,
|
||||
"noise": 7.2
|
||||
"spread": 36.6,
|
||||
"focus": 42.1,
|
||||
"gain": 5.5,
|
||||
"noise": 0.8
|
||||
},
|
||||
"the_committee": {
|
||||
"n": 2,
|
||||
@@ -205,24 +205,24 @@
|
||||
},
|
||||
"night_registry": {
|
||||
"n": 4,
|
||||
"spread": 62.1,
|
||||
"focus": 76.2,
|
||||
"gain": 14,
|
||||
"spread": 63.1,
|
||||
"focus": 73.9,
|
||||
"gain": 10.7,
|
||||
"noise": 2.5
|
||||
},
|
||||
"the_locum": {
|
||||
"n": 5,
|
||||
"spread": 49.3,
|
||||
"focus": 61.5,
|
||||
"gain": 12.2,
|
||||
"noise": 2.4
|
||||
"spread": 49.2,
|
||||
"focus": 63.1,
|
||||
"gain": 13.9,
|
||||
"noise": 1.9
|
||||
},
|
||||
"lost_property": {
|
||||
"n": 2,
|
||||
"spread": 54.9,
|
||||
"focus": 64.1,
|
||||
"gain": 9.2,
|
||||
"noise": 2.1
|
||||
"spread": 54.3,
|
||||
"focus": 63.2,
|
||||
"gain": 8.9,
|
||||
"noise": 1.3
|
||||
},
|
||||
"the_minutes": {
|
||||
"n": 2,
|
||||
@@ -230,71 +230,74 @@
|
||||
},
|
||||
"waiting_room": {
|
||||
"n": 3,
|
||||
"spread": 63.2,
|
||||
"focus": 71.8,
|
||||
"gain": 8.6,
|
||||
"noise": 2.1
|
||||
"spread": 63.4,
|
||||
"focus": 70.8,
|
||||
"gain": 7.4,
|
||||
"noise": 0.3
|
||||
},
|
||||
"the_predecessor": {
|
||||
"n": 4,
|
||||
"spread": 55.5,
|
||||
"focus": 69.2,
|
||||
"gain": 13.7,
|
||||
"noise": 6.7
|
||||
"spread": 58.6,
|
||||
"focus": 71,
|
||||
"gain": 12.5,
|
||||
"noise": 5.1
|
||||
},
|
||||
"switchboard": {
|
||||
"n": 6,
|
||||
"pinned": 86.1
|
||||
"pinned": 86.9
|
||||
},
|
||||
"the_census": {
|
||||
"n": 3,
|
||||
"spread": 64.3,
|
||||
"focus": 75.3,
|
||||
"gain": 11,
|
||||
"noise": 4.2
|
||||
"focus": 76.5,
|
||||
"gain": 12.1,
|
||||
"noise": 1
|
||||
},
|
||||
"the_courier": {
|
||||
"n": 2,
|
||||
"spread": 17.5,
|
||||
"focus": 25.2,
|
||||
"gain": 7.7,
|
||||
"noise": 4
|
||||
"spread": 17.8,
|
||||
"focus": 25,
|
||||
"gain": 7.2,
|
||||
"noise": 1.8
|
||||
},
|
||||
"the_relay": {
|
||||
"n": 2,
|
||||
"spread": 52.6,
|
||||
"focus": 58,
|
||||
"gain": 5.4,
|
||||
"noise": 6.7
|
||||
"spread": 52.7,
|
||||
"focus": 58.9,
|
||||
"gain": 6.2,
|
||||
"noise": 2.8
|
||||
},
|
||||
"the_stanchion": {
|
||||
"n": 2,
|
||||
"pinned": 4
|
||||
"pinned": 5.8
|
||||
},
|
||||
"the_yield": {
|
||||
"n": 2,
|
||||
"spread": 48.7,
|
||||
"focus": 62,
|
||||
"gain": 13.3,
|
||||
"noise": 3.8
|
||||
"spread": 46.8,
|
||||
"focus": 57.8,
|
||||
"gain": 11,
|
||||
"noise": 1.5
|
||||
},
|
||||
"the_overwriter": {
|
||||
"n": 3,
|
||||
"spread": 62.6,
|
||||
"focus": 71.7,
|
||||
"gain": 9.2,
|
||||
"noise": 3.9
|
||||
"spread": 64.1,
|
||||
"focus": 72.7,
|
||||
"gain": 8.6,
|
||||
"noise": 1.9
|
||||
},
|
||||
"the_arrears": {
|
||||
"n": 2,
|
||||
"pinned": 14.3
|
||||
"spread": 14.7,
|
||||
"focus": 21,
|
||||
"gain": 6.3,
|
||||
"noise": 1.5
|
||||
},
|
||||
"the_margin": {
|
||||
"n": 5,
|
||||
"spread": 46.7,
|
||||
"focus": 59.6,
|
||||
"gain": 12.8,
|
||||
"noise": 6.2
|
||||
"spread": 45.3,
|
||||
"focus": 59.3,
|
||||
"gain": 14.1,
|
||||
"noise": 2.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { NPCS } from "./content.mjs";
|
||||
import { ROSTER } from "./roster.mjs";
|
||||
import { runFight } from "./simulate.mjs";
|
||||
import { runFight, makeRng } from "./simulate.mjs";
|
||||
|
||||
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), "..");
|
||||
const base = JSON.parse(readFileSync(path.join(ROOT, "tools", "lethality-baseline.json"), "utf8"));
|
||||
@@ -59,8 +59,9 @@ const rec = focus.creatures[KEY];
|
||||
defect this file exists to help find. Solo also matches what check-lethality records. */
|
||||
const N = Number(opt("--count", 0)) || (rec?.gain !== undefined ? rec.n : 1);
|
||||
|
||||
/* The same generator shape the guards use, so a seed here names the same fight there. */
|
||||
const mk = seed => { let s = seed >>> 0; return () => ((s = (1664525 * s + 1013904223) >>> 0) / 4294967296); };
|
||||
/* THE generator the guards use, not merely one of the same shape. A seed here names the
|
||||
first fight of `simulate.mjs --seed <n>` exactly. */
|
||||
const mk = makeRng;
|
||||
const firstName = c => (c.name ?? "?").split(" ")[0];
|
||||
const hp = c => `${Math.max(0, c.hp)}/${c.hpMax}`;
|
||||
|
||||
@@ -116,7 +117,7 @@ for (const mode of MODES) {
|
||||
: " — party picking targets at random";
|
||||
console.log(`\n${"=".repeat(78)}\n${N} × ${spec.name.toUpperCase()}${how}\n${"=".repeat(78)}`);
|
||||
console.log(lines.join("\n"));
|
||||
console.log(`\n → ${r.won ? "PARTY WINS" : r.wiped ? "PARTY WIPED" : "neither side finished it"}`
|
||||
console.log(`\n → [${N > 1 ? mode : "solo"}] ${r.won ? "PARTY WINS" : r.wiped ? "PARTY WIPED" : "neither side finished it"}`
|
||||
+ ` in ${r.rounds} rounds · ${r.partyDown} of ${party.length} down · ${r.dead} dead`);
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -98,7 +98,11 @@ export function seedFor(base, key = "") {
|
||||
}
|
||||
|
||||
/** mulberry32 — small, fast, and seedable, which is the only property that matters. */
|
||||
function makeRng(seed) {
|
||||
/* R-265. Exported because anything that cites a measured fight has to draw from the same
|
||||
stream to name the same fight. tools/playthrough.mjs had its own LCG, so "seed 2" meant
|
||||
one fight there and a different one here, which quietly broke the only thing that tool
|
||||
exists for. */
|
||||
export function makeRng(seed) {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a = (a + 0x6D2B79F5) >>> 0;
|
||||
|
||||
Reference in New Issue
Block a user