Files
JesseMarkowitzandClaude Opus 5 d84ae495f4 Keep money mattering at the top, and fix the instruments that hid it
Both settings now carry a charge levied by standing itself, which no purchase
ends. Corporate Ladder's steps on money (1200/-60, 2400/-120, 3600/-240): a flat
charge cannot flatten a slope, since late income scales with how well the player
plays, and the flat charge needed to stop an optimiser accumulating put ordinary
archetypes below zero for 44% of a run. Stepped, the balance settles around
$4,250 and ordinary play comes out better than before it existed. The Frontier's
is flat at -4 a run, because its late game never accumulated in the first place.

Found in the first two human sessions of this work:

- A career saved before a pack gained a stat had no path for it, and conditions
  compare numbers only, so every event, option and upkeep entry gated on a new
  stat silently did not exist for that run. Loading now fills those in.
- 'Wrap up my career' sat immediately after the last option in tab order, so one
  Tab too many and Enter read as the game ending at day 31. It goes last, and
  the retrospective says plainly that it is a summary.
- A 181-turn session ran against a stale bundle and the change under test never
  fired. Every logged turn now records app_version and build.
- src/version.js had reported 0.3.0 since 0.3.0, while playtesters were told to
  check the status bar. A test now holds it to package.json.
- 'Put money against the book' was shown locked to a player who owed nothing,
  six times. Options gated on a state the player cannot choose to enter are now
  hidden rather than greyed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxdJFbLq1rBKsUeticGV1g
2026-09-12 23:07:35 -04:00

75 lines
3.6 KiB
JavaScript

// The single-file build.
//
// A build that produces a file proves nothing; this one runs the bundled
// script against the fake DOM and plays a turn through it. That is the closest
// thing to opening dist/theladder.html that is available without a browser —
// it catches a broken bundle, which is otherwise only discovered by whoever
// was handed the file.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { installFakeDom, byClass, firstByClass } from './fixtures/fake-dom.js';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
test('the version the app reports is the version in package.json', async () => {
// It silently was not. The status bar read 0.3.0 through the whole of the
// 0.3.1 and 0.3.2 work, while playtesters were being told to check it to know
// which build they were on — a version string that lies is worse than none,
// because it is trusted.
const { APP } = await import('../src/version.js');
const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
assert.equal(APP.version, pkg.version,
'src/version.js and package.json disagree about what this is');
});
test('the bundle builds, and the bundled game boots and plays', () => {
execFileSync('node', ['tools/build.js'], { cwd: root, stdio: 'pipe' });
const html = readFileSync(resolve(root, 'dist/theladder.html'), 'utf8');
assert.doesNotMatch(html, /<link\s+rel="stylesheet"/, 'the stylesheet is inlined');
assert.doesNotMatch(html, /type="module"/, 'no module scripts survive; file:// would block them');
assert.doesNotMatch(html, /^[ \t]*import\s/m, 'no bare import statements remain');
assert.match(html, /--paper:/, 'the CSS made it in');
// A distributed file has to be able to say which commit made it.
assert.match(html, /commit: '[^']+'/, 'the build stamp is present');
assert.doesNotMatch(html, /commit: 'dev'/, 'a built file is never stamped "dev"');
const script = html.match(/<script>([\s\S]*?)<\/script>/)?.[1];
assert.ok(script, 'the bundled script is present');
const dom = installFakeDom();
try {
// eslint-disable-next-line no-new-func -- running the build output is the point
new Function(script)();
// Nothing is saved and nothing is remembered, so a build shipping more
// than one setting opens on the picker. Click into the first one and carry
// on — this exercises the front door in the real bundle, which is the one
// place the pack registry, the picker screen and the save slots all meet.
assert.ok(firstByClass(dom.root, 'screen--packs'), 'the bundled game rendered the picker');
assert.match(dom.root.textContent, /Corporate Ladder/, 'the packs are named');
assert.match(dom.root.textContent, /The Frontier/);
const start = byClass(dom.root, 'button').find((b) => /Start here|Keep playing/.test(b.textContent));
assert.ok(start, 'the picker offers a way in');
start.click();
assert.ok(firstByClass(dom.root, 'screen--turn'), 'the bundled game rendered a turn');
assert.match(dom.root.textContent, /Day 1/);
assert.match(dom.root.textContent, /Marlene Hobb/);
assert.match(dom.root.textContent, /The Ladder/, 'the status bar is in the bundle');
const playable = byClass(dom.root, 'option').filter((b) => !b.disabled);
assert.ok(playable.length > 0, 'the bundled game offers choices');
playable[0].click();
assert.ok(firstByClass(dom.root, 'outcome'), 'a turn can be played in the bundle');
} finally {
dom.restore();
}
});