Add The Frontier, a second setting, and a picker to choose between them

Cold Fork is a freight yard: 29 events across two stages, a four-day cycle, and
its own relationship and skill vocabulary. src/engine/ was not touched. Adding it
needed three changes outside the engine: one save slot per pack, app.js holding
the current pack, and turn.js no longer naming the mailroom (DECISIONS #34).
build.js now handles imports wrapped across lines. A conformance suite runs over
every registered pack. DECISIONS #35 records three calls on open questions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxdJFbLq1rBKsUeticGV1g
This commit is contained in:
JesseMarkowitz
2026-09-12 21:03:50 -04:00
co-authored by Claude Opus 5
parent 4957c24e31
commit 7f20be83e8
25 changed files with 3015 additions and 300 deletions
+30 -11
View File
@@ -210,19 +210,38 @@ async function main() {
return;
}
const runs = byRun(entries);
console.log(`${entries.length} turns logged across ${runs.size} run${runs.size === 1 ? '' : 's'} ` +
`(pack ${entries[0].pack_id} ${entries[0].pack_version ?? ''})`.trimEnd());
for (const [seed, run] of runs) {
const stages = [...new Set(run.map((e) => e.stage))];
console.log(` seed ${seed}: ${run.length} turns, ${stages.join(' → ')}`);
// One log can hold turns from more than one setting, and pooling them would
// put a mailroom and a freight yard in the same repetition table. The same
// rule as the run boundary in DECISIONS #30, a level up: every statistic
// here is per pack, and a log is not one sequence.
const byPack = new Map();
for (const entry of entries) {
const id = entry.pack_id ?? 'unknown';
if (!byPack.has(id)) byPack.set(id, []);
byPack.get(id).push(entry);
}
reportRepetition(runs);
reportChoices(entries);
reportGates(entries);
reportFeedback(entries);
console.log('');
if (byPack.size > 1) {
console.log(`${entries.length} turns logged across ${byPack.size} settings.`);
}
for (const [packId, packEntries] of byPack) {
const runs = byRun(packEntries);
console.log(
`${packEntries.length} turns logged across ${runs.size} run${runs.size === 1 ? '' : 's'} ` +
`(pack ${packId} ${packEntries[0].pack_version ?? ''})`.trimEnd(),
);
for (const [seed, run] of runs) {
const stages = [...new Set(run.map((e) => e.stage))];
console.log(` seed ${seed}: ${run.length} turns, ${stages.join(' → ')}`);
}
reportRepetition(runs);
reportChoices(packEntries);
reportGates(packEntries);
reportFeedback(packEntries);
console.log('');
}
}
main().catch((error) => {
+5 -1
View File
@@ -79,7 +79,11 @@ async function collect(entryPath, seen = new Map()) {
const source = await readFile(entryPath, 'utf8');
seen.set(entryPath, null); // reserve the slot before recursing
const specifiers = [...source.matchAll(/^[ \t]*import\s.*?from\s*['"]([^'"]+)['"];?[ \t]*$/gm)]
// `[\s\S]*?` rather than `.*?`: a destructured import may be wrapped across
// several lines, and `transform` below already rewrites those — a scan that
// missed them produced a bundle with a __require for a module that was never
// collected, which only failed when the bundled game was run.
const specifiers = [...source.matchAll(/^[ \t]*import\s[\s\S]*?from\s*['"]([^'"]+)['"];?[ \t]*$/gm)]
.map((match) => match[1])
.filter((specifier) => specifier.startsWith('.'));