Let the LLM place named point markers on the map (cities, factories, armies, ports, capitals, fleets, rocket launch sites, political/social gatherings), anchored to a region key, persisted per game, and rendered as a new MapLibre symbol layer — v0.27.0.
Scope
Data model
MarkerType union of exactly 8: city, factory, army, port, capital, fleet, rocket, gathering (+ MARKER_TYPES array, display-name map, per-type color/glyph in shared/protocol.ts).
MapMarker = { id, type, name, regionKey, lat, lon, ownerNationId, placedTurn }; GameState.markers: MapMarker[] required, [] at init, additive (legacy rows load with [], no wipe — same pattern as unowned).
Marker id: stable deterministic 4-char code (FNV-1a 32-bit, base-26 lowercase) of type + name + regionKey — same approach as region keys; re-add is idempotent, remove-by-id unambiguous.
User payload gains a markers field (current markers as { id, type, name, regionKey, owner }); system prompt documents the rule concisely: selective placement (a handful per turn), meaningful (capital in capital region, factory industrial, army near frontier, fleet near coast), gathering = event enabling non-military takeover, real region keys only.
Engine (server/engine.ts)
applyTurnResult applies marker ops after territoryChanges so ownerNationId uses post-turn ownership; the owner is recomputed every turn (released region → null).
add: drop invalid type / unknown regionKey / empty name; position = region centroid + deterministic spread offset (bounded by the region spread); dedupe by id; cap total markers (drop new adds at cap).
remove: drop by id, no-op if absent. Never throws; version bumped once per turn.
Centroid manifest
New scripts/build-region-centroids.mjs (turf allowed in .mjs only) → public/region-centroids.json = { [regionKey]: { lat, lon, spread } }, keyed identically to region-keys.json (~1,800 entries).
Centroid = centroid of the largest-area part (avoids open-water MultiPolygon centroids); spread ≈ 0.1 × that part's min bbox dimension.
server/regions.ts: turf-free loader with the same public/ → dist/ → root probe → centroidOf(key) / hasCentroids().
Rendering (WorldMap.tsx)
Second GeoJSON source built from GameState.markers (Point at [lon, lat]) + a symbol layer above the choropleth: per-type color via a match expression on type, name as text label, small/legible in the dark command-center theme.
Reuse the existing hover tooltip (type + name + owning nation); click focuses the owning nation's dossier.
Source/layer update reactively when markers change.
Stub
server/stub.ts returns markers = { add: [], remove: [] } — marker-free (e2e stays deterministic; the marker path is unit-tested only, mirroring no-man's-land releases and nation ops).
Tests
scripts/region-centroids.test.ts (dependency-free): count ~1,800, keys == region-keys keys, deterministic, each centroid within the region bbox.
server/engine.test.ts: add (valid → correct position/owner/id; bad regionKey/type dropped; dedupe; cap), remove (valid gone; bad no-op), owner-follows-region (captured same turn; released → null), prompt includes current markers + system prompt documents the rule.
server/regions.test.ts: centroid loading on the prod path (dist/ when public/ absent).
Stub test: markers empty. All 168 existing tests stay green.
Ship
Version 0.26.0 → 0.27.0; CHANGELOG entry; AGENTS.md documents the feature; tag + manual deploy, no data wipe.
## Goal
Let the LLM place **named point markers on the map** (cities, factories, armies, ports, capitals, fleets, rocket launch sites, political/social gatherings), anchored to a region key, persisted per game, and rendered as a new MapLibre **symbol** layer — v0.27.0.
## Scope
### Data model
- MarkerType union of exactly 8: city, factory, army, port, capital, fleet, rocket, gathering (+ MARKER_TYPES array, display-name map, per-type color/glyph in shared/protocol.ts).
- MapMarker = { id, type, name, regionKey, lat, lon, ownerNationId, placedTurn }; GameState.markers: MapMarker[] required, [] at init, **additive** (legacy rows load with [], no wipe — same pattern as unowned).
- Marker id: stable deterministic 4-char code (FNV-1a 32-bit, base-26 lowercase) of type + name + regionKey — same approach as region keys; re-add is idempotent, remove-by-id unambiguous.
### LLM contract (shared/protocol.ts)
- TurnResult.markers = { add: MarkerSpec[]; remove: string[] }, both required (default []); MarkerSpec = { type, name, regionKey }.
- User payload gains a markers field (current markers as { id, type, name, regionKey, owner }); system prompt documents the rule concisely: selective placement (a handful per turn), meaningful (capital in capital region, factory industrial, army near frontier, fleet near coast), gathering = event enabling non-military takeover, real region keys only.
### Engine (server/engine.ts)
- applyTurnResult applies marker ops **after** territoryChanges so ownerNationId uses **post-turn** ownership; the owner is **recomputed every turn** (released region → null).
- add: drop invalid type / unknown regionKey / empty name; position = region centroid + deterministic spread offset (bounded by the region spread); dedupe by id; cap total markers (drop new adds at cap).
- remove: drop by id, no-op if absent. Never throws; version bumped once per turn.
### Centroid manifest
- New scripts/build-region-centroids.mjs (turf allowed in .mjs only) → public/region-centroids.json = { [regionKey]: { lat, lon, spread } }, keyed identically to region-keys.json (~1,800 entries).
- Centroid = centroid of the **largest-area part** (avoids open-water MultiPolygon centroids); spread ≈ 0.1 × that part's min bbox dimension.
- Deterministic + idempotent (byte-identical re-run); npm script build:region-centroids.
- server/regions.ts: turf-free loader with the same public/ → dist/ → root probe → centroidOf(key) / hasCentroids().
### Rendering (WorldMap.tsx)
- Second GeoJSON source built from GameState.markers (Point at [lon, lat]) + a **symbol layer** above the choropleth: per-type color via a match expression on type, name as text label, small/legible in the dark command-center theme.
- Reuse the existing hover tooltip (type + name + owning nation); click focuses the owning nation's dossier.
- Source/layer update reactively when markers change.
### Stub
- server/stub.ts returns markers = { add: [], remove: [] } — **marker-free** (e2e stays deterministic; the marker path is unit-tested only, mirroring no-man's-land releases and nation ops).
### Tests
- scripts/region-centroids.test.ts (dependency-free): count ~1,800, keys == region-keys keys, deterministic, each centroid within the region bbox.
- server/engine.test.ts: add (valid → correct position/owner/id; bad regionKey/type dropped; dedupe; cap), remove (valid gone; bad no-op), owner-follows-region (captured same turn; released → null), prompt includes current markers + system prompt documents the rule.
- server/regions.test.ts: centroid loading on the prod path (dist/ when public/ absent).
- Stub test: markers empty. All 168 existing tests stay green.
### Ship
- Version 0.26.0 → **0.27.0**; CHANGELOG entry; AGENTS.md documents the feature; tag + manual deploy, no data wipe.
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.
Goal
Let the LLM place named point markers on the map (cities, factories, armies, ports, capitals, fleets, rocket launch sites, political/social gatherings), anchored to a region key, persisted per game, and rendered as a new MapLibre symbol layer — v0.27.0.
Scope
Data model
LLM contract (shared/protocol.ts)
Engine (server/engine.ts)
Centroid manifest
Rendering (WorldMap.tsx)
Stub
Tests
Ship