LLM map markers: named point markers (city/factory/army/port/capital/fleet/rocket/gathering) anchored to regions — v0.27.0 #44

Closed
opened 2026-10-14 23:35:03 +02:00 by kamilkrzywda · 0 comments
Owner

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.
## 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.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OpenCode/History#44