refactor(@projects/@magic-civilization): 🏛️ p3-24 phase 1 — port gold aggregation GDScript→Rust (Rail-1)
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) <noreply@anthropic.com>
This commit is contained in:
parent
d45ba32a3a
commit
a330704fe4
7 changed files with 165 additions and 41 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7857,7 +7857,10 @@ impl GdEconomy {
|
|||
units_json: GString,
|
||||
params_json: GString,
|
||||
) -> Dictionary {
|
||||
let cities: Vec<mc_economy::CityGoldInput> =
|
||||
// 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<mc_economy::CityGoldRaw> =
|
||||
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<mc_economy::CityGoldInput> =
|
||||
raw_cities.iter().map(mc_economy::aggregate_city_gold).collect();
|
||||
let units: Vec<mc_economy::UnitMaintenanceInput> =
|
||||
match serde_json::from_str(&units_json.to_string()) {
|
||||
Ok(v) => v,
|
||||
|
|
|
|||
|
|
@ -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<i32>,
|
||||
/// Each building's `gold_percent` effect (fraction, e.g. 0.25 = +25%).
|
||||
#[serde(default)]
|
||||
pub gold_percent_effects: Vec<f64>,
|
||||
/// Each building's `gold_per_city_pop` effect — multiplied by `population`.
|
||||
#[serde(default)]
|
||||
pub gold_per_pop_effects: Vec<i32>,
|
||||
/// Each building's `gold_from_mines` effect — multiplied by `mine_count`.
|
||||
#[serde(default)]
|
||||
pub gold_from_mines_effects: Vec<i32>,
|
||||
/// 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::<i32>() * raw.population;
|
||||
let per_mine: i32 = raw.gold_from_mines_effects.iter().sum::<i32>() * 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue