feat(@projects/@magic-civilization): 📇 p3-25 step 3 — resource-category catalog into Rust state
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<String,String> (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) <noreply@anthropic.com>
This commit is contained in:
parent
37fbb6153d
commit
e376920766
4 changed files with 66 additions and 7 deletions
|
|
@ -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<String,String>` (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
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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<String, String>,
|
||||
/// 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::<BTreeMap<String, String>>(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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue