feat(simulator): Add logic to account for new collectible yield channels in the happiness pool

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-16 22:02:36 -07:00
parent 61a49f3fed
commit aee31e286f

View file

@ -1,6 +1,7 @@
//! Happiness pool calculation, racial growth tiers, and Golden Age logic.
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
// ── Constants (match GDScript Happiness class) ─────────────────────────
@ -142,8 +143,9 @@ pub struct HappinessInput {
pub ascension_active: bool,
/// Total happiness from buildings (already summed by GDScript from DataLoader).
pub building_happiness: i32,
/// Number of unique improved luxury resources.
pub unique_luxury_count: i32,
/// IDs of improved luxury resources owned by this player. Duplicates are
/// ignored — only unique luxury types grant happiness.
pub owned_luxuries: BTreeSet<String>,
/// Racial growth tier string (e.g., "balanced", "expansionist").
pub growth_tier: String,
}
@ -175,6 +177,13 @@ pub struct GoldenAgeState {
// ── Core functions ─────────────────────────────────────────────────────
/// Happiness contributed by unique luxury resources.
/// Each distinct luxury ID contributes `config.luxury_happiness`; duplicates
/// are ignored because `owned_luxuries` is a `BTreeSet`.
pub fn happiness_from_luxuries(owned_luxuries: &BTreeSet<String>, config: &HappinessConfig) -> i32 {
owned_luxuries.len() as i32 * config.luxury_happiness
}
/// 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);
@ -193,7 +202,7 @@ pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) ->
};
let base_unhappiness = city_unhappiness + citizen_unhappiness + war_weariness + ascension_penalty;
let luxury_happiness = input.unique_luxury_count * config.luxury_happiness;
let luxury_happiness = happiness_from_luxuries(&input.owned_luxuries, config);
let total = (input.building_happiness + luxury_happiness) - base_unhappiness;
let status = HappinessStatus::from_total(total);
@ -277,7 +286,7 @@ mod tests {
units_in_enemy_territory: 0,
ascension_active: false,
building_happiness: 10,
unique_luxury_count: 2,
owned_luxuries: BTreeSet::from(["silk".into(), "gold_ore".into()]),
growth_tier: "balanced".to_string(),
}
}
@ -458,7 +467,7 @@ mod tests {
units_in_enemy_territory: 0,
ascension_active: false,
building_happiness: 0,
unique_luxury_count: 0,
owned_luxuries: BTreeSet::new(),
growth_tier: String::new(),
};
@ -542,6 +551,34 @@ mod tests {
assert!((get_combat_modifier(5) - 1.0).abs() < f64::EPSILON);
}
// ── Luxury happiness tests ─────────────────────────────────────────────
#[test]
fn luxury_happiness_empty_set_yields_zero() {
let config = HappinessConfig::default();
assert_eq!(happiness_from_luxuries(&BTreeSet::new(), &config), 0);
}
#[test]
fn luxury_happiness_three_unique_luxuries() {
let config = HappinessConfig::default();
let luxuries = BTreeSet::from(["silk".into(), "amber".into(), "deepstone".into()]);
// 3 unique * LUXURY_HAPPINESS(4) = 12
assert_eq!(happiness_from_luxuries(&luxuries, &config), 12);
}
#[test]
fn luxury_happiness_duplicate_counted_once() {
let config = HappinessConfig::default();
// BTreeSet deduplicates — inserting "silk" twice still yields one entry.
let mut luxuries = BTreeSet::new();
luxuries.insert("silk".to_string());
luxuries.insert("silk".to_string());
luxuries.insert("amber".to_string());
// 2 unique * 4 = 8, not 3 * 4 = 12
assert_eq!(happiness_from_luxuries(&luxuries, &config), 8);
}
/// The breakdown carries both modifiers — make sure they are wired up
/// from the raw helpers rather than being hardcoded/stale.
#[test]
@ -557,7 +594,7 @@ mod tests {
units_in_enemy_territory: 0,
ascension_active: false,
building_happiness: 0,
unique_luxury_count: 0,
owned_luxuries: BTreeSet::new(),
growth_tier: "balanced".to_string(),
};
let result = calculate_happiness(&input, &config);