feat(city): ✨ Implement dynamic food consumption scaling to cap late-game population growth sustainably
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
abd7016e88
commit
d161604add
1 changed files with 28 additions and 21 deletions
|
|
@ -99,12 +99,17 @@ impl TileYield {
|
|||
}
|
||||
}
|
||||
|
||||
/// Food consumed per citizen per turn. Tuned to 1.2 (well below Civ5's 2.0)
|
||||
/// so small cities can climb past the pop 2↔3 oscillation observed in iter10.
|
||||
/// With 4-food center: pop 3 needs 3.6, breakeven on center alone (+0.4
|
||||
/// surplus); pop 4 = 4.8, covered by one 1-food tile; pop 6 = 7.2, reachable
|
||||
/// with two decent food tiles. Target: median p0_pop_peak ≥ 7 at T150.
|
||||
pub const FOOD_PER_POP: f64 = 1.2;
|
||||
/// Food consumed per citizen per turn. Lowered from 1.2 → 1.0 so late-game
|
||||
/// cities don't stall against the exponential threshold curve. At pop 20,
|
||||
/// consumption drops 24 → 20, freeing ~4 food/turn for growth. Target:
|
||||
/// median p0_pop_peak ≥ 30 at T300 (Civ5-like capital size).
|
||||
pub const FOOD_PER_POP: f64 = 1.0;
|
||||
|
||||
/// Fraction of the previous growth threshold retained as stored food on
|
||||
/// growth (Civ5-style always-on granary effect). Each new pop starts with a
|
||||
/// head-start toward the next pop, cutting the cumulative food needed to
|
||||
/// reach pop 30 from ~7000 to ~3500 — the dominant lever for pop_peak.
|
||||
pub const GROWTH_FOOD_CARRYOVER: f64 = 0.5;
|
||||
|
||||
/// Base city HP before population scaling. Tuned up from 200 to 260 to
|
||||
/// extend TTV alongside the melee-city-damage fraction in resolver.rs. The
|
||||
|
|
@ -378,14 +383,15 @@ impl City {
|
|||
self.food_stored += surplus;
|
||||
|
||||
if self.food_stored >= self.growth_threshold() {
|
||||
// Growth: population increases, surplus food carries over
|
||||
self.food_stored -= self.growth_threshold();
|
||||
// Growth: pop increases. Civ5-style always-on granary: new pop
|
||||
// starts with GROWTH_FOOD_CARRYOVER * prev_threshold stored food,
|
||||
// halving the cumulative food to reach pop 30. Surplus beyond
|
||||
// the threshold also carries over.
|
||||
let prev_threshold = self.growth_threshold();
|
||||
let surplus_over = (self.food_stored - prev_threshold).max(0.0);
|
||||
self.food_stored = GROWTH_FOOD_CARRYOVER * prev_threshold + surplus_over;
|
||||
self.population += 1;
|
||||
self.recalc_max_hp();
|
||||
// Clamp negative carried food to 0
|
||||
if self.food_stored < 0.0 {
|
||||
self.food_stored = 0.0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -744,9 +750,9 @@ mod tests {
|
|||
let mut city = City::found("Ironhold", (5, 5), true, 1);
|
||||
city.population = 1;
|
||||
// With no tile yields, city center gives 4 food.
|
||||
// Surplus = 4.0 - 1.2*1 = 2.8
|
||||
// Surplus = 4.0 - 1.0*1 = 3.0
|
||||
let surplus = city.get_food_surplus(&[]);
|
||||
assert!((surplus - 2.8).abs() < 1e-9);
|
||||
assert!((surplus - 3.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -759,15 +765,16 @@ mod tests {
|
|||
let ty = vec![
|
||||
TileYield { coord: (6, 5), food: 5.0, ..TileYield::default() },
|
||||
];
|
||||
// Surplus per turn: (4 + 5) - 1.2*1 = 7.8
|
||||
// Surplus per turn: (4 + 5) - 1.0*1 = 8.0
|
||||
// Threshold at pop 1: 15.0
|
||||
// Turn 1: food_stored = 7.8
|
||||
// Turn 1: food_stored = 8.0
|
||||
assert_eq!(city.process_growth(&ty), 0);
|
||||
assert!((city.food_stored - 7.8).abs() < 1e-9);
|
||||
// Turn 2: food_stored = 15.6 >= 15 → grow, carry 0.6
|
||||
assert!((city.food_stored - 8.0).abs() < 1e-9);
|
||||
// Turn 2: food_stored = 16.0 >= 15 → grow. Carryover = 0.5*15 = 7.5
|
||||
// Surplus-over = 16.0 - 15.0 = 1.0. New stored = 7.5 + 1.0 = 8.5
|
||||
assert_eq!(city.process_growth(&ty), 1);
|
||||
assert_eq!(city.population, 2);
|
||||
assert!((city.food_stored - 0.6).abs() < 1e-9);
|
||||
assert!((city.food_stored - 8.5).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -775,8 +782,8 @@ mod tests {
|
|||
let mut city = City::found("Ironhold", (5, 5), true, 1);
|
||||
city.population = 5;
|
||||
// No worked tiles beyond center → yields = 4 food
|
||||
// Consumption = 1.2*5 = 6.0. Surplus = 4 - 6.0 = -2.0
|
||||
// food_stored: 0 + (-2.0) = -2.0 < 0, pop > 1 → starve
|
||||
// Consumption = 1.0*5 = 5.0. Surplus = 4 - 5.0 = -1.0
|
||||
// food_stored: 0 + (-1.0) = -1.0 < 0, pop > 1 → starve
|
||||
let ty: Vec<TileYield> = vec![];
|
||||
assert_eq!(city.process_growth(&ty), -1);
|
||||
assert_eq!(city.population, 4);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue