From d161604addba8d7271b2cd703877abd48d8a729a Mon Sep 17 00:00:00 2001 From: autocommit Date: Thu, 16 Apr 2026 16:24:15 -0700 Subject: [PATCH] =?UTF-8?q?feat(city):=20=E2=9C=A8=20Implement=20dynamic?= =?UTF-8?q?=20food=20consumption=20scaling=20to=20cap=20late-game=20popula?= =?UTF-8?q?tion=20growth=20sustainably?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/simulator/crates/mc-city/src/city.rs | 49 ++++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/simulator/crates/mc-city/src/city.rs b/src/simulator/crates/mc-city/src/city.rs index 1198cccd..86dcb10d 100644 --- a/src/simulator/crates/mc-city/src/city.rs +++ b/src/simulator/crates/mc-city/src/city.rs @@ -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 = vec![]; assert_eq!(city.process_growth(&ty), -1); assert_eq!(city.population, 4);