Show the bot's reasoning and rejected options in the replay

This commit is contained in:
Jesse
2026-07-31 15:22:45 -04:00
parent d261ad7af8
commit 160190da3f
3 changed files with 328 additions and 49 deletions
+37 -2
View File
@@ -191,7 +191,7 @@ describe('replay HTML', () => {
});
it('offers the controls that make it usable', () => {
for (const id of ['play', 'back', 'fwd', 'scrub', 'stage', 'money', 'crash']) {
for (const id of ['play', 'back', 'fwd', 'scrub', 'stage', 'money', 'crash', 'waste', 'jam', 'decision']) {
assert.ok(html.includes(`id="${id}"`), `missing control: ${id}`);
}
});
@@ -207,7 +207,7 @@ describe('replay HTML', () => {
it('wires a handler to every control', () => {
const js = html.slice(html.lastIndexOf('<script>') + 8, html.lastIndexOf('</script>'));
for (const id of ['play', 'back', 'fwd', 'first', 'stage', 'money', 'crash']) {
for (const id of ['play', 'back', 'fwd', 'first', 'stage', 'money', 'crash', 'waste', 'jam']) {
assert.match(js, new RegExp(`\\$\\('${id}'\\)\\.onclick`), `no handler bound to ${id}`);
}
assert.match(js, /\$\('scrub'\)\.oninput/, 'no handler bound to scrub');
@@ -233,6 +233,41 @@ describe('replay HTML', () => {
assert.match(String(els.get('when')?.textContent ?? ''), /Day \d+/);
});
it('records what the bot chose, why, and what it passed over', () => {
// The replay could always show what happened, never what COULD have happened — so a daft move
// was visible but the alternatives it declined were not, which is what you need to say what it
// should have done instead.
const rec = record(202, 'standard');
const decided = rec.frames.filter((f) => f.decision !== null);
assert.ok(decided.length > 0, 'no frame carried a decision');
for (const f of decided) {
assert.ok(f.decision!.chose.length > 0, 'a decision with no chosen action');
assert.ok(f.decision!.why.length > 0, 'a decision with no stated reason');
assert.ok(f.decision!.totalOptions >= 1, 'a decision offering nothing');
}
// The reasons must come from the bot's own branches, not a generic fallback for everything.
const reasons = new Set(decided.map((f) => f.decision!.why));
assert.ok(reasons.size > 5, `only ${reasons.size} distinct reasons — the branches are not reporting`);
// Turns with real alternatives must show them, or the panel is decoration.
assert.ok(
decided.some((f) => f.decision!.rejected.length > 0),
'no frame ever recorded a rejected option',
);
});
it('flags jammed facilities and can tell them from ready ones', () => {
const rec = record(202, 'standard');
for (const f of rec.frames) {
for (const x of f.facilities) {
// A jam is precisely "work on WORK that cannot come off", so it can never be 'ready'.
assert.ok(!(x.jammed && x.canFinish), `${x.name} reported both jammed and ready`);
}
}
});
it('stays a sane size', () => {
const mb = Buffer.byteLength(html) / 1024 / 1024;
assert.ok(mb < 5, `replay is ${mb.toFixed(1)} MB`);