Files
TheLadder/tools/story-to-pack/recovered/probe.mjs
T
JesseMarkowitzandClaude Opus 5 fa3769d0fe Add story-to-pack research and a structured situation review
Research toward building a content pack from a story corpus, kept on its own
branch and independent of the game. Records the selection experiments against
blind labels, and settles selection as gate G2 followed by a human review:
review.py writes REVIEW.md and a review.json form, apply_review.py checks the
filled form and writes situations.json for the next stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6UDQ9o6L6Ey173U7XVou6
2026-09-15 06:59:35 -04:00

60 lines
2.8 KiB
JavaScript

// How hard is it to hit the conformance bar by picking numbers at random?
// Keep the Frontier's structure and prose; replace every effect magnitude and
// gate threshold with a random plausible value; see what survives.
import { frontier } from '../../../content/frontier/index.js';
import { validatePack } from '../../../src/engine/validate.js';
import { play, sweep, strategies } from '../../../test/fixtures/strategies.js';
const clone = (o) => JSON.parse(JSON.stringify(o));
const ri = (lo, hi) => lo + Math.floor(Math.random() * (hi - lo + 1));
function randomize(base, { keepSigns }) {
const pack = { ...base, events: clone(base.events) };
for (const e of pack.events) {
if (e.weight !== undefined && e.weight < 5000) e.weight = ri(20, 120);
if (e.cooldown !== undefined) e.cooldown = ri(2, 12);
for (const o of e.options ?? []) {
for (const r of o.requires ?? []) if (typeof r.value === 'number') r.value = ri(5, 60);
for (const f of o.effects ?? []) {
if (f.op !== 'add' || typeof f.value !== 'number') continue;
const mag = f.path === 'money' ? ri(1, 25) : ri(1, 12);
f.value = keepSigns ? Math.sign(f.value) * mag : (Math.random() < 0.5 ? -mag : mag);
}
}
}
return pack;
}
function score(pack) {
if (!validatePack(pack).valid) return { ok: false, why: 'invalid' };
try {
const { firedEvents, offeredOptions } = sweep(pack, { seeds: 4, turns: 200 });
const allE = pack.events.map((e) => e.id);
const allO = pack.events.flatMap((e) => e.options.map((o) => `${e.id}.${o.id}`));
if (allE.some((i) => !firedEvents.has(i))) return { ok: false, why: 'dead events' };
if (allO.some((i) => !offeredOptions.has(i))) return { ok: false, why: 'dead options' };
for (const s of Object.keys(strategies)) {
let red = 0, n = 0; const counts = new Map();
play(pack, { strategy: s, seed: 'probe', turns: 150, visit: ({ state, turn }) => {
n++; if (state.money < 0) red++;
counts.set(turn.event.id, (counts.get(turn.event.id) ?? 0) + 1);
}});
if (red / n >= 0.25) return { ok: false, why: 'stuck below zero' };
const top = Math.max(...counts.values()) / n;
if (top >= 0.25) return { ok: false, why: 'one situation dominates' };
}
return { ok: true };
} catch (err) { return { ok: false, why: 'threw: ' + err.message.slice(0, 40) }; }
}
for (const keepSigns of [true, false]) {
const tally = {}; let pass = 0; const N = 60;
for (let i = 0; i < N; i++) {
const r = score(randomize(frontier, { keepSigns }));
if (r.ok) pass++; else tally[r.why] = (tally[r.why] ?? 0) + 1;
}
console.log(`\nsigns ${keepSigns ? 'preserved' : 'randomised'}: ${pass}/${N} passed`);
for (const [why, n] of Object.entries(tally).sort((a,b)=>b[1]-a[1])) console.log(' ', n, why);
}