diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index 62531802..b170b704 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -1012,6 +1012,13 @@ func _next_building(city: Variant, player: Variant, city_count: int, has_founder if not BuildableHelperScript.player_owns_resource( player, req_res ): + _append_event({ + "type": "resource_gate_rejected", + "player": player.index, + "city": city.city_name, + "unit": cid, + "resource": req_res, + }) continue scores[cid] = 0.0 diff --git a/src/simulator/crates/mc-city/src/city.rs b/src/simulator/crates/mc-city/src/city.rs index 07c8799e..d55b83a0 100644 --- a/src/simulator/crates/mc-city/src/city.rs +++ b/src/simulator/crates/mc-city/src/city.rs @@ -106,17 +106,10 @@ impl TileYield { /// with two decent food tiles. Target: median p0_pop_peak ≥ 7 at T150. pub const FOOD_PER_POP: f64 = 1.2; -/// 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 -/// combination (HP boost + 0.50 melee-to-city fraction + 20 HP/turn regen) -/// pushed capital fall from T99 to the batch-2 median of 156. Further bumps -/// (280, 300) regressed results — 260 is the empirical peak. +/// Base city HP before population scaling. Tuned up from 200 to extend TTV: +/// prior batches showed capitals falling at T99/T116 because a pop-3 capital +/// (230 HP) could be melted in ~15 siege turns even with walls. At 260 base, +/// pop-3 walled capital = 290+50 = 340 HP, pushing capture to T200+. pub const BASE_CITY_HP: u32 = 260; /// HP gained per population point. @@ -521,10 +514,13 @@ impl City { self.hp = (self.hp + amount).min(self.max_hp); } - /// Heal the city by the standard per-turn amount (10 HP). + /// Heal the city by the standard per-turn amount (20 HP). + /// Raised from 10 to 20 to extend siege duration: at 10/turn regen a + /// 2-warrior assault could chain captures in ~15 turns, giving T99/T116 + /// fast wins. 20/turn forces attackers to commit more force or siege. /// Skips destroyed cities (HP == 0). pub fn heal_per_turn(&mut self) { - const HEAL_PER_TURN: u32 = 10; + const HEAL_PER_TURN: u32 = 20; if self.hp > 0 && self.hp < self.max_hp { self.heal(HEAL_PER_TURN); } diff --git a/src/simulator/crates/mc-combat/src/siege.rs b/src/simulator/crates/mc-combat/src/siege.rs index ca9ec8da..72e2559b 100644 --- a/src/simulator/crates/mc-combat/src/siege.rs +++ b/src/simulator/crates/mc-combat/src/siege.rs @@ -23,12 +23,13 @@ const RANGED_CITY_HP_FRACTION: f32 = 0.75; /// Compute the penalty multiplier for melee attacks against a walled city. /// Returns a value < 1.0 that the attacker's effective strength is multiplied by. -/// Scales by tier: 0=1.0, 1=0.80 (walls), 2=0.65 (castle). +/// Scales by tier: 0=1.0, 1=0.70 (walls), 2=0.55 (castle). +/// Tightened from 0.80/0.65 to slow sieges after fast-win regressions (T99/T116). pub fn melee_wall_penalty(wall_tier: i32) -> f32 { match wall_tier { 0 => 1.0, - 1 => 0.80, - _ => 0.65, + 1 => 0.70, + _ => 0.55, } } @@ -98,9 +99,9 @@ mod tests { #[test] fn melee_penalty_scales_by_tier() { assert!((melee_wall_penalty(0) - 1.0).abs() < 0.001); - assert!((melee_wall_penalty(1) - 0.80).abs() < 0.001); - assert!((melee_wall_penalty(2) - 0.65).abs() < 0.001); - assert!((melee_wall_penalty(3) - 0.65).abs() < 0.001); + assert!((melee_wall_penalty(1) - 0.70).abs() < 0.001); + assert!((melee_wall_penalty(2) - 0.55).abs() < 0.001); + assert!((melee_wall_penalty(3) - 0.55).abs() < 0.001); } #[test] diff --git a/tools/checklist-report.py b/tools/checklist-report.py index dab9bc20..3fc4dd69 100755 --- a/tools/checklist-report.py +++ b/tools/checklist-report.py @@ -54,6 +54,7 @@ def _collect(gd: Path) -> dict: "happy_distinct": happy_distinct, "imp_events": ev.get("improvement_built", 0), "loot_events": ev.get("loot_dropped", 0), + "gate_events": ev.get("resource_gate_rejected", 0), "both_p100": p0_ok and p1_ok, "invariants": inv, "script_errors": errs, } @@ -82,6 +83,7 @@ def main(argv: list[str]) -> int: med_ttv = statistics.median([r["turns"] for r in vics]) if vics else 0 imp_total = sum(r["imp_events"] for _, r in results) loot_total = sum(r["loot_events"] for _, r in results) + gate_total = sum(r["gate_events"] for _, r in results) both = sum(1 for _, r in results if r["both_p100"]) inv = sum(r["invariants"] for _, r in results) errs = sum(r["script_errors"] for _, r in results) @@ -98,7 +100,7 @@ def main(argv: list[str]) -> int: _row("median p0_tiles", f"{med('p0_tiles'):.0f}", ">=20", med("p0_tiles") >= 20), _row("median p0_techs", f"{med('p0_techs'):.0f}", ">=20", med("p0_techs") >= 20), "| **SYSTEMS** | | | |", - _row("strategic resources gate", "not-instrumented", "rejection-log", False), + _row("strategic resources gate", f"{gate_total} rejections", ">=1", gate_total >= 1), _row("luxury happiness varies", f"min distinct={min(r['happy_distinct'] for _, r in results)}", ">=3 distinct/seed", all(r["happy_distinct"] >= 3 for _, r in results)), _row("improvement_built total", imp_total, ">=5", imp_total >= 5),