feat(#52): scenario generator LLM steps + deterministic stub

- server/scenario.ts: buildGeneratePrompt / buildRegeneratePrompt, defensive
  parseGenerate / parseRegenerate (fence-strip + balanced-object + validate),
  generateDraft (draft world + advisor checkpoints) and finalizeWorld (final
  world constrained to approved checkpoints -> playable GameState), all over
  the injectable LlmClient with a nudge re-send. SCENARIO_* markers for the
  stub.
- server/stubScenario.ts: deterministic scenario stub transport (fixed 3-nation
  world + checkpoint list; finalize refines stability per approved checkpoint)
  + makeScenarioStubLlmClient. Distinct from and never touching turn stub.ts.
- server/regions.ts: validCountryCodes() accessor (ISO_A3 parents) for draft
  territory validation.
- shared/scenario.ts: worldDraftToGameState now guards a dangling playerNationId
  (falls back to the first nation) so a finalize that drops it stays playable.
- server/scenario.test.ts: 16 tests (prompt builders, defensive parsing,
  generateDraft/finalizeWorld over a fake client + the stub).

Fixes #52
This commit is contained in:
2026-10-15 09:46:34 +00:00
parent 2191514db1
commit 68402694c5
5 changed files with 871 additions and 1 deletions
+12
View File
@@ -259,6 +259,18 @@ export function validKeys(): ReadonlySet<string> {
return data ? data.validKeys : new Set<string>();
}
/**
* The set of ISO_A3 country codes that own at least one sub-region in the
* manifest (the valid `territory` codes for a world draft, v0.31 #52). These
* are the keys of `keysByCountry` — i.e. the distinct `properties.parent`
* values in regions.geo.json. Empty when no region data is loaded; callers
* pass it (or `undefined` to skip filtering) to `validateWorldDraft` so a
* draft's territory is checked against real countries.
*/
export function validCountryCodes(): ReadonlySet<string> {
return data ? new Set(Object.keys(data.keysByCountry)) : new Set<string>();
}
/**
* True when the region-adjacency manifest loaded with at least one entry.
* Callers (the server's jump route) use this to decide whether to hand the