Dynamic scenario generation: 3 era templates + AI world-state + advisor checkpoint review (v0.31.0) #52

Closed
opened 2026-10-15 11:18:55 +02:00 by kamilkrzywda · 0 comments
Owner

Summary

Add a dynamic scenario generation path alongside the existing 5 static presets: the user picks one of 3 light era templates + a free-text description (+ a year), the LLM generates a draft world + an advisor checkpoint list, the user reviews/approves checkpoints and picks a nation to lead, and a second LLM call finalizes the world into a saved, playable GameState (a normal game at #/game/<id>). The 5 static presets stay untouched — this is purely additive.

1. Two LLM steps (both plain chat(), no tools, no advisor endpoint)

  • generate: chat() returns BOTH a draft WorldDraft and a list of string checkpoints. No separate advisor endpoint — the advisor is embodied in the checkpoint list.
  • finalize/commit: chat() returns the final world constrained to the approved checkpoints the user selected.

2. Three light era templates

presets/templates/ with an index.json manifest (mirrors presets/index.json + createPresetStore):

  • ww2 (1939) — WWII-era powers
  • present (2026) — the present day
  • near-future (~2035) — a plausible near future
    Each template is a small seed: { id, name, year, eraDescription, suggestedPowers: [{ name, region (ISO_A3), note }], tensions: [string] }. Keep them simple.

3. Shared types (dependency-free, shared/)

  • WorldDraft = { nations: WorldNation[]; events?: ScenarioEvent[] }
  • WorldNation = { id, name, type?, stats: NationStats, territory: string[] (ISO_A3), relations: Record<string, number> (-100..100) }
  • Checkpoint = string
  • Pure validateWorldDraft(raw, { validCountryCodes }): WorldDraft | null — parse/validate/clamp, dedup & assign ids, drop invalid country codes, null if unusable, never throws.
  • Pure worldDraftToGameState(draft, expand, playerNationId): GameState — region-key territory (via expandCountriesToKeys), regionStats seeded (via seedRegionStats), version:1, unowned:[], markers:[], pendingNationOps:[], pending:[], playerNationId, startDate from year, turn:1.

4. server/scenario.ts

  • buildGeneratePrompt, buildRegeneratePrompt
  • generateDraft({ templateId, description, year }) → { world: WorldDraft, checkpoints: string[] }
  • finalizeWorld({ templateId, description, year, approvedCheckpoints, playerNationId }) → final GameState (saved via db.save, returns new game id)
  • Both wired to llm.chat() with the existing injectable Transport.

5. server/stubScenario.ts — deterministic scenario stub (LLM_STUB=1)

A distinct deterministic generator stub for the scenario calls only. The turn server/stub.ts stays 100% unchanged. The scenario stub returns a fixed small world (3 nations) + a fixed checkpoint list, stable (same input → same output), with a trivial mapping so approved checkpoints affect the output.

6. API routes (server/index.ts, same /api prefix, same-origin)

  • GET /api/scenarios/templates → the 3 templates
  • POST /api/scenarios/draft body { templateId, description, year? } → { world: WorldDraft, checkpoints: string[] }
  • POST /api/scenarios/commit body { templateId, description, year?, approvedCheckpoints: string[], playerNationId } → { id }
    All existing routes untouched.

7. Client (Tailwind + shadcn/ui, dark slate/amber/cyan command-center)

  • src/components/ScenarioForm.tsx — 3 template cards (with default year), description textarea, year number input (pre-filled + editable), Generate → POST /draft.
  • src/components/CheckpointReview.tsx — draft world summary + checkpoint checklist (approve/remove; approved = bound), reuse NationSelect over the draft's nations, Finalize → POST /commit → navigate #/game/<id>.
  • src/components/PresetPicker.tsx — add a "Create scenario" entry.
  • src/App.tsx — create/review flow as client state (home → form → review → navigate); handle LLM errors (failed generate/commit → toast, stay on form/review).

Constraints

  • shared/ has no runtime deps; change it first.
  • Never import @turf/@types/geojson from .ts (geo stays in .mjs scripts).
  • server/stub.ts (turn stub) stays 100% unchanged; the new dynamic e2e is additive.
  • Bad LLM output is dropped/clamped, never thrown.
  • Client uses same-relative /api paths (no hardcoded origins).
  • Version bump to 0.31.0 in package.json; CHANGELOG.md entry.

Acceptance

  • Unit tests: validateWorldDraft (valid→clean, clamp, bad country codes dropped, garbage→null no throw, id dedup); worldDraftToGameState (territory expansion, regionStats seeded, version:1, unowned/markers empty, playerNationId set, startDate from year); the 3 templates load (manifest + each file valid); generateDraft/finalizeWorld with the scenario stub (deterministic, finalize respects approved checkpoints, returns a saved game); prompt builders include era + description + year + (country reference / approved checkpoints).
  • New e2e exercising the dynamic flow deterministically under LLM_STUB=1 (template → draft → review/approve → commit → game starts); existing e2e/sim.spec.ts stays unchanged and green.
  • npm test + typecheck + lint + build green; e2e verified by CI. Deploy (additive, no data wipe) and verify a live qwen3.8-27b generate + commit (no 502): sensible world + checkpoints from /draft, valid world from /commit + GET /api/games/<id>.
## Summary Add a **dynamic scenario generation** path alongside the existing 5 static presets: the user picks one of **3 light era templates** + a free-text **description** (+ a year), the LLM **generates** a draft world + an **advisor checkpoint list**, the user reviews/approves checkpoints and picks a nation to lead, and a second LLM call **finalizes** the world into a saved, playable `GameState` (a normal game at `#/game/<id>`). The 5 static presets stay untouched — this is purely additive. ### 1. Two LLM steps (both plain `chat()`, no tools, no advisor endpoint) - **generate**: `chat()` returns BOTH a draft `WorldDraft` **and** a list of `string` checkpoints. No separate advisor endpoint — the advisor is embodied in the checkpoint list. - **finalize/commit**: `chat()` returns the final world **constrained to the approved checkpoints** the user selected. ### 2. Three light era templates `presets/templates/` with an `index.json` manifest (mirrors `presets/index.json` + `createPresetStore`): - `ww2` (1939) — WWII-era powers - `present` (2026) — the present day - `near-future` (~2035) — a plausible near future Each template is a small seed: `{ id, name, year, eraDescription, suggestedPowers: [{ name, region (ISO_A3), note }], tensions: [string] }`. Keep them simple. ### 3. Shared types (dependency-free, `shared/`) - `WorldDraft = { nations: WorldNation[]; events?: ScenarioEvent[] }` - `WorldNation = { id, name, type?, stats: NationStats, territory: string[] (ISO_A3), relations: Record<string, number> (-100..100) }` - `Checkpoint = string` - Pure `validateWorldDraft(raw, { validCountryCodes }): WorldDraft | null` — parse/validate/clamp, dedup & assign ids, drop invalid country codes, null if unusable, **never throws**. - Pure `worldDraftToGameState(draft, expand, playerNationId): GameState` — region-key territory (via `expandCountriesToKeys`), `regionStats` seeded (via `seedRegionStats`), `version:1`, `unowned:[]`, `markers:[]`, `pendingNationOps:[]`, `pending:[]`, `playerNationId`, `startDate` from year, `turn:1`. ### 4. `server/scenario.ts` - `buildGeneratePrompt`, `buildRegeneratePrompt` - `generateDraft({ templateId, description, year })` → `{ world: WorldDraft, checkpoints: string[] }` - `finalizeWorld({ templateId, description, year, approvedCheckpoints, playerNationId })` → final `GameState` (saved via `db.save`, returns new game `id`) - Both wired to `llm.chat()` with the existing injectable `Transport`. ### 5. `server/stubScenario.ts` — deterministic scenario stub (`LLM_STUB=1`) A distinct deterministic generator stub for the scenario calls only. The turn `server/stub.ts` stays **100% unchanged**. The scenario stub returns a fixed small world (3 nations) + a fixed checkpoint list, stable (same input → same output), with a trivial mapping so approved checkpoints affect the output. ### 6. API routes (`server/index.ts`, same `/api` prefix, same-origin) - `GET /api/scenarios/templates` → the 3 templates - `POST /api/scenarios/draft` body `{ templateId, description, year? }` → `{ world: WorldDraft, checkpoints: string[] }` - `POST /api/scenarios/commit` body `{ templateId, description, year?, approvedCheckpoints: string[], playerNationId }` → `{ id }` All existing routes untouched. ### 7. Client (Tailwind + shadcn/ui, dark slate/amber/cyan command-center) - `src/components/ScenarioForm.tsx` — 3 template cards (with default year), description textarea, year number input (pre-filled + editable), Generate → `POST /draft`. - `src/components/CheckpointReview.tsx` — draft world summary + checkpoint checklist (approve/remove; approved = bound), reuse `NationSelect` over the draft's nations, Finalize → `POST /commit` → navigate `#/game/<id>`. - `src/components/PresetPicker.tsx` — add a "Create scenario" entry. - `src/App.tsx` — create/review flow as client state (home → form → review → navigate); handle LLM errors (failed generate/commit → toast, stay on form/review). ### Constraints - `shared/` has **no runtime deps**; change it first. - Never import `@turf`/`@types/geojson` from `.ts` (geo stays in `.mjs` scripts). - `server/stub.ts` (turn stub) stays **100% unchanged**; the new dynamic e2e is additive. - Bad LLM output is dropped/clamped, **never thrown**. - Client uses same-relative `/api` paths (no hardcoded origins). - Version bump to **0.31.0** in `package.json`; `CHANGELOG.md` entry. ### Acceptance - Unit tests: `validateWorldDraft` (valid→clean, clamp, bad country codes dropped, garbage→null no throw, id dedup); `worldDraftToGameState` (territory expansion, `regionStats` seeded, `version:1`, `unowned`/`markers` empty, `playerNationId` set, `startDate` from year); the 3 templates load (manifest + each file valid); `generateDraft`/`finalizeWorld` with the scenario stub (deterministic, finalize respects approved checkpoints, returns a saved game); prompt builders include era + description + year + (country reference / approved checkpoints). - New e2e exercising the dynamic flow deterministically under `LLM_STUB=1` (template → draft → review/approve → commit → game starts); existing `e2e/sim.spec.ts` stays unchanged and green. - `npm test` + `typecheck` + `lint` + `build` green; e2e verified by CI. Deploy (additive, no data wipe) and verify a **live** `qwen3.8-27b` generate + commit (no 502): sensible world + checkpoints from `/draft`, valid world from `/commit` + `GET /api/games/<id>`.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OpenCode/History#52