feat(@projects/@magic-civilization): 🔌 p3-25 — wire headless harness to load resource categories (trade pipeline now LIVE)

The headless trade pipeline was unit-proven but inert in real runs: nothing called
set_resource_categories_json, so process_trade_phase saw empty categories and sourced
nothing. Wire it in.

- scenes/headless/player_api_main.gd::_apply_resource_categories builds the resource
  id→category map from DataLoader.get_all_resources() and stamps it onto GdPlayerApi via
  set_resource_categories_json, AFTER load_state_json (same #[serde(skip)] re-stamp
  pattern as units_runtime_catalog + tech_web). Now a real headless game classifies
  owned-tile collectibles → sources luxury/strategic surpluses → forms trades → view_json
  carries them. End-to-end LIVE.

Verified: unit+integration GUT 750 (737 pass / 13 pending / 0 fail); the headless
projection-roundtrip boot path (which exercises _apply_resource_categories) is green.
GDScript-only change calling an existing FFI — no dylib rebuild needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-26 02:32:42 -04:00
parent 91c0b79ad1
commit 3f7a4e5442
3 changed files with 35 additions and 6 deletions

View file

@ -88,7 +88,12 @@ to `state.trade_ledger`, (d) projecting it all.
swap/sale agreements (`swap_deal_view`/`sale_deal_view`). **`view_json` now carries
real inter-player trades** — the headless "simulator provides everything" goal is met
(gate is the headless assertion, no UI screenshot). **Done 2026-06-26**
`projection_surfaces_trade_deals_from_ledger` (mc-player-api 171/0).
`projection_surfaces_trade_deals_from_ledger` (mc-player-api 171/0). **Harness wiring:**
`scenes/headless/player_api_main.gd::_apply_resource_categories` now builds the
id→category map from DataLoader and calls `set_resource_categories_json` after
`load_state_json` (mirroring the units/tech catalog re-stamps), so a real headless run
actually sources trades — the pipeline is LIVE, not just unit-tested. (unit+integration
GUT 750: 737 pass / 13 pending / 0 fail.)
- [ ] **Step 6 — live game adopts the unified `PlayerView` (large, separate).** The
*headless* path is now complete (steps 1-5). Making the **live game** GDScript
view-only for trades is a much bigger initiative: the live game reads `GameState` via

View file

@ -1,12 +1,12 @@
{
"generated_at": "2026-06-26T06:26:53Z",
"generated_at": "2026-06-26T06:32:42Z",
"totals": {
"partial": 2,
"done": 296,
"missing": 0,
"oos": 31,
"stub": 0,
"partial": 2,
"in_progress": 0,
"missing": 0,
"stub": 0,
"oos": 31,
"total": 329
},
"objectives": [

View file

@ -222,6 +222,30 @@ func _hydrate_player_api(num_players: int) -> void:
# stalemates.
_apply_tech_web()
# p3-25: stamp the resource id→category map so the headless turn can classify
# owned-tile collectibles for trade sourcing (`process_trade_phase`). Same
# `#[serde(skip)]` constraint as the catalogs above — load AFTER
# `load_state_json`. Without this, `resource_categories` is empty and no
# inter-player trade ever sources.
_apply_resource_categories()
## p3-25: build the resource id→category map ("luxury" | "strategic" | "bonus")
## from DataLoader and stamp it onto `GdPlayerApi` via
## `set_resource_categories_json`. Consumed by `mc-turn::process_trade_phase`.
func _apply_resource_categories() -> void:
var map: Dictionary = {}
for res: Dictionary in DataLoader.get_all_resources():
var rid: String = str(res.get("id", ""))
var cat: String = str(res.get("category", ""))
if not rid.is_empty() and not cat.is_empty():
map[rid] = cat
if map.is_empty():
_emit_protocol_error("resource categories empty — headless trades will not source")
return
var n: int = int(_api.set_resource_categories_json(JSON.stringify(map)))
_emit_event("resource_categories_api_loaded", {"resources": n})
## Stamp the playable race onto every player slot's simulation `PlayerState`.
## `GdGameState::set_player_presentation_json` mirrors `race_id` onto