From e37692076681ac7035fd05ca83c8ad99b8262654 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 26 Jun 2026 01:37:59 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=93=87=20p3-25=20step=203=20=E2=80=94=20resource-category?= =?UTF-8?q?=20catalog=20into=20Rust=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rail-1 city-model unification, step 3: give the headless sim the luxury/strategic categories it needs to classify owned-tile resources for trade sourcing — currently GDScript-only (DataLoader). No content hardcoded in Rust (Rail-2): loaded from JSON. - GameState.resource_categories: BTreeMap (id → "luxury"/"strategic"/ "bonus"), #[serde(skip)] boot-loaded exactly like units_catalog/civic_catalog (not save-persisted; empty Default → nothing tradeable, a safe no-op). - GameState::load_resource_categories_json parses the flat {id:category} object GDScript's DataLoader emits; no-clobber on malformed input. - GdPlayerApi.set_resource_categories_json FFI loads it onto the held state (call after load_state_json, since the field is serde-skip). Verified: mc-state load_resource_categories_parses_flat_map + suite 13/0; workspace cargo check clean (GameState field addition broke no literals — all use ..Default). Rust-only; live game unaffected. Unblocks step 4 (process_trade_phase classification). p3-25 steps 1-3 done; 4-6 remain. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-model-unify-headless-view-completeness.md | 10 +++-- .../games/age-of-dwarves/data/objectives.json | 8 ++-- src/simulator/api-gdext/src/player_api.rs | 11 +++++ .../crates/mc-state/src/game_state.rs | 44 +++++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/.project/objectives/p3-25-rail1-city-model-unify-headless-view-completeness.md b/.project/objectives/p3-25-rail1-city-model-unify-headless-view-completeness.md index b5eb720d..0218f0e4 100644 --- a/.project/objectives/p3-25-rail1-city-model-unify-headless-view-completeness.md +++ b/.project/objectives/p3-25-rail1-city-model-unify-headless-view-completeness.md @@ -64,9 +64,13 @@ to `state.trade_ledger`, (d) projecting it all. (mc-turn) + `projection_surfaces_city_owned_tiles` (mc-player-api); fixed a pre-existing broken `serde_roundtrip` HappinessInput literal in passing. cargo mc-city+mc-turn+ mc-player-api **725/0**. -- [ ] **Step 3 — resource-category catalog in Rust.** Load luxury/strategic categories - (currently GDScript-only via DataLoader) into Rust via a `set_resources_catalog_json` - + storage, so the sim can classify owned-tile resources. +- [x] **Step 3 — resource-category catalog in Rust.** `GameState.resource_categories: + BTreeMap` (id→"luxury"/"strategic"/"bonus"), `#[serde(skip)]` + boot-loaded like `units_catalog`. `GameState::load_resource_categories_json` parses the + flat `{id:category}` map GDScript's DataLoader emits; `GdPlayerApi. + set_resource_categories_json` FFI loads it onto the held state (call after + `load_state_json`). **Done 2026-06-26** — `load_resource_categories_parses_flat_map` + (mc-state 13/0); workspace `cargo check` clean (no GameState-literal breakage). - [ ] **Step 4 — real trade sourcing + persistence.** `mc-turn::process_trade_phase` sources real owned-tile luxuries/strategics (owned tiles → `tile_collectibles` → category-classify), runs `evaluate_trades`, and **persists** the swap/sale ledger into diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 5236ff87..c599984b 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-06-26T05:18:24Z", + "generated_at": "2026-06-26T05:37:59Z", "totals": { - "done": 296, "in_progress": 0, - "oos": 31, - "partial": 2, "stub": 0, + "oos": 31, + "done": 296, "missing": 0, + "partial": 2, "total": 329 }, "objectives": [ diff --git a/src/simulator/api-gdext/src/player_api.rs b/src/simulator/api-gdext/src/player_api.rs index 30116f5b..e6fa752d 100644 --- a/src/simulator/api-gdext/src/player_api.rs +++ b/src/simulator/api-gdext/src/player_api.rs @@ -146,6 +146,17 @@ impl GdPlayerApi { } } + /// p3-25: load the resource id→category map (`{"iron_ore":"strategic", + /// "silk":"luxury",…}`) so the headless turn can classify owned-tile + /// resources for trade sourcing (`process_trade_phase`). Returns the number + /// of entries loaded, or 0 on parse failure. Call AFTER `load_state_json` — + /// the map is `#[serde(skip)]` and is not restored by a state load. + #[func] + pub fn set_resource_categories_json(&mut self, json: GString) -> i64 { + self.state + .load_resource_categories_json(json.to_string().as_str()) as i64 + } + /// Stamp the runtime `UnitsCatalog` (id → `UnitStats`) onto the held /// `GameState`. Distinct from `set_units_catalog_json` (which loads the /// tactical `ai_unit_catalog`): this is the same `mc_units::UnitsCatalog` diff --git a/src/simulator/crates/mc-state/src/game_state.rs b/src/simulator/crates/mc-state/src/game_state.rs index af80c4e1..96cfc2f0 100644 --- a/src/simulator/crates/mc-state/src/game_state.rs +++ b/src/simulator/crates/mc-state/src/game_state.rs @@ -403,6 +403,14 @@ pub struct GameState { /// and pre-load paths see no civic effect. #[serde(skip)] pub civic_catalog: mc_civics::CivicCatalog, + /// p3-25: resource id → category ("luxury" | "strategic" | "bonus"), loaded + /// from `public/resources/deposits/*.json`. Lets the headless turn classify + /// owned-tile resources for trade sourcing (`process_trade_phase`) without + /// reaching into GDScript's DataLoader. `#[serde(skip)]` mirrors + /// `units_catalog` — boot-loaded via `set_resource_categories_json`, not + /// save-persisted. Empty (Default) → no resource is tradeable (safe no-op). + #[serde(skip)] + pub resource_categories: BTreeMap, /// p2-71: tactical-AI view of the producible-unit catalog. Mirrors /// `TacticalState::unit_catalog` and is populated once at harness boot /// by `GdPlayerApi::set_units_catalog_json` (or directly in Rust tests). @@ -658,6 +666,21 @@ impl PendingCaptureEvents { } impl GameState { + /// p3-25: load the resource id→category map from a flat JSON object + /// (`{"iron_ore":"strategic","silk":"luxury",…}`), the shape GDScript's + /// DataLoader emits from `public/resources/deposits/*.json`. Replaces any + /// existing map. Returns the number of entries loaded, or 0 on parse failure. + pub fn load_resource_categories_json(&mut self, json: &str) -> usize { + match serde_json::from_str::>(json) { + Ok(map) => { + let n = map.len(); + self.resource_categories = map; + n + } + Err(_) => 0, + } + } + /// p2-65 Phase7 test helper: construct a GameState whose combat_balance /// (and future SimConfig fields) are pre-populated without touching the /// global RwLock singleton. Callers that need isolated config for @@ -1418,6 +1441,27 @@ pub struct TechState { mod p2_72a_save_round_trip_tests { use super::*; + #[test] + fn load_resource_categories_parses_flat_map() { + let mut state = GameState::default(); + let n = state.load_resource_categories_json( + r#"{"iron_ore":"strategic","silk":"luxury","wheat":"bonus"}"#, + ); + assert_eq!(n, 3); + assert_eq!( + state.resource_categories.get("iron_ore").map(String::as_str), + Some("strategic") + ); + assert_eq!( + state.resource_categories.get("silk").map(String::as_str), + Some("luxury") + ); + // Malformed JSON → 0 and the existing map is left intact (no clobber). + let n2 = state.load_resource_categories_json("not json"); + assert_eq!(n2, 0); + assert_eq!(state.resource_categories.len(), 3); + } + #[test] fn default_game_state_round_trips_through_serde() { let g = GameState::default();