19 KiB
19 KiB
Changelog
All notable changes to History are listed here, newest first.
v0.13.0 — 2026-10-12 08:30
- Replaced the geometric sub-region grid with real named sub-national administrative regions (#16, PR 2). The choropleth source
public/regions.geo.jsonnow contains real named administrative regions — states, provinces, governorates, oblasts, … (Natural Earth admin-1, 1:10m) — instead of the abstract ~100–200 km geometric grid. Each region keeps a stableidand aname. The map's data contract is unchanged (properties.parent→ the parent country's ISO_A3,properties.name→ the region name), so no change to the map code or the LLM contract:WorldMap.tsx,App.tsx,shared/protocol.ts, andserver/map.tsall keep working as-is. - Generator rewritten —
scripts/generate-regions.mjs(same path, dependency-free,npm run generate:regions) now groups the admin-1 features by theiradm0_a3(the parent's ISO_A3), keeps only parents in the 177-country set, and id's each region with its ISO 3166-2 code when present (e.g.US-CA,FR-75,RU-MOW) else<ISO_A3>-<nn>; within a country, ids are disambiguated with a-2/-3/…suffix on collision (ISO 3166-2 codes are not always unique within a country — e.g. all nine of Bosnia's entities reportBA-BIH). A country with no admin-1 data would collapse to one whole-country region<ISO_A3>-01(0 are needed at 1:10m, which covers all 177). Coordinates are rounded to 4 decimal places, and the file is Douglas–Peucker-simplified only if it exceeds the ~8 MB guard. Output is sorted by (parent, id) and fully deterministic. - The dataset is 4,138 regions covering all 177 countries, 2,831 KB (2.76 MB) after Douglas–Peucker (the 4-dp base is ~23 MB). Per-country region count: min 1 / median 15 / max 232 (United Kingdom). There are 0 whole-country fallbacks — 1:10m admin-1 covers every one of the 177 (the five single-region countries are SOL, SAH, CYN, PRI, FLK). Samples:
US-CACalifornia andUS-NYNew York (US);FR-75Paris andFR-13Bouches-du-Rhône (France);RU-MOWMoskovskaya (Russia). - Validation test updated —
scripts/regions.test.tswas rewritten for the new dataset: well-formed features, globally unique ids, all 177 countries covered (≥1 region each), sane region areas (median ≥ 100 km², no non-Antarctica region ≥ 5,000,000 km² — Antarctica is exempt because its full-360° wrap ring breaks the planar area model), and each country's region area summing to within [40%, 175%] of its own area (median within [70%, 130%] — the band is wider than a naive [40%, 110%] because Natural Earth's admin-1 polygons are digitized independently of the admin-0 country polygons and are not clipped to them). Runs in O(total vertices). - Part of #16.
v0.12.0 — 2026-10-12 01:03
- Map now renders the sub-national regional dataset (#16, PR 2). The choropleth source switches from
public/countries.geo.json(177 countries) topublic/regions.geo.json(2,597 sub-regions): each country is subdivided into ~100–200 km regions, shown as a finer choropleth with thin internal border lines. The MapLibre source/layer ids are renamedcountries/countries-fill/countries-line→regions/regions-fill/regions-line, and the border line is thinned to0.75(from1.25) so the subdivision reads within each country. - Ownership stays at country granularity. A nation still owns whole countries, so every sub-region of a country shares that country's owning-nation color (the fill is "same colors as the country map, but subdivided"). The owner of a sub-region is resolved from the RegionMap via the feature's
properties.parent(the parent country's ISO_A3) — never the sub-region id (FRA-03) nor the repacked top-levelfeature.id. Clicking a sub-region switches the dossier to its parent country's owning nation (unchanged behavior); the hover tooltip now also shows the sub-region name + its parent country; a small "Selected …" label appears in the dossier after a map click. - New shared helper
mapFeatureOwner(regions, feature)in@shared/protocol— the single source of truth for resolving a rendered map feature's owner viaproperties.parent→RegionMap[parent](built on the existingregionOwnerkey lookup). The territory-change flash now expands a changed country to all of its sub-regions (via the newfeaturesByParents), so a whole-country transfer still pulses correctly at the finer resolution. - No ownership-model or LLM change —
buildRegionMap/RegionMapremain ISO_A3-keyed and the engine still references whole countries; this PR only changes what the map draws. Sub-region-level ownership (individual regions changing hands) is PR 3. - Tests — new
mapFeatureOwnerunit tests and an updated #6 regression guard pinning the sub-region feature shape (numericid,regionId= sub-region code,parent= ISO_A3) and proving resolution viaparentwhile the sub-region id and repacked id do not resolve (55 unit tests). The e2e suite needed no changes: it asserts on the legend (derived from the ISO_A3 RegionMap) and screen structure, not on map geometry. - Part of #16.
v0.11.0 — 2026-10-12 00:31
- Sub-national region dataset + generator (#16, foundation only — no app behavior change). A new dependency-free
scripts/generate-regions.mjs(run vianpm run generate:regions) splits each of the 177 countries inpublic/countries.geo.jsoninto adaptive ~100–200 km sub-national regions on a regular lat/lon grid clipped to the country (Sutherland–Hodgman), and writes the committedpublic/regions.geo.json(2,597 regions, ~755 KB). Each region is id'd<ISO_A3>-<nn>(nn = 1..k in row-major order, capped at 36 per country) withproperties = { name, parent }, so downstream work can map any region back to its parent nation. Antimeridian countries are handled three ways: small island groups (Fiji) stay a single whole-country region; large ones (Russia) are tiled in shifted [0,360) longitude space with output rings split at ±180°; and the full-360° Antarctic wrap ring becomes two hemispheric wedges. The output is fully deterministic — the same input yields a byte-identical file (verified by sha256). - Validation test — new self-contained
scripts/regions.test.ts(picked up bynpm test) reads the two GeoJSON files once each and asserts: the file is a well-formed FeatureCollection; every region is a valid feature whose id matches^[A-Z]{3}-\d+$and whoseparentis a real country id; every country has 1–36 regions (≥2 for any country larger than 50,000 km²); every region has a sane area (≥ 200 km², with a microstate exemption, and < 5,000,000 km²); and each country's regions sum to 45–108% of its own area (the measured range is ~[64%, 105%] — Antarctica's wedges under-fill, Norway's high-latitude fjord coast over-fills by 0.1%). No change to the running app, the LLM contract, or the map — this is purely the dataset + tooling that later PRs for #16 build on. - Fixes #16.
v0.10.0 — 2026-10-11 21:25
- Fix world map style (#2) — made the choropleth fill fully opaque (
fill-opacity0.85 → 1.0) and darkened the Natural Earth terrain raster (raster-opacity0.5 → 0.15), so the raster's flat water tint and fine 1:10m coastline no longer bleed through countries and compete with the borders. - Strengthened country border lines — border width 0.7 → 1.25, and the per-region stroke alphas raised (owned 0.10 → 0.30, unowned 0.06 → 0.14) so the vector borders read clearly as the dominant edge, with owned regions still more prominent than unowned ones.
- Fixes #2.
v0.9.0 — 2026-10-11 20:57
- Reduced turn latency (prompt-level): capped
recentEventsin the turn prompt from the last 10 to the last 5 events, and added a HARD CONSTRAINT asking the reasoning model to keep its internal analysis brief before emitting the JSON. This shortens the chain-of-thought (the 170–223s turn driver) and lowers the risk of exhausting the 4000-token output budget before the answer (the original 502 failure mode). Reasoning stays enabled (reasoning_effort: low); the deterministic stub, theTurnResultcontract,max_tokens, andreasoning_effortare all unchanged.
v0.8.0 — 2026-10-11 20:29
- Remove a saved game (#20) — every saved game now has a delete control: a trash button on its card in the home screen's "Saved games" section opens a confirmation dialog ("Delete this game?" showing the scenario/turn/date) with Cancel and a destructive Delete. The new
DELETE /api/games/:idroute removes the game's row from SQLite and returns{ deleted: true }(404 if the id is unknown). - Redirect on a missing/deleted game (#20) — loading a non-existent game id no longer leaves you stranded. A deep link to a missing game already fell back to home, and now a game that disappears while its tab is open (deleted from another tab or the same session) is pushed home too: the server broadcasts an SSE
errorevent on delete, and the client'suseSsegains anonErrorhandler that returns the tab to the home screen (matching the existing "game not found" connect behavior). PresetPickergained anonDeleteprop, a per-carddata-game-idattribute, the delete button, and the confirm dialog;api.deleteGame(id),Db.delete(id), and the sharedDeleteGameResponsetype were added to support it.- Unit + e2e tests — new
Db.deletetest (removes by id; reports absence for unknown / already-gone ids) and two e2e specs: (a) deleting a saved game from the home screen (open → cancel leaves it; confirm removes the card, the response is{deleted:true}, and a follow-upGETis 404), and (b) deleting an open game (the live tab is pushed home via the SSE error, and a fresh deep-link to the deleted id lands on home).
v0.7.0 — 2026-10-11 19:17
- Clicking a map region now switches the selected dossier to that region's owning nation instead of opening a popup (#7). The closeable click popup is gone; a hover tooltip (owner + key stats) is kept. The manual "Inspect" selector still works and stays in sync — both it and a map click drive the same dossier target.
- Fixed: a clicked owned region no longer reports "no owner" (#6). Root cause: MapLibre repacks a GeoJSON source's top-level
id(the ISO_A3 string) into a numeric index when it stores the source, so the feature a click/hover handler receives has a number forid— the oldregions[f.id]lookup resolved to nothing, and the popup read "No owner" even for owned regions (the choropleth fill still worked because it is stamped from the originalidbeforesetData). Fix:applyRegionsnow stamps the stable ISO_A3 code intoproperties.regionId(andownerId), which survives into the rendered features, and both the click and hover handlers resolve the owning nation from that code via the authoritative RegionMap — never from the repackedfeature.id. - Unowned regions (no entry in the RegionMap) do not change the dossier when clicked — the current target is left in place (a deliberate, non-crashing choice rather than a special "unowned" dossier state).
- New shared helper
regionOwner(regions, isoA3)in@shared/protocol(the pure, single source of truth for "ISO_A3 → owner record"). - Unit tests — new
regionOwnertests (owned → owner record, unowned → undefined, empty/missing → undefined) plus a #6 regression guard that pins the exact MapLibre-repacked feature shape (numericid, ISO_A3 inproperties.regionId) and asserts resolution viaproperties.regionIdworks while the oldfeature.idpath fails (44 tests, up from 39).
v0.6.0 — 2026-10-11 18:27
- Advisor fetches now show a "thinking" indicator (mirroring the turn's "Simulating…" hint) while the model responds, and the Ask-advisor button disables during the fetch; the top-bar status shows "Advisor thinking…" in the meantime. Fixes #15.
v0.5.0 — 2026-10-11 17:36
- HUD revamp — removed the "Map" window (the full-screen map is directly interactive via scroll/trackpad; no floating zoom control, and the now-dead
MapApi/onMapReadymap plumbing is removed fromWorldMap); the top bar is now a fixed, non-movable element pinned to top-center a few px from the top (a newpinnedmode onFloatingWindow— viewport-centered, no drag, no persisted position, always above other windows); and the dossier / event-feed / action panels can each be docked into a left or right full-height pane (a newdock/onDockChangemode onFloatingWindow, with small left/right/float controls in each title bar, choice persisted inlocalStorageand reproduced on reload; the default "float" reproduces the current first-run layout exactly). The legend stays a small floating widget. Fixes #4, Fixes #5. FloatingWindow— new optional props:pinned(top-center fixed bar),dock('left'/'right'full-height columns) andonDockChange(renders the dock controls and reports the mode), plusdataTestId(adata-testidon the window root for e2e).anchoris now optional (defaults totop-center).- New
data-testids —top-bar(pinned header),panel-dossier/panel-feed/panel-action(per-window roots), anddock-left/dock-right/dock-float(per-window dock controls). - New localStorage key —
history.floatwin.v1.dock(per-id dock mode); mirrors the existinghistory.floatwin.v1.collapsed. - E2E — new "HUD revamp" suite: asserts the Map window and its zoom buttons are gone, the top bar is pinned and horizontally centered a few px from the top (and doesn't move under a drag attempt), the dossier docks to the left edge (full-height) and the feed to the right edge, and a dock choice survives a page reload — all deterministic under
LLM_STUB=1(11 tests, up from 9).
v0.4.0 — 2026-10-11 16:02
- Fix: queued items disappearing from the pending list (#10). Root cause: SSE
stateevents carried no freshness information, and the client'sonState/onTurnhandlers applied any incoming snapshot unconditionally. A stale snapshot — e.g. the SSE on-connect resync taken just before an add was saved, or an EventSource auto-reconnect — could arrive after the add's REST response and clobber it, so "add one: nothing happens, add another: both appear". Fix: every mutation now bumps a monotonicGameState.version(persisted in SQLite, started at 1 on create, bumped byqueueRoundItem, the newremoveRoundItem, andapplyTurnResult; legacy games without the field are treated as version 0), and the client routes every state ingestion (SSE events, add/remove/jump responses, game loads) through a single "never go backwards" rule: a different game id is always adopted, the same game only when the incoming version is not older than the current one (compared inside the functionalsetGameupdater, so it is stale-closure-proof). - New: remove button for queued items (#11). Each pending-queue row in "Your move" has a small trash-icon button that opens a shadcn
Dialogconfirmation (item quoted, Cancel / destructive Remove, dark command-center styling). Confirming calls the newPOST /api/games/:id/queue/remove(body{ index }) — same fast save → broadcast → respond order as the add routes, no LLM, no turn advance; invalid indices get a 400. The clientapi.removeQueueItem(id, index)is version-gated like every other state update, so the list can never regress after a removal. - Unit tests — new
removeRoundItemtests (valid removal + version bump, out-of-bounds/non-integer →null), version-bump assertions forqueueRoundItem/applyTurnResult/stub turn, a legacy-no-version case, and version round-trip indb.test.ts(39 tests, up from 35). - E2E — new "queue add is never clobbered by a stale SSE snapshot" test (one add appears immediately, still there after a 1s grace period for late SSE events, version strictly increases across adds, queue survives a full page reload) and a "remove with confirmation" test (cancel keeps the item, confirm removes it, no turn advance, version persisted) — all deterministic under
LLM_STUB=1.
v0.3.0 — 2026-10-11 15:29
- Deep-linking — the active game's id now lives in the URL (
#/game/<id>), so a link opens that exact game and two tabs can run two different games; a bad or missing id falls back to the home screen. Fixes #3.
v0.2.0 — 2026-10-11 14:24
- Batched round model — replaced the single-action/turn with a queued-round model. Actions and diplomatic messages accumulate in
game.pending(no LLM call, no turn advance); a single "Jump forward" button commits the whole batch in one LLM turn. This fixes the 502 reliability problem by making each LLM call do one coherent batch of work instead of one tiny fragment. - New routes —
POST /api/games/:id/actionandPOST /api/games/:id/diplomacy(fast, no LLM);POST /api/games/:id/jump(the single LLM call);POST /api/games/:id/turnremoved. - New client API functions —
api.addAction(id, text),api.addDiplomacy(id, to, message),api.jump(id);api.turn(...)removed. - ActionPanel rewrite — Action tab: textarea + small "Add action" button. Diplomacy tab: target select + textarea + small "Add to round" button. Advisor tab: input + "Ask advisor" button. Below the tabs: a visible pending-queue list (⚡ for action, 💬 for diplomacy with target name, truncated text, count badge) and a large "Jump forward" primary button that shows the pending count and disables while a jump is in flight.
- App.tsx —
runAddAction/runAddDiplomacyhandlers (no busy state);runJump(with busy); the "Your move" window's title bar now shows a pending-count badge chip instead of the old jump button. - Deterministic stub updated —
deterministicTurnnow emits one event per queued item (action → military/success, diplomacy → diplomatic/partial) instead of one event per turn; still sets player military toclamp(cur+5)and allies with the first other nation. - Unit tests — rewrote
engine.test.tswith batch-aware tests (7 new tests, 35 total up from 28); updateddb.test.tsandmap.test.tsfixtures to includepending: []. - E2E — rewrote
e2e/sim.spec.tsto the new flow: queue action → assert pending list → queue diplomacy → assert pending list → jump → assert turn+1, date+2, military 80→85, two events, pending cleared, ally relation. - AGENTS.md — added Gitea-issue step to the change process; updated Layout and Conventions to reflect the batched-round model.
v0.1.0 — 2026-10-11 08:02
- Added a Gitea Actions CI pipeline (
.gitea/workflows/ci.yml:typecheck → lint → unit → build → deterministic Playwright e2e). E2e runs against the app's own dev webServer (LLM_STUB=1) — no dind/docker. - Committed
package-lock.jsonso CI uses a reproduciblenpm ci. - Switched to the mandatory PR-based workflow: feature branch → PR to
master→ CI green → auto-merge → tag + manual deploy (documented inAGENTS.md). - Versioned the repo at
0.1.0(package.json) and addedCHANGELOG.md,.env(gitignored) and.env.example. - This change is the first one landed through the new PR process (dogfooding it).