Move territory from whole-country (ISO_A3) to individual sub-region id
granularity (PR 3 of #16):
- new server/regions.ts (turf-free): loadRegions (boot), expandTerritory
(game creation), migrateTerritoryToRegions (legacy games on db.load),
regionNamesFor / regionLabel (LLM prompt), graceful ISO_A3 fallback
- game creation expands each preset nation's ISO_A3 list to its sub-region
ids; pre-v0.19 saved games migrate transparently on read (idempotent)
- LLM contract: territoryChanges gain/lose sub-region ids (one region at a
time; gain moves out of the prior owner; lost-without-gainer = unowned);
user prompt carries a top-level regions reference table
({id: 'name (parent)'} over the state's territory ids)
- WorldMap: choropleth fill per sub-region (countries can split between
nations), ownership via properties.regionId through the shared
mapFeatureOwner, flash lights exactly the changed features
(featuresByRegionIds)
- deterministic stub: one region-level move per non-empty round (player
annexes the lexicographically-first region of the first other nation)
- tests: new server/regions.test.ts (real geo + fallback + seams);
map/engine tests re-pinned to region ids; scripts/regions.test.ts stays green
Measured full wwii-1939 user prompt (411 regions): ~24.7k chars / ~6.2k
tokens — far under the 80k frontier limit, so the full regions table is
always sent (no frontier-limited mode needed).
Part of #16
- DELETE /api/games/:id route + Db.delete() (404 when absent)
- PresetPicker: per-saved-game trash button + confirm dialog (Cancel/Delete)
- App: runDeleteGame; useSse onError -> goHome on 'deleted'/'not found'
(a deleted/missing game now returns the live tab and any deep link home)
- DeleteGameResponse in shared/protocol.ts; api.deleteGame()
- unit test (Db.delete) + 2 e2e tests (home-screen delete; in-flight delete
returns the active tab home and a fresh deep link lands on home)
- version 0.7.0 -> 0.8.0 + changelog
Fixes#20
Map (basemap was rendering black):
- Inline MapLibre style (dark-navy background + subtle OpenFreeMap ne2sr
raster) instead of the remote "dark" style, whose vector tiles are empty at
world zoom and gated the choropleth on a remote styledata fetch.
- Explicit prod worker URL (maplibre-gl-worker via ?url) so GeoJSON sources
process in the production bundle.
- Hardened the choropleth against geo load-order races (empty sources up
front, ownership stamped once data lands) and bumped fill opacity so the
vivid per-nation colors are the star on the non-black background.
Save/resume (a refresh used to drop users to the home screen):
- Persist the active game id in localStorage (history.activeGameId) on create
and resume; clear it when explicitly starting a new game.
- Auto-resume the last game on a page refresh (GET /api/games/:id + /map),
re-attaching the SSE stream; fall back to home gracefully if it's gone.
- Home screen: prominent "Resume last game" button + a "Saved games" section
(newest first). New GET /api/games list endpoint.
e2e: two new deterministic tests — auto-resume-on-reload and explicit resume
from the home screen (both under LLM_STUB=1).
Quality gates: typecheck, lint, 21 unit tests, 3 e2e tests — all pass.
- MapLibre map is now a fixed, full-viewport background layer (z-0)
- New FloatingWindow: drag by title bar (pointer capture, viewport-clamped,
position persisted to localStorage), collapsible to title bar, bring-to-front
z-index; semi-transparent slate window with amber/cyan accents
- Game screen re-laid out as floating windows over the map: slim top header
(scenario, turn + date, player nation chip, Idle/Simulating status, Focus map
collapse-all), dossier (top-left), event feed (right), Your move deck
(bottom-center), legend (bottom-left), tiny zoom control (bottom-right)
- MapLibre default nav controls removed; custom floating zoom-in/out control
- Panels gain an inWindow mode (no card chrome, same testids)
- server: honor HISTORY_API_PORT as PORT fallback (dev proxy/e2e agreement)
- css: keep maplibre canvas base color and Radix portals above HUD windows
Map (WorldMap):
- Keep the near-black OpenFreeMap `dark` basemap (verified HTTP 200; the
suggested dark-matter / dark-outdoors / liberty-dark variants 404 on that
host) so the choropleth nation colors pop
- Stable per-nation fills at 0.8 opacity, thin subtle borders, and a brighter
thicker amber border for the player nation so "this is me" is always obvious
- Hover brightens a region and shows a color-chipped owner + key stats popup;
click opens a full dossier popup and focuses the left panel on that nation
- Transient, self-cleaning flash (pulsing amber outline) over regions whose
ownership changed this turn
- Bottom-left legend auto-generated from the region map; per-scenario initial
camera (Europe for the WWII scenarios, wider for Cold War / Modern / Aegis)
Command-center UI:
- New header bar: scenario title, prominent turn + date, the player nation
(name + color chip), and a live Idle / "Simulating…" status indicator
- 3-column operations-console grid: nation dossier (left) / dominant map
(center) / command deck + event feed (right); stacks map-first on mobile
- EventFeed: one card per event with a category icon, a colored outcome badge
(success/failure/partial/neutral), a nation-colored accent edge, and the
turn + date; new events slide in and flash at the top
- NationPanel: value-colored animated stat bars (red → green), territory as
color-coded region chips, and diplomacy as a list with a colored indicator
per relation (ally/rival/war/neutral)
- ActionPanel: a "Your move" composer with one prominent Send button and a
spinner + "turns can take a few seconds" hint while a turn resolves
- sonner error toasts: a 502 / model-down surfaces as "AI temporarily
unavailable — try again"
Live LLM rewire (server, previously uncommitted):
- Default endpoint https://ai.kkhost.pl/v1 (vLLM, no API key); reasoning model
qwen3.8-27b; always send a generous max_tokens (4000) so it finishes its
chain-of-thought before emitting content; read only `content`, ignore
`reasoning`. docker-compose + AGENTS.md updated to match.
Tests: the deterministic Playwright e2e gains assertions for the dossier stat
bars, the map legend (all nations), and the event-card type icon + outcome
badge; it stays fully deterministic under LLM_STUB=1.