feat(@projects/@magic-civilization): ⚒️ p3-26 B6a (1/2) — resource-refinement (recipe) tick in the headless turn
Wires the previously-unwired mc_city::recipes system into the turn: - GameState += recipes_json (#[serde(skip)] boot) + load_recipes_json (mc-state holds the raw bundle JSON; no mc-city dep). - recipe_phase::process_recipe_phase — per player, loads strategic_ledger into a ResourceStockpile, runs tick_recipes over the player's processing buildings (consume raw → produce refined), writes the transformed ledger back. Registered in END_OF_TURN_PHASES (ecology, healing, improvement_build, recipe_refine). Refined resources land in strategic_ledger (which trade/economy already use), so it's a real economic transform, not inert. Tests: refines, idle-on-shortage, no-op-without-recipes. mc-turn recipe_phase 3/3. Remaining (2/2): FFI + harness to boot recipes.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
45d52c8ce6
commit
dd1a537ab5
4 changed files with 118 additions and 1 deletions
|
|
@ -439,6 +439,12 @@ pub struct GameState {
|
|||
/// no improvements build or yield (safe no-op).
|
||||
#[serde(skip)]
|
||||
pub improvement_defs: BTreeMap<String, ImprovementDef>,
|
||||
/// p3-26 B6a: raw recipe-bundle JSON (`{"recipes":[{building_id,consumes,produces}]}`)
|
||||
/// from `public/resources/recipes/recipes.json`, boot-loaded. `#[serde(skip)]` static
|
||||
/// content; the recipe phase parses it into a `RecipeRegistry` each turn (so mc-state
|
||||
/// needs no mc-city dependency). Empty → no resource refinement runs (safe no-op).
|
||||
#[serde(skip)]
|
||||
pub recipes_json: 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).
|
||||
|
|
@ -776,6 +782,13 @@ impl GameState {
|
|||
n
|
||||
}
|
||||
|
||||
/// p3-26 B6a: store the raw recipe-bundle JSON (parsed into a RecipeRegistry by the
|
||||
/// recipe phase each turn). Returns the byte length stored (0 if empty). Called at boot.
|
||||
pub fn load_recipes_json(&mut self, json: &str) -> usize {
|
||||
self.recipes_json = json.to_string();
|
||||
self.recipes_json.len()
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ pub mod healing;
|
|||
pub mod ecology_phase;
|
||||
/// p3-26 B3 — tile-improvement build-tick.
|
||||
pub mod improvement_phase;
|
||||
/// p3-26 B6a — resource-refinement (recipe) tick.
|
||||
pub mod recipe_phase;
|
||||
/// End-of-turn world-simulation phase registry (ecology, healing, …).
|
||||
pub mod sim_phases;
|
||||
#[cfg(feature = "gpu")]
|
||||
|
|
|
|||
98
src/simulator/crates/mc-turn/src/recipe_phase.rs
Normal file
98
src/simulator/crates/mc-turn/src/recipe_phase.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
//! p3-26 B6a — resource-refinement (recipe) tick.
|
||||
//!
|
||||
//! Wires `mc_city::recipes` into the headless turn: each player's processing
|
||||
//! buildings consume raw resources and produce refined ones, transforming the
|
||||
//! player's `strategic_ledger`. The recipe registry is parsed from the
|
||||
//! boot-loaded `recipes_json` each turn (mc-state holds it as raw JSON so it
|
||||
//! needs no mc-city dependency). No-op when no recipes are booted or the player
|
||||
//! has no buildings.
|
||||
//!
|
||||
//! Registered in [`crate::sim_phases::END_OF_TURN_PHASES`]. The player's
|
||||
//! `strategic_ledger` is loaded into a `ResourceStockpile` the recipes operate
|
||||
//! on, then written back (capturing both consumed and produced deltas).
|
||||
|
||||
use mc_city::recipes::{tick_recipes, RecipeRegistry};
|
||||
use mc_core::{BuildingId, ResourceId, ResourceStockpile};
|
||||
use mc_state::game_state::GameState;
|
||||
|
||||
/// Run every player's processing-building recipes once. No-op without recipes.
|
||||
pub fn process_recipe_phase(state: &mut GameState) {
|
||||
if state.recipes_json.is_empty() {
|
||||
return;
|
||||
}
|
||||
let registry = match RecipeRegistry::from_json(&state.recipes_json) {
|
||||
Ok(r) if !r.is_empty() => r,
|
||||
_ => return,
|
||||
};
|
||||
for player in &mut state.players {
|
||||
// Every processing building the player owns, across all cities.
|
||||
let buildings: Vec<BuildingId> = player
|
||||
.city_buildings
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|b| BuildingId::new(b.clone()))
|
||||
.collect();
|
||||
if buildings.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Load the player's resources into the stockpile the recipes operate on.
|
||||
let mut stock = ResourceStockpile::new();
|
||||
for (id, qty) in &player.strategic_ledger {
|
||||
stock.add(ResourceId::new(id.clone()), *qty);
|
||||
}
|
||||
let _outcomes = tick_recipes(&buildings, ®istry, &mut stock);
|
||||
// Write the refined ledger back (captures produced + consumed deltas).
|
||||
let mut ledger = std::collections::BTreeMap::new();
|
||||
for (rid, qty) in stock.entries() {
|
||||
if *qty > 0 {
|
||||
ledger.insert(rid.as_str().to_string(), *qty);
|
||||
}
|
||||
}
|
||||
player.strategic_ledger = ledger;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mc_state::game_state::{GameState, PlayerState};
|
||||
|
||||
const RECIPES: &str = r#"{"recipes":[{"building_id":"smelter","consumes":[{"resource":"iron_ore","qty_per_turn":2}],"produces":[{"resource":"iron_ingot","qty_per_turn":1}]}]}"#;
|
||||
|
||||
fn player_with(ledger: &[(&str, u32)], buildings: &[&str]) -> PlayerState {
|
||||
let mut p = PlayerState::default();
|
||||
p.strategic_ledger = ledger.iter().map(|(k, v)| (k.to_string(), *v)).collect();
|
||||
p.city_buildings = vec![buildings.iter().map(|b| b.to_string()).collect()];
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recipe_phase_refines_resources() {
|
||||
let mut state = GameState::default();
|
||||
state.recipes_json = RECIPES.to_string();
|
||||
state.players.push(player_with(&[("iron_ore", 10)], &["smelter"]));
|
||||
process_recipe_phase(&mut state);
|
||||
let led = &state.players[0].strategic_ledger;
|
||||
assert_eq!(led.get("iron_ore").copied().unwrap_or(0), 8, "consumed 2 ore");
|
||||
assert_eq!(led.get("iron_ingot").copied().unwrap_or(0), 1, "produced 1 ingot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recipe_phase_idle_without_enough_input() {
|
||||
let mut state = GameState::default();
|
||||
state.recipes_json = RECIPES.to_string();
|
||||
state.players.push(player_with(&[("iron_ore", 1)], &["smelter"])); // need 2
|
||||
process_recipe_phase(&mut state);
|
||||
let led = &state.players[0].strategic_ledger;
|
||||
assert_eq!(led.get("iron_ore").copied().unwrap_or(0), 1, "insufficient input → idle");
|
||||
assert_eq!(led.get("iron_ingot").copied().unwrap_or(0), 0, "nothing produced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recipe_phase_noop_without_recipes() {
|
||||
let mut state = GameState::default();
|
||||
state.players.push(player_with(&[("iron_ore", 5)], &["smelter"]));
|
||||
process_recipe_phase(&mut state);
|
||||
assert_eq!(state.players[0].strategic_ledger.get("iron_ore").copied(), Some(5));
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ pub const END_OF_TURN_PHASES: &[(&str, SimPhaseFn)] = &[
|
|||
("ecology", crate::ecology_phase::process_ecology_phase),
|
||||
("healing", crate::healing::process_healing_phase),
|
||||
("improvement_build", crate::improvement_phase::process_improvement_build_phase),
|
||||
("recipe_refine", crate::recipe_phase::process_recipe_phase),
|
||||
];
|
||||
|
||||
/// Run every registered end-of-turn phase in order.
|
||||
|
|
@ -41,7 +42,10 @@ mod tests {
|
|||
#[test]
|
||||
fn registry_lists_phases_in_documented_order() {
|
||||
let names: Vec<&str> = END_OF_TURN_PHASES.iter().map(|(n, _)| *n).collect();
|
||||
assert_eq!(names, vec!["ecology", "healing", "improvement_build"]);
|
||||
assert_eq!(
|
||||
names,
|
||||
vec!["ecology", "healing", "improvement_build", "recipe_refine"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue