The LLM can now place named point markers on the map each turn (issue #44). Eight marker types (city, factory, army, port, capital, fleet, rocket, gathering) are anchored to a region key, persisted per game, and rendered on the choropleth as a MapLibre symbol layer.
Marker id = FNV-1a 32-bit of type + NUL + name + NUL + regionKey -> 4 lowercase base-26 chars -> idempotent re-add, unambiguous remove.
GameState.markers: MapMarker[] required ([] at init; legacy SQLite rows load as [] - additive, never wipes).
applyTurnResult processes markers after territory claims: remove -> owner recompute (a marker always follows its region; released -> null) -> add (centroid + deterministic jitter, cap MAX_MARKERS=400, never throws).
New data
public/region-centroids.json (1,800 anchors = largest-part centroid + spread ~ 0.1x min-bbox), built by the new idempotent scripts/build-region-centroids.mjs (npm run build:region-centroids).
Rendering (deliberate deviation)
Markers render as canvas-generated per-type ICON images, not text labels: every external glyph endpoint is a dead 404 (OpenFreeMap fonts/...pbf, demotiles.maplibre.org/font/...) and the OFM dark basemap only works because it's raster (labels baked into tiles) - there is no usable vector font. Names show in the hover tooltip; clicking a marker focuses its owning nation's dossier. No separate marker event handlers (MapLibre layer events don't stopPropagation) - the region handlers queryRenderedFeatures the marker layer under the cursor.
Tests
168 -> 189. New scripts/region-centroids.test.ts; server/regions.test.ts (centroids in dev + prod dist/), server/db.test.ts (legacy -> []), engine.test.ts marker suite. Stub stays marker-free -> e2e (CI, LLM_STUB=1) unaffected.
Verification
Typecheck, lint, 189 unit tests, and the production build all pass locally (the build copies region-centroids.json into dist/). Deploy is a manual docker compose up -d --build (no down -v).
## What
The LLM can now place **named point markers** on the map each turn (issue #44). Eight marker types (`city, factory, army, port, capital, fleet, rocket, gathering`) are anchored to a region key, persisted per game, and rendered on the choropleth as a MapLibre symbol layer.
## Contract
- `TurnResult.markers = { add: MarkerSpec[]; remove: string[] }` (both required, default `[]`); `MarkerSpec = { type, name, regionKey }`.
- Marker `id` = FNV-1a 32-bit of `type + NUL + name + NUL + regionKey` -> 4 lowercase base-26 chars -> **idempotent re-add**, **unambiguous remove**.
- `GameState.markers: MapMarker[]` required (`[]` at init; legacy SQLite rows load as `[]` - additive, never wipes).
- `applyTurnResult` processes markers **after** territory claims: remove -> **owner recompute** (a marker always follows its region; released -> `null`) -> add (centroid + deterministic jitter, cap `MAX_MARKERS=400`, never throws).
## New data
`public/region-centroids.json` (1,800 anchors = **largest-part centroid** + `spread ~ 0.1x min-bbox`), built by the new idempotent `scripts/build-region-centroids.mjs` (`npm run build:region-centroids`).
## Rendering (deliberate deviation)
Markers render as **canvas-generated per-type ICON images, not text labels**: every external glyph endpoint is a dead 404 (OpenFreeMap `fonts/...pbf`, `demotiles.maplibre.org/font/...`) and the OFM dark basemap only works because it's raster (labels baked into tiles) - there is no usable vector font. Names show in the **hover tooltip**; clicking a marker focuses its owning nation's dossier. No separate marker event handlers (MapLibre layer events don't stopPropagation) - the region handlers `queryRenderedFeatures` the marker layer under the cursor.
## Tests
**168 -> 189.** New `scripts/region-centroids.test.ts`; `server/regions.test.ts` (centroids in dev + prod `dist/`), `server/db.test.ts` (legacy -> `[]`), `engine.test.ts` marker suite. **Stub stays marker-free** -> e2e (CI, `LLM_STUB=1`) unaffected.
## Verification
Typecheck, lint, **189 unit tests**, and the production build all pass locally (the build copies `region-centroids.json` into `dist/`). Deploy is a manual `docker compose up -d --build` (no `down -v`).
Fixes #44
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
The LLM can now place named point markers on the map each turn (issue #44). Eight marker types (
city, factory, army, port, capital, fleet, rocket, gathering) are anchored to a region key, persisted per game, and rendered on the choropleth as a MapLibre symbol layer.Contract
TurnResult.markers = { add: MarkerSpec[]; remove: string[] }(both required, default[]);MarkerSpec = { type, name, regionKey }.id= FNV-1a 32-bit oftype + NUL + name + NUL + regionKey-> 4 lowercase base-26 chars -> idempotent re-add, unambiguous remove.GameState.markers: MapMarker[]required ([]at init; legacy SQLite rows load as[]- additive, never wipes).applyTurnResultprocesses markers after territory claims: remove -> owner recompute (a marker always follows its region; released ->null) -> add (centroid + deterministic jitter, capMAX_MARKERS=400, never throws).New data
public/region-centroids.json(1,800 anchors = largest-part centroid +spread ~ 0.1x min-bbox), built by the new idempotentscripts/build-region-centroids.mjs(npm run build:region-centroids).Rendering (deliberate deviation)
Markers render as canvas-generated per-type ICON images, not text labels: every external glyph endpoint is a dead 404 (OpenFreeMap
fonts/...pbf,demotiles.maplibre.org/font/...) and the OFM dark basemap only works because it's raster (labels baked into tiles) - there is no usable vector font. Names show in the hover tooltip; clicking a marker focuses its owning nation's dossier. No separate marker event handlers (MapLibre layer events don't stopPropagation) - the region handlersqueryRenderedFeaturesthe marker layer under the cursor.Tests
168 -> 189. New
scripts/region-centroids.test.ts;server/regions.test.ts(centroids in dev + proddist/),server/db.test.ts(legacy ->[]),engine.test.tsmarker suite. Stub stays marker-free -> e2e (CI,LLM_STUB=1) unaffected.Verification
Typecheck, lint, 189 unit tests, and the production build all pass locally (the build copies
region-centroids.jsonintodist/). Deploy is a manualdocker compose up -d --build(nodown -v).Fixes #44
- shared: 8 MarkerTypes + MARKER_TYPE_INFO (label/color/glyph); MapMarker; GameState.markers (required, [] at init); TurnResult.markers {add,remove}; MarkerSpec/MarkerInfo; FNV-1a 4-char markerId; MAX_MARKERS=400. - data: new public/region-centroids.json (1,800 anchors = largest-part centroid + spread ~0.1x min-bbox) built by new idempotent scripts/build-region-centroids.mjs. - server: regions.ts centroid loader (public/ then dist/); engine applyTurnResult marker ops (remove -> owner-recompute -> add, jitter, cap, never throws); prompt documents the contract; buildUserPrompt carries markers; stub marker-free. - client: WorldMap symbol layer rendered as canvas per-type icons (glyph CDN dead); hover tooltip shows name; click focuses owning nation. - tests: 168 -> 189 (centroids script, regions centroids, db legacy, engine markers).