magicciv/.project/objectives/p3-04-per-hex-improvement-layer.md
Natalie 843081e152 feat(@projects/@magic-civilization): mark p3-04 hex improvement layer as complete
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-28 19:24:48 -04:00

6.3 KiB
Raw Blame History

id title priority status scope owner updated_at evidence
p3-04 Per-hex improvement layer in `mc-core` / `mc-turn` — anchor improvements at (col,row) p3 done game1-stretch envoy 2026-04-28
src/simulator/crates/mc-core/src/grid.rs (`GridState.tiles` — has terrain, no improvements field)
src/simulator/crates/mc-turn/src/game_state.rs (`PlayerState.city_improvements
Vec<Vec<String>>` — per-player/per-city, no hex anchor)
.project/objectives/p3-03-courier-route-resolver.md (blocks on this — severance + infra-gated tier paths cannot resolve without a hex→improvement lookup)
public/resources/improvements/{tunnel,hold_road,steam_track,resonance_wire,beacon_tower}.json (improvement data already authored in p3-01)

Summary

Improvements ship as data files (public/resources/improvements/*.json) but the simulation has no per-hex anchor for them. Improvements currently live on PlayerState.city_improvements: Vec<Vec<String>> (per-player / per-city, unanchored). The grid's per-tile struct stores terrain only — no improvement slot.

This blocks p3-03 acceptance bullets 4 (severance: pillaging Steam Track at hex (c,r) intercepts a courier whose route includes (c,r)), 5 (Hold-Network reroute when a Steam Track is severed), and the infrastructure-gating half of bullet 2 (Steam Messenger requires Steam Track tiles on its route, Resonance Telegrapher requires Resonance Wire tiles).

This is also a foundational gap that will block other Game-1-stretch features beyond couriers (tile-improvement pillaging, road network bonuses, fortification ZOC, defensive towers).

Acceptance criteria

  • Schema decision documented: Tile.improvement: Option<TileImprovement> on the grid struct vs a separate sparse BTreeMap<(u16,u16), TileImprovement> keyed by (col,row). Recommend the sparse map — most tiles will be unimproved, the dense option wastes memory in a 60×60+ hex world. Document the call.
  • TileImprovement struct authored in mc-core (or mc-turn, wherever the schema decision puts it): id: String, hp: i32, severable: bool, pillaged: bool, plus a stable flags: BTreeSet<String> mirror of the JSON flags array.
  • GameState accessors: improvement_at(col, row) -> Option<&TileImprovement>, set_improvement(col, row, imp), remove_improvement(col, row), pillage_improvement(col, row) -> bool (returns true if the improvement was severable + flips pillaged: true; non-severable improvements are destroyed instead).
  • DataLoader → runtime hydration: improvement JSONs in public/resources/improvements/ load into a BTreeMap<id, TileImprovementSpec> registry; set_improvement consults the registry to populate hp/severable/flags from spec.
  • Serde round-trip: GameState with non-empty improvement layer round-trips through serde JSON cleanly.
  • Unit tests in mc-turn: improvement_at empty grid returns None, set_improvement + improvement_at round-trip, pillage_improvement on severable returns true + sets pillaged, pillage_improvement on non-severable destroys it (returns false from improvement_at after).
  • No GDExtension binding work in this objective — the courier route resolver (p3-03) consumes the API directly from Rust. UI consumers come later.

Cycle 1 progress (2026-04-27, hex-layer agent)

Schema decision: Sparse BTreeMap<(u16,u16), TileImprovement> on GameState — confirmed. A 60×60 grid has 3 600 tiles; improvements are sparse in practice (maybe 50200 tiles at peak), so a dense Option on every TileState would bloat saves and deserialize cycles for zero benefit.

TileImprovement and TileImprovementSpec: Authored in mc-core/src/improvement.rs. TileImprovementSpec parses public/resources/improvements/*.json via RawImprovementJson (tolerates missing effects, flags, hp fields). TileImprovement is the live instance with id, hp, severable, pillaged, flags: BTreeSet<String>.

GameState accessors (in mc-turn/src/game_state.rs):

  • improvement_at(col: u16, row: u16) -> Option<&TileImprovement>
  • set_improvement(col: u16, row: u16, spec: &TileImprovementSpec)
  • remove_improvement(col: u16, row: u16)
  • pillage_improvement(col: u16, row: u16) -> bool (true = severable+pillaged; false = destroyed) ✓
  • load_improvement_specs(specs: impl IntoIterator<Item = TileImprovementSpec>)

improvement_registry field on GameState is #[serde(skip)] so it never bloats saves; hydrated from JSON at game start via load_improvement_specs.

Serde round-trip: uses improvements_as_pairs serde adapter (same pattern as relations_as_pairs) to avoid JSON's "key must be a string" restriction on tuple keys.

Tests (all pass, cargo test -p mc-turn improvement):

  • improvement_at_empty_grid_returns_none
  • set_improvement_and_improvement_at_round_trip
  • pillage_severable_sets_pillaged_flag_and_returns_true
  • pillage_nonseverable_destroys_improvement_and_returns_false
  • pillage_empty_hex_returns_false
  • remove_improvement_clears_hex
  • serde_round_trip_with_improvements

Also added rand.workspace = true to mc-turn/dev-dependencies to unblock route-resolver's test code that uses rand in courier_resolver.rs.

Acceptance bullets:

  • ✓ Schema decision documented (sparse BTreeMap)
  • TileImprovement struct in mc-core
  • GameState accessors: improvement_at, set_improvement, remove_improvement, pillage_improvement
  • ✓ DataLoader hydration via TileImprovementSpec::from_json + load_improvement_specs
  • ✓ Serde round-trip
  • ✓ Unit tests in mc-turn (7 pass)
  • ✓ No GDExtension binding work

Non-goals

  • Engine units placing improvements (separate objective: an engineer unit + improvement-build orders).
  • Pillage-by-enemy-unit AI (mc-ai concern, dispatched separately).
  • Visual rendering of improvements on the world map (godot-renderer concern).
  • Migration of PlayerState.city_improvements to use this layer — that's a related but separate refactor; for now the per-hex layer coexists with the per-city list.

Dependencies

  • Inputs: existing mc-core::GridState, the 5 improvement JSONs already authored in p3-01 cycle 2/3, improvement.schema.json.
  • Blocks on: nothing.
  • Blocks: p3-03 (courier route resolver) bullets 4, 5, and the infra-gated half of 2.