From a330704fe4e3db1b104257be94d2e7ac3e4990c6 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 25 Jun 2026 18:48:57 -0400 Subject: [PATCH] =?UTF-8?q?refactor(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=8F=9B=EF=B8=8F=20p3-24=20phase=201=20=E2=80=94=20port=20?= =?UTF-8?q?gold=20aggregation=20GDScript=E2=86=92Rust=20(Rail-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit economy.gd:65-79 computed gold IN GDScript (building-effect sum, gold-per-pop multiply, gold-from-mines loop, percent sum) before the GdEconomy call — violating "GDScript is presentation only". Moved all of it into mc-economy: - New CityGoldRaw (per-building effect VALUES + population + mine_count) and aggregate_city_gold() that does the building-sum + per-pop×pop + per-mine×mines + percent composition. Pure arithmetic, cargo-tested. - GdEconomy FFI now deserializes the raw shape and aggregates before process_gold. - economy.gd reduced to data extraction: _collect_effect_ints/_floats (no summing) + mine count; zero gold arithmetic. gdlint clean. Verified: 3 new mc-economy cargo tests (sums/per-pop+per-mine/percent+e2e); GdEconomy bridge GUT tests migrated to the raw shape; mc-economy green; dylib rebuilt + canonical GUT 747/0. p3-24 bullet 1 done; stays partial — remaining phases: happiness assembly→ mc-happiness, climate HP-loss→Rust, orchestration (stretch). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../p3-24-rail1-economy-turn-logic-port.md | 22 ++++- .../games/age-of-dwarves/data/objectives.json | 10 +- src/game/engine/src/modules/empire/economy.gd | 55 ++++++----- .../tests/unit/empire/test_economy_bridge.gd | 14 +-- src/simulator/api-gdext/src/lib.rs | 7 +- src/simulator/crates/mc-economy/src/gold.rs | 94 +++++++++++++++++++ src/simulator/crates/mc-economy/src/lib.rs | 4 +- 7 files changed, 165 insertions(+), 41 deletions(-) diff --git a/.project/objectives/p3-24-rail1-economy-turn-logic-port.md b/.project/objectives/p3-24-rail1-economy-turn-logic-port.md index 1cb18c1f..d34e5c63 100644 --- a/.project/objectives/p3-24-rail1-economy-turn-logic-port.md +++ b/.project/objectives/p3-24-rail1-economy-turn-logic-port.md @@ -27,16 +27,32 @@ economy/happiness/event/turn surface. ## Acceptance -- [ ] Gold income/expense aggregation (incl. per-pop, per-mine, building sums) +- [x] Gold income/expense aggregation (incl. per-pop, per-mine, building sums) computed entirely in `mc-economy` from state; `economy.gd` only passes state + - applies the result. + applies the result. **Done (phase 1):** `mc-economy::aggregate_city_gold` + + `CityGoldRaw` do the building-sum, gold-per-pop×pop, gold-from-mines×mines, and + gold_percent composition; the `GdEconomy` FFI deserializes raw per-building + effect lists and aggregates; `economy.gd` reduced to data extraction + (`_collect_effect_ints/_floats`, mine count) — no gold arithmetic. - [ ] Happiness input assembly + computation fully in `mc-happiness`; `happiness.gd` reduced to a thin bridge. - [ ] Climate-effect damage application (unit HP loss) owned by Rust; GDScript renders/animates only. - [ ] (Stretch) per-turn orchestration moved behind a Rust turn driver so the GDScript turn loop is a thin pump (overlaps the broader pathfinder/turn port). -- [ ] No regression: cargo + canonical GUT suite green. +- [~] No regression: cargo + canonical GUT suite green — green for phase 1 + (mc-economy 3 new aggregate tests + GUT 747/0); re-affirm each phase. + +## Progress (2026-06-25) + +**Phase 1 — gold aggregation ported (bullet 1 done).** Moved economy.gd:65-79's +building-sum + per-pop + per-mine + percent composition into Rust: +`mc-economy::aggregate_city_gold(CityGoldRaw) -> CityGoldInput`. The GdEconomy FFI +now deserializes the raw per-building effect shape and aggregates before +`process_gold`; economy.gd `_build_cities_json` emits raw effect lists + population ++ mine_count (pure extraction). Verified: 3 cargo tests, GdEconomy bridge GUT tests +migrated to the raw shape, mc-economy + canonical GUT 747/0. **Remaining phases:** +happiness assembly→mc-happiness, climate HP-loss→Rust, orchestration (stretch). ## Code sites diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index e1383ac3..a3659f7b 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-25T22:18:25Z", + "generated_at": "2026-06-25T22:48:57Z", "totals": { - "done": 295, - "stub": 0, - "in_progress": 0, - "partial": 2, "oos": 31, "missing": 0, + "in_progress": 0, + "done": 295, + "stub": 0, + "partial": 2, "total": 328 }, "objectives": [ diff --git a/src/game/engine/src/modules/empire/economy.gd b/src/game/engine/src/modules/empire/economy.gd index 6f87453d..5d89e04d 100644 --- a/src/game/engine/src/modules/empire/economy.gd +++ b/src/game/engine/src/modules/empire/economy.gd @@ -54,6 +54,10 @@ static func process_turn(player: RefCounted, game_map: RefCounted) -> void: static func _build_cities_json(player: RefCounted, game_map: RefCounted) -> String: + ## Gather RAW per-city gold inputs (p3-24, Rail-1): per-building effect VALUES + ## plus population + mine_count. All aggregation — building sums, gold-per-pop, + ## gold-from-mines, percent — happens in `mc-economy::aggregate_city_gold`. No + ## gold arithmetic lives here; this is pure data extraction. var cities: Array = [] for city_ref: RefCounted in player.cities: if not city_ref is CityScript: @@ -62,26 +66,23 @@ static func _build_cities_json(player: RefCounted, game_map: RefCounted) -> Stri var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map) var yields: Dictionary = c.get_yields(tile_json) - var building_gold: int = _sum_effect_int(c, "gold") - # gold_per_city_pop — flat gold per pop point (wonder effect). - var gold_per_pop: int = _sum_effect_int(c, "gold_per_city_pop") - if gold_per_pop > 0: - building_gold += gold_per_pop * int(c.population) - # gold_from_mines — +N gold per mine improvement in owned tiles. - var gold_mine: int = _sum_effect_int(c, "gold_from_mines") - if gold_mine > 0: - for tile_pos: Vector2i in c.owned_tiles: - var tile: Resource = game_map.get_tile(tile_pos) - if tile != null and tile.improvement == "mine": - building_gold += gold_mine + # Count mine improvements in owned tiles (data extraction; the + # gold-from-mines multiply happens in Rust). + var mine_count: int = 0 + for tile_pos: Vector2i in c.owned_tiles: + var tile: Resource = game_map.get_tile(tile_pos) + if tile != null and tile.improvement == "mine": + mine_count += 1 - var tile_gold: int = int(yields.get("gold", 0)) - var gold_pct: float = _sum_effect_float(c, "gold_percent") cities.append( { - "building_gold": building_gold, - "building_gold_percent": gold_pct, - "tile_gold": tile_gold, + "building_gold_effects": _collect_effect_ints(c, "gold"), + "gold_percent_effects": _collect_effect_floats(c, "gold_percent"), + "gold_per_pop_effects": _collect_effect_ints(c, "gold_per_city_pop"), + "gold_from_mines_effects": _collect_effect_ints(c, "gold_from_mines"), + "population": int(c.population), + "mine_count": mine_count, + "tile_gold": int(yields.get("gold", 0)), "building_upkeep": 0, } ) @@ -155,25 +156,29 @@ static func _disband_cheapest(player: RefCounted, count: int) -> void: remaining -= 1 -static func _sum_effect_int(city: CityScript, effect_type: String) -> int: - var total: int = 0 +static func _collect_effect_ints(city: CityScript, effect_type: String) -> Array[int]: + ## Per-building integer VALUES of `effect_type` — one entry per contributing + ## building, NOT summed (mc-economy aggregates; Rail-1 p3-24). + var values: Array[int] = [] for building_id: String in city.buildings: var bdata: Dictionary = DataLoader.get_building(building_id) if bdata.is_empty(): continue for effect: Dictionary in bdata.get("effects", []): if effect.get("type", "") == effect_type: - total += int(effect.get("value", 0)) - return total + values.append(int(effect.get("value", 0))) + return values -static func _sum_effect_float(city: CityScript, effect_type: String) -> float: - var total: float = 0.0 +static func _collect_effect_floats(city: CityScript, effect_type: String) -> Array[float]: + ## Per-building float VALUES of `effect_type` — one entry per contributing + ## building, NOT summed (mc-economy aggregates; Rail-1 p3-24). + var values: Array[float] = [] for building_id: String in city.buildings: var bdata: Dictionary = DataLoader.get_building(building_id) if bdata.is_empty(): continue for effect: Dictionary in bdata.get("effects", []): if effect.get("type", "") == effect_type: - total += float(effect.get("value", 0)) - return total + values.append(float(effect.get("value", 0))) + return values diff --git a/src/game/engine/tests/unit/empire/test_economy_bridge.gd b/src/game/engine/tests/unit/empire/test_economy_bridge.gd index d06ec20f..bc821346 100644 --- a/src/game/engine/tests/unit/empire/test_economy_bridge.gd +++ b/src/game/engine/tests/unit/empire/test_economy_bridge.gd @@ -19,10 +19,12 @@ func _marketplace_plus_13_inputs() -> Dictionary: ## The t7b bench test (`t7b_building_gold_table_adds_to_income`) expects 13; ## the discrepancy is because the bench test passes `tile_gold=0` and folds ## the 10 into `building_gold`. We mirror that here for parity. + # p3-24: GdEconomy now consumes the RAW per-building effect shape; mc-economy + # aggregates (building sums + per-pop + per-mine + percent). var cities: Array = [ { - "building_gold": 13, # 10 base + 3 marketplace flat - "building_gold_percent": 0.25, + "building_gold_effects": [13], # 10 base + 3 marketplace flat + "gold_percent_effects": [0.25], "tile_gold": 0, "building_upkeep": 0, } @@ -68,8 +70,8 @@ func test_rust_bridge_matches_inline_gdscript_formula() -> void: # income = 16; upkeep = 2 units * 1 = 2; net = 14 var cities: Array = [ { - "building_gold": 3, - "building_gold_percent": 0.25, + "building_gold_effects": [3], + "gold_percent_effects": [0.25], "tile_gold": 10, "building_upkeep": 0, } @@ -101,8 +103,8 @@ func test_golden_age_applies_20_percent_bonus_post_process_gold() -> void: return var cities: Array = [ { - "building_gold": 10, - "building_gold_percent": 0.0, + "building_gold_effects": [10], + "gold_percent_effects": [], "tile_gold": 0, "building_upkeep": 0, } diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index 1bc70aa5..794894a4 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -7857,7 +7857,10 @@ impl GdEconomy { units_json: GString, params_json: GString, ) -> Dictionary { - let cities: Vec = + // p3-24: GDScript now passes RAW per-building effect lists; the + // building-sum + per-pop + per-mine + percent aggregation happens in + // mc-economy (aggregate_city_gold), not economy.gd. Rail-1. + let raw_cities: Vec = match serde_json::from_str(&cities_json.to_string()) { Ok(v) => v, Err(e) => { @@ -7865,6 +7868,8 @@ impl GdEconomy { Vec::new() } }; + let cities: Vec = + raw_cities.iter().map(mc_economy::aggregate_city_gold).collect(); let units: Vec = match serde_json::from_str(&units_json.to_string()) { Ok(v) => v, diff --git a/src/simulator/crates/mc-economy/src/gold.rs b/src/simulator/crates/mc-economy/src/gold.rs index 35c30c45..a9ffb345 100644 --- a/src/simulator/crates/mc-economy/src/gold.rs +++ b/src/simulator/crates/mc-economy/src/gold.rs @@ -101,6 +101,57 @@ pub fn process_gold( } } +/// Raw per-city gold inputs gathered from game state by the caller (p3-24). +/// +/// Carries per-building effect VALUES (not pre-summed) plus the multipliers, so +/// the building-sum + per-pop + per-mine + percent composition happens in Rust +/// ([`aggregate_city_gold`]) rather than GDScript — Rail-1. The caller only +/// extracts data from JSON/entities; no gold arithmetic lives presentation-side. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CityGoldRaw { + /// Each contributing building's flat `gold` effect. + #[serde(default)] + pub building_gold_effects: Vec, + /// Each building's `gold_percent` effect (fraction, e.g. 0.25 = +25%). + #[serde(default)] + pub gold_percent_effects: Vec, + /// Each building's `gold_per_city_pop` effect — multiplied by `population`. + #[serde(default)] + pub gold_per_pop_effects: Vec, + /// Each building's `gold_from_mines` effect — multiplied by `mine_count`. + #[serde(default)] + pub gold_from_mines_effects: Vec, + /// City population (for `gold_per_city_pop`). + #[serde(default)] + pub population: i32, + /// Count of mine improvements in the city's owned tiles (for `gold_from_mines`). + #[serde(default)] + pub mine_count: i32, + /// Gold from tile yields (already aggregated by the yields system). + #[serde(default)] + pub tile_gold: i32, + /// Total building upkeep in this city. + #[serde(default)] + pub building_upkeep: i32, +} + +/// Aggregate raw per-city inputs into the [`CityGoldInput`] consumed by +/// [`process_gold`] (p3-24, Rail-1). Pure arithmetic — moves the building-sum, +/// gold-per-pop multiply, gold-from-mines multiply, and percent composition out +/// of `economy.gd` into the Rust source of truth. +pub fn aggregate_city_gold(raw: &CityGoldRaw) -> CityGoldInput { + let flat: i32 = raw.building_gold_effects.iter().sum(); + let per_pop: i32 = raw.gold_per_pop_effects.iter().sum::() * raw.population; + let per_mine: i32 = raw.gold_from_mines_effects.iter().sum::() * raw.mine_count; + let percent: f64 = raw.gold_percent_effects.iter().sum(); + CityGoldInput { + building_gold: flat + per_pop + per_mine, + building_gold_percent: percent, + tile_gold: raw.tile_gold, + building_upkeep: raw.building_upkeep, + } +} + #[cfg(test)] mod tests { use super::*; @@ -218,4 +269,47 @@ mod tests { let result = process_gold(&cities, &units); assert_eq!(result.gold_per_turn, result.net_gold); } + + // ── p3-24: gold aggregation moved from GDScript into aggregate_city_gold ── + + #[test] + fn aggregate_sums_building_gold_effects() { + let raw = CityGoldRaw { + building_gold_effects: vec![2, 3, 5], + tile_gold: 4, + ..Default::default() + }; + let agg = aggregate_city_gold(&raw); + assert_eq!(agg.building_gold, 10, "2+3+5 summed in Rust"); + assert_eq!(agg.tile_gold, 4); + } + + #[test] + fn aggregate_applies_per_pop_and_per_mine() { + let raw = CityGoldRaw { + building_gold_effects: vec![1], + gold_per_pop_effects: vec![2], // 2 gold / pop + gold_from_mines_effects: vec![3], // 3 gold / mine + population: 4, // → +8 + mine_count: 2, // → +6 + ..Default::default() + }; + let agg = aggregate_city_gold(&raw); + assert_eq!(agg.building_gold, 1 + 8 + 6, "flat + per_pop*pop + per_mine*mines"); + } + + #[test] + fn aggregate_sums_percent_and_feeds_process_gold() { + let raw = CityGoldRaw { + building_gold_effects: vec![2], + gold_percent_effects: vec![0.10, 0.15], // +25% + tile_gold: 4, + ..Default::default() + }; + let agg = aggregate_city_gold(&raw); + assert!((agg.building_gold_percent - 0.25).abs() < 1e-9); + // End-to-end matches percentage_bonus_applied_to_base: base 6, +25% → +1 → 7. + let result = process_gold(&[agg], &[]); + assert_eq!(result.gold_income, 7); + } } diff --git a/src/simulator/crates/mc-economy/src/lib.rs b/src/simulator/crates/mc-economy/src/lib.rs index 001ab34c..1a05727c 100644 --- a/src/simulator/crates/mc-economy/src/lib.rs +++ b/src/simulator/crates/mc-economy/src/lib.rs @@ -13,7 +13,9 @@ pub use anarchy::{ }; pub use cascade::{emit as cascade_emit, CascadeConfig}; pub use city_yield::{compute as compute_city_yield, CityYield}; -pub use gold::{process_gold, CityGoldInput, GoldResult, UnitMaintenanceInput}; +pub use gold::{ + aggregate_city_gold, process_gold, CityGoldInput, CityGoldRaw, GoldResult, UnitMaintenanceInput, +}; pub use inequality::{amplified as inequality_amplified, compute as inequality_compute, coefficient_of_variation, InequalityStat}; pub use stockpile::{Stockpile, StockpileError}; pub use treasury::{AddOutcome, Treasury, TreasuryError, TreasuryItem, UnitId, TREASURY_SOFT_CAP};