From 417c8d195b90c5f9f76731eb49572c5556b47e1a Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 25 Jun 2026 19:15:35 -0400 Subject: [PATCH] =?UTF-8?q?refactor(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=8F=9B=EF=B8=8F=20p3-24=20phase=202=20=E2=80=94=20port=20?= =?UTF-8?q?happiness=20aggregation=20GDScript=E2=86=92Rust=20(Rail-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit happiness.gd summed building happiness effects and applied the happiness_per_city × city_count multiply IN GDScript before the GdHappiness call. Moved both into mc-happiness: - HappinessInput gains building_happiness_effects + happiness_per_city_effects (#[serde(default)]); building_happiness_total() does the sum + per-city multiply. calculate_happiness uses it. Legacy building_happiness kept as a back-compat default field. - happiness.gd passes the raw effect lists (no arithmetic); turn_processor_helpers sum_building_effects → collect_building_effects (pure per-building extraction, its only caller was happiness.gd). The luxury-map assembly stays GDScript (tile/ DataLoader extraction; mc-happiness is pure). Verified: 2 new mc-happiness cargo tests (aggregates effects+per-city; back-compat legacy field); mc-happiness 23/0; dylib rebuilt + canonical GUT 747/0 (full happiness.gd path test_happiness_turn -6/-4 unchanged). p3-24 bullet 2 done; stays partial — remaining: climate HP-loss→Rust, orchestration. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../p3-24-rail1-economy-turn-logic-port.md | 21 ++++++-- .../games/age-of-dwarves/data/objectives.json | 12 ++--- .../engine/src/modules/empire/happiness.gd | 17 +++--- .../management/turn_processor_helpers.gd | 19 +++---- src/simulator/crates/mc-happiness/src/pool.rs | 53 +++++++++++++++++-- .../mc-happiness/tests/golden_age_window.rs | 4 ++ 6 files changed, 96 insertions(+), 30 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 d34e5c63..dfd83d43 100644 --- a/.project/objectives/p3-24-rail1-economy-turn-logic-port.md +++ b/.project/objectives/p3-24-rail1-economy-turn-logic-port.md @@ -34,8 +34,13 @@ economy/happiness/event/turn surface. 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. +- [x] Happiness input assembly + computation fully in `mc-happiness`; `happiness.gd` + reduced to a thin bridge. **Done (phase 2):** `HappinessInput` gains + `building_happiness_effects` + `happiness_per_city_effects`; `building_happiness_total()` + does the building-sum + `happiness_per_city × city_count` multiply in Rust. + `happiness.gd` passes raw effect lists (no arithmetic); `sum_building_effects` → + `collect_building_effects` (data extraction). The luxury-map assembly stays + GDScript (tile/DataLoader extraction; mc-happiness is pure). 2 cargo tests; GUT 747/0. - [ ] 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 @@ -51,8 +56,16 @@ building-sum + per-pop + per-mine + percent composition into Rust: 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). +migrated to the raw shape, mc-economy + canonical GUT 747/0. + +**Phase 2 — happiness aggregation ported (bullet 2 done).** Moved happiness.gd's +building-effect sum + `happiness_per_city × city_count` multiply into +`mc-happiness::HappinessInput::building_happiness_total()` (new +`building_happiness_effects` + `happiness_per_city_effects` fields). happiness.gd +passes raw effect lists; `sum_building_effects` → `collect_building_effects` (pure +extraction). `building_happiness` kept as a `#[serde(default)]` legacy field for +back-compat. 2 cargo tests; dylib rebuilt + GUT 747/0. **Remaining phases:** +climate HP-loss→Rust, per-turn 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 a3659f7b..36951deb 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:48:57Z", + "generated_at": "2026-06-25T23:15:35Z", "totals": { - "oos": 31, - "missing": 0, - "in_progress": 0, - "done": 295, - "stub": 0, "partial": 2, + "oos": 31, + "stub": 0, + "missing": 0, + "done": 295, + "in_progress": 0, "total": 328 }, "objectives": [ diff --git a/src/game/engine/src/modules/empire/happiness.gd b/src/game/engine/src/modules/empire/happiness.gd index bc75d2bb..d5f8bcd0 100644 --- a/src/game/engine/src/modules/empire/happiness.gd +++ b/src/game/engine/src/modules/empire/happiness.gd @@ -57,15 +57,15 @@ static func process_turn(player: RefCounted, _game_map: RefCounted) -> void: total_citizens += int(city.population) var units_in_enemy_territory: int = _count_units_in_enemy_territory(player) - var building_happiness: int = TurnProcessorHelpersScript.sum_building_effects( - player, "happiness" + # p3-24: pass RAW per-building effect lists; mc-happiness sums them and applies + # the per-city multiply (happiness_per_city × city_count). No happiness + # arithmetic lives here — Rail-1. + var building_happiness_effects: Array[int] = ( + TurnProcessorHelpersScript.collect_building_effects(player, "happiness") ) - # happiness_per_city (wonder effect) — +N happiness per owned city, applied - # once per building that carries the effect (royal_runestone +1/city etc.). - var per_city_bonus: int = TurnProcessorHelpersScript.sum_building_effects( - player, "happiness_per_city" + var happiness_per_city_effects: Array[int] = ( + TurnProcessorHelpersScript.collect_building_effects(player, "happiness_per_city") ) - building_happiness += per_city_bonus * player.cities.size() var growth_tier: String = player.growth_tier if player.growth_tier != "" else "balanced" # Collect distinct luxury IDs + per-deposit happiness_per_unique_copy values. @@ -77,7 +77,8 @@ static func process_turn(player: RefCounted, _game_map: RefCounted) -> void: "city_count": player.cities.size(), "total_citizens": total_citizens, "units_in_enemy_territory": units_in_enemy_territory, - "building_happiness": building_happiness, + "building_happiness_effects": building_happiness_effects, + "happiness_per_city_effects": happiness_per_city_effects, "owned_luxuries": owned_luxuries, "growth_tier": growth_tier, } diff --git a/src/game/engine/src/modules/management/turn_processor_helpers.gd b/src/game/engine/src/modules/management/turn_processor_helpers.gd index 3326cbbb..cdeb73ae 100644 --- a/src/game/engine/src/modules/management/turn_processor_helpers.gd +++ b/src/game/engine/src/modules/management/turn_processor_helpers.gd @@ -147,13 +147,14 @@ static func process_improvements(player: RefCounted) -> void: # ── Building effect summation ──────────────────────────────────── -static func sum_building_effects(player: RefCounted, effect_type: String) -> int: - ## Sum the numeric `value` of every building effect matching `effect_type` - ## across all of a player's cities. Building roster lives on `city.buildings` - ## (a `PackedStringArray`/`Array[String]` mirroring the Rust-side list). - ## Building data is read from DataLoader.get_building; missing entries are - ## skipped silently so saves referencing legacy ids do not crash. - var total: int = 0 +static func collect_building_effects(player: RefCounted, effect_type: String) -> Array[int]: + ## Collect the numeric `value` of every building effect matching `effect_type` + ## across all of a player's cities — one entry per matching effect, NOT summed. + ## Aggregation (sum, per-city multiply) happens Rust-side in mc-happiness / + ## mc-economy; this is pure data extraction (Rail-1 p3-24). Building roster + ## lives on `city.buildings` (mirrors the Rust-side list); data is read from + ## DataLoader.get_building, missing entries skipped so legacy-id saves don't crash. + var values: Array[int] = [] for city: RefCounted in player.cities: if not city is CityScript: continue @@ -168,8 +169,8 @@ static func sum_building_effects(player: RefCounted, effect_type: String) -> int continue var ed: Dictionary = effect as Dictionary if str(ed.get("type", "")) == effect_type: - total += int(ed.get("value", 0)) - return total + values.append(int(ed.get("value", 0))) + return values # ── Tile yields JSON builder ───────────────────────────────────── diff --git a/src/simulator/crates/mc-happiness/src/pool.rs b/src/simulator/crates/mc-happiness/src/pool.rs index e778b96e..b60fbf67 100644 --- a/src/simulator/crates/mc-happiness/src/pool.rs +++ b/src/simulator/crates/mc-happiness/src/pool.rs @@ -145,8 +145,17 @@ pub struct HappinessInput { pub total_citizens: i32, /// Number of military units in enemy territory. pub units_in_enemy_territory: i32, - /// Total happiness from buildings (already summed by GDScript from DataLoader). + /// Pre-summed flat building happiness. Retained for backward-compat / tests; + /// GDScript now passes 0 and the raw effect lists below instead (p3-24). + #[serde(default)] pub building_happiness: i32, + /// p3-24: per-building flat `happiness` effect VALUES (summed in Rust, not GDScript). + #[serde(default)] + pub building_happiness_effects: Vec, + /// p3-24: per-building `happiness_per_city` effect VALUES — each multiplied by + /// `city_count` and added (e.g. royal_runestone +1/city). Summed in Rust. + #[serde(default)] + pub happiness_per_city_effects: Vec, /// Map of luxury resource ID → `happiness_per_unique_copy` from deposit JSON. /// Each key is a unique luxury type; duplicates are excluded by the caller. /// Value is the per-deposit happiness value; falls back to `LUXURY_HAPPINESS` @@ -194,6 +203,18 @@ pub fn happiness_from_luxuries(owned_luxuries: &BTreeMap, config: & .sum() } +impl HappinessInput { + /// p3-24: total building happiness = legacy pre-summed `building_happiness` + /// + the per-building flat `happiness` effects + per-building + /// `happiness_per_city` × `city_count`. Moves the building-sum and per-city + /// multiply out of GDScript (`happiness.gd`) into the Rust source of truth. + pub fn building_happiness_total(&self) -> i32 { + self.building_happiness + + self.building_happiness_effects.iter().sum::() + + self.happiness_per_city_effects.iter().sum::() * self.city_count + } +} + /// Calculate the full happiness breakdown for a player. pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) -> HappinessBreakdown { let tier = GrowthTier::parse_or_default(&input.growth_tier); @@ -212,7 +233,8 @@ pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) -> let base_unhappiness = city_unhappiness + citizen_unhappiness + war_weariness + ascension_penalty; let luxury_happiness = happiness_from_luxuries(&input.owned_luxuries, config); - let total = (input.building_happiness + luxury_happiness) - base_unhappiness; + let building_happiness = input.building_happiness_total(); + let total = (building_happiness + luxury_happiness) - base_unhappiness; let status = HappinessStatus::from_total(total); HappinessBreakdown { @@ -221,7 +243,7 @@ pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) -> citizen_unhappiness, war_weariness, ascension_penalty, - building_happiness: input.building_happiness, + building_happiness, luxury_happiness, total, status: status.as_str().to_string(), @@ -294,12 +316,33 @@ mod tests { total_citizens: 12, units_in_enemy_territory: 0, building_happiness: 10, + building_happiness_effects: Vec::new(), + happiness_per_city_effects: Vec::new(), // silk=4, gold_ore=4 → luxury_happiness = 8 (matches old flat rate) owned_luxuries: BTreeMap::from([("silk".into(), 4), ("gold_ore".into(), 4)]), growth_tier: "balanced".to_string(), } } + #[test] + fn building_happiness_total_aggregates_effects_and_per_city() { + // p3-24: per-building flat effects summed + per-city effects × city_count, + // all in Rust (moved out of happiness.gd). + let mut input = default_input(); + input.building_happiness = 0; + input.city_count = 4; + input.building_happiness_effects = vec![2, 3]; // flat → 5 + input.happiness_per_city_effects = vec![1, 1]; // (1+1)/city × 4 → 8 + assert_eq!(input.building_happiness_total(), 5 + 8); + } + + #[test] + fn building_happiness_total_backward_compat_legacy_field() { + // Legacy pre-summed building_happiness still honored when no lists given. + let input = default_input(); // building_happiness: 10, no effect lists + assert_eq!(input.building_happiness_total(), 10); + } + #[test] fn balanced_tier_basic_calculation() { let input = default_input(); @@ -475,6 +518,8 @@ mod tests { total_citizens: 10, units_in_enemy_territory: 0, building_happiness: 0, + building_happiness_effects: Vec::new(), + happiness_per_city_effects: Vec::new(), owned_luxuries: BTreeMap::new(), growth_tier: String::new(), }; @@ -609,6 +654,8 @@ mod tests { total_citizens: 4, units_in_enemy_territory: 0, building_happiness: 0, + building_happiness_effects: Vec::new(), + happiness_per_city_effects: Vec::new(), owned_luxuries: BTreeMap::new(), growth_tier: "balanced".to_string(), }; diff --git a/src/simulator/crates/mc-happiness/tests/golden_age_window.rs b/src/simulator/crates/mc-happiness/tests/golden_age_window.rs index 962ca3a3..637e8b45 100644 --- a/src/simulator/crates/mc-happiness/tests/golden_age_window.rs +++ b/src/simulator/crates/mc-happiness/tests/golden_age_window.rs @@ -22,6 +22,8 @@ fn content_input() -> HappinessInput { total_citizens: 0, units_in_enemy_territory: 0, building_happiness: 0, + building_happiness_effects: Vec::new(), + happiness_per_city_effects: Vec::new(), owned_luxuries: BTreeMap::new(), growth_tier: "balanced".to_string(), } @@ -34,6 +36,8 @@ fn happy_input() -> HappinessInput { total_citizens: 2, units_in_enemy_territory: 0, building_happiness: 10, + building_happiness_effects: Vec::new(), + happiness_per_city_effects: Vec::new(), owned_luxuries: BTreeMap::new(), growth_tier: "balanced".to_string(), }