diff --git a/README.md b/README.md index d7d1873..dc5b8e6 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ npm run serve # then open http://localhost:8080 The game plays from the keyboard: Enter takes the focused option, Enter again moves to the next day, and Tab / Shift+Tab pick a different option. The top of -every screen names the game, the pack, your current title, and the version and -commit you are running. +every screen names the game, the pack, your current title, and the build — +`0.3.0 · dev` when running from source, and the commit it came from when built. A server is needed during development because browsers refuse ES module imports over `file://`. If port 8080 is taken, run `python3 -m http.server ` @@ -88,6 +88,29 @@ real person did, including anything they typed into the feedback box. There are no runtime dependencies and no build step for development. Node is used only to run the tests, the single-file build, and the log analyser. +## Where things stand + +The MVP is complete and has survived three playtests. Two tiers, 31 events, a +retrospective, saves, and a feedback log that has already found one real bug. + +Open questions, in the order they are likely to matter: + +- **Does money stop mattering late?** A careful player ends around $8,300 over + 200 turns. Tuning has been corrected twice from playtest logs and should be + judged from a session, not from simulated play — that is how the last trap got + in. +- **`skills.negotiation >= 15` gates four different early options.** The logs + show players bouncing off it repeatedly in the first thirty turns. It is + probably set too high for how slowly the skill grows. +- **Nobody uses the feedback widget.** Zero ratings across three sessions. The + locked-gate data in the log has been more useful than anything typed; the + widget may need to be more present, or may not need to exist. +- **Dispatch is eleven events against the mailroom's nineteen.** It passes the + variety guard, but it has had far less play than the mailroom. +- **The third tier, or the second content pack.** Promotion working entirely as + data is decent evidence the engine/content split holds; a second *setting* + would be the proof. + ## Layout ``` diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index a8220a9..e0997cc 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -320,15 +320,22 @@ Playtest: *"need entry on the main screen showing what version we're running… so it's easy to tell when new stuff is."* A status line at the top of every screen names the game, the pack, the player's -current title, and the version plus the commit it was built from. `tools/stamp.js` -writes the commit into `src/build-info.js` before serving and before building, -so a distributed `dist/theladder.html` states exactly which commit produced it, -and a dev session says so too. A working tree with uncommitted changes gets a -`+` suffix — "4cab9fc+" is honest in a way that a bare SHA would not be. +current title, and the build. -The file is tracked rather than generated-and-ignored so a fresh clone runs -without a build step first. It changes at most once per commit, and the stamper -ignores a same-commit restamp so a running server does not churn the tree. +`src/build-info.js` is tracked and permanently reads `commit: 'dev'`; it is +never rewritten. `tools/build.js` substitutes the real commit into the *bundled +output* only, so `dist/theladder.html` states exactly which commit produced it +while the working tree never churns. A build from a dirty tree gets a `+` +suffix — "5c1784d+" is honest in a way a bare SHA would not be, and the build +fails loudly if the substitution finds nothing to replace. + +Running from source therefore reads "0.3.0 · dev", which is what it is. + +The first attempt stamped the tracked file in place before every serve and +build. It was wrong in a way worth recording: after each commit the committed +stamp named the *previous* commit, and the next serve rewrote it and dirtied the +tree. A generated value does not belong in a tracked file if anything routinely +regenerates it. ## 26. The whole game is playable from the keyboard diff --git a/package.json b/package.json index 4c81a31..3bcbd00 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,8 @@ "type": "module", "scripts": { "test": "node --test \"test/**/*.test.js\"", - "stamp": "node tools/stamp.js", - "serve": "node tools/stamp.js && python3 -m http.server 8080", - "build": "node tools/stamp.js && node tools/build.js", + "serve": "python3 -m http.server 8080", + "build": "node tools/build.js", "analyze": "node tools/analyze-log.js" } } diff --git a/src/build-info.js b/src/build-info.js index 1fc3b07..62c96e3 100644 --- a/src/build-info.js +++ b/src/build-info.js @@ -1,7 +1,10 @@ -// Stamped by tools/stamp.js, which runs before `npm run serve` and -// `npm run build`. The commit is whatever HEAD was at stamp time, so a dist -// file says exactly which commit produced it. +// The build identity shown in the status bar. +// +// This file is never rewritten. Running from source is honestly "dev"; +// tools/build.js substitutes the real commit into the bundled output only, so +// dist/theladder.html names the commit that produced it and the working tree +// never churns. export const BUILD = { - commit: '76072ed+', - builtAt: '2026-09-10', + commit: 'dev', + builtAt: null, }; diff --git a/tools/build.js b/tools/build.js index 9e8bd9e..d7ec36d 100644 --- a/tools/build.js +++ b/tools/build.js @@ -10,6 +10,7 @@ // a silent mis-bundle would be far worse than a failed build. import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; import { dirname, resolve, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -89,6 +90,23 @@ async function collect(entryPath, seen = new Map()) { return seen; } +/** + * The commit this build came from, marked `+` when the tree had uncommitted + * changes. Substituted into the output only — src/build-info.js stays as it is + * in the repository, so building never dirties the working tree. + */ +function buildIdentity() { + try { + const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, stdio: 'pipe' }) + .toString().trim(); + const dirty = execFileSync('git', ['status', '--porcelain'], { cwd: root, stdio: 'pipe' }) + .toString().trim().length > 0; + return dirty ? `${sha}+` : sha; + } catch { + return 'unknown'; + } +} + async function build() { const html = await readFile(entryHtml, 'utf8'); @@ -130,15 +148,21 @@ async function build() { '})();', ].join('\n'); + const commit = buildIdentity(); + const stamped = script + .replace("commit: 'dev'", `commit: '${commit}'`) + .replace('builtAt: null', `builtAt: '${new Date().toISOString().slice(0, 10)}'`); + if (stamped === script) throw new Error('could not stamp the build identity into the bundle'); + const output = html .replace(styleMatch[0], ``) - .replace(scriptMatch[0], ``); + .replace(scriptMatch[0], ``); await mkdir(outDir, { recursive: true }); await writeFile(outFile, output, 'utf8'); const kb = (Buffer.byteLength(output) / 1024).toFixed(1); - console.log(`built ${id(outFile)} — ${modules.size} modules, ${kb} kB`); + console.log(`built ${id(outFile)} — ${modules.size} modules, ${kb} kB, commit ${commit}`); } build().catch((error) => { diff --git a/tools/stamp.js b/tools/stamp.js deleted file mode 100644 index 4338cc0..0000000 --- a/tools/stamp.js +++ /dev/null @@ -1,44 +0,0 @@ -// Write the current commit into src/build-info.js. -// -// Runs before serving and before building, so what is on screen always names -// the commit it came from. The file is tracked rather than generated-and- -// ignored so that a fresh clone works without running anything first; it -// changes at most once per commit, and only when the value actually differs. - -import { execFileSync } from 'node:child_process'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const target = resolve(root, 'src/build-info.js'); - -function commit() { - try { - const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, stdio: 'pipe' }) - .toString().trim(); - const dirty = execFileSync('git', ['status', '--porcelain'], { cwd: root, stdio: 'pipe' }) - .toString().trim().length > 0; - return dirty ? `${sha}+` : sha; - } catch { - return 'dev'; - } -} - -const next = `// Stamped by tools/stamp.js, which runs before \`npm run serve\` and -// \`npm run build\`. The commit is whatever HEAD was at stamp time, so a dist -// file says exactly which commit produced it. -export const BUILD = { - commit: '${commit()}', - builtAt: '${new Date().toISOString().slice(0, 10)}', -}; -`; - -const current = readFileSync(target, 'utf8'); -// Only the date differs on a same-commit restamp; leave the file alone then, -// so a working tree does not churn every time the server starts. -const strip = (text) => text.replace(/builtAt: '[^']*'/, ''); -if (strip(current) !== strip(next)) { - writeFileSync(target, next, 'utf8'); - console.log(`stamped build ${commit()}`); -}