From c81744b01e8963fed39d710c6538917347969daf Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 18 Apr 2026 20:24:31 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20iron-ore=20density=20strategy=20objective?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../p0-40-iron-ore-resource-density.md | 59 +++++++++++++++++++ .../crates/mc-ai/src/tactical/thresholds.rs | 15 ++--- 2 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 .project/objectives/p0-40-iron-ore-resource-density.md diff --git a/.project/objectives/p0-40-iron-ore-resource-density.md b/.project/objectives/p0-40-iron-ore-resource-density.md new file mode 100644 index 00000000..66b671e5 --- /dev/null +++ b/.project/objectives/p0-40-iron-ore-resource-density.md @@ -0,0 +1,59 @@ +--- +id: p0-40 +title: Iron-ore strategic resource density — unblock tier 3-6 unit chain +priority: p0 +status: stub +scope: game1 +owner: shipwright +updated_at: 2026-04-18 +evidence: + - public/games/age-of-dwarves/data/resources.json + - public/games/age-of-dwarves/data/units/cavalry.json + - public/games/age-of-dwarves/data/units/ironwarden.json + - .local/iter/apricot-20260418_194533/ +--- + +## Summary + +Warcouncil filed 2026-04-18 after p0-39 (AI tier-progression) unlocked tier-2 units. Post-p0-39 smoke batch (`.local/iter/apricot-20260418_194533/`) shows pikemen (tier 2, tech=bronze_working, no resource) building reliably (107 in seed 2, 83 in seed 3), but no tier 3+ unit (cavalry, ironwarden, forge_titan, mithril_vanguard) ever gets built. + +Root cause is NOT tactical AI: the p0-39 `_best_melee_for_player` helper correctly checks `requires_resource` and filters cavalry (and thus downstream tier 4+ units that also gate on iron_ore) when the player owns no iron_ore tile. Empirically, 10/10 seeds in the smoke batch have player 0 with zero iron_ore ownership at T300. + +Iron ore density in current map gen is too low for tier 3+ unit emergence. Fix is either (a) bias map gen toward iron_ore resource placement OR (b) drop the `requires_resource` gate on tier 3 units that previously used it as a "forbidden chokepoint" balance lever. + +## Acceptance + +- ✗ **Iron-ore frequency audit** against `public/games/age-of-dwarves/data/resources.json` — confirm current `frequency` / `placement` rules and document expected tiles-per-player at `duel` (2p) / `small` (4p) map sizes. +- ✗ **Map-gen tune** — raise iron_ore availability to target: **median player owns ≥1 iron_ore tile by turn 50** on the `duel` map size. Implementation in `tools/` map-gen scripts OR `mc-map` resource placement. +- ✗ **Smoke batch** (10 seeds T300 smoke, post-tune) shows `median_peak_unit_tier ≥ 3` across seeds. Each player has reached cavalry or higher before game end in majority of seeds. +- ✗ **No regression** on p0-39 gates: pikemen still dominant in early-game builds when bronze_working lands but iron_working doesn't. +- ✗ **p0-01 re-test** after p0-40 lands — measure whether tier_peak median rises past 4.0 (current post-p0-37+p0-39 baseline). + +## Why P0 + +Without iron_ore availability, tier 3-6 units are inaccessible by design. Every p0-01 state-at-end gate (`peak_unit_tier ≥ 6`, `tier_peak_gap ≤ 2`, `wonder_count ≥ 1 per player in ≥5/10 games`) is structurally gated by this. p0-22 ultimate_stress (5-clan huge map) likewise — clans can't diverge on tier 3+ if nobody can build them. p0-02 era-divergence (production vs expansion clan pairs) similarly gated. + +## Fix direction (non-prescriptive — shipwright picks) + +1. **Resource frequency bump**: raise iron_ore placement weight in `resources.json` or map-gen so expected player ownership hits ≥1 tile by T50. +2. **Clustered placement**: prefer spawning iron_ore near starting positions to guarantee at least one per player, regardless of map size. +3. **Gate relaxation**: drop `requires_resource: iron_ore` from cavalry/ironwarden (tier 3+ still tech-gated). Loses some strategic-resource flavor but unblocks the ladder immediately. + +## Non-goals + +- Re-adding tier-3+ units to `ai_personalities.json` clan preferences — existing p0-37 axis-driven selection handles this once the units are buildable. +- `horses` / other strategic resources — scope is iron_ore for the tier-3-6 chain only. Other resources can be audited if similar patterns emerge. + +## Depends on + +- None (this is the blocker for others). + +## Blocks + +- **p0-01** MCTS wiring — `peak_unit_tier ≥ 6 in ≥ 7/10 games` gate. +- **p0-22** Ultimate AI stress test — matchup-grid + huge-map median-turn gates need post-tier-1 armies. +- **p0-02** era-divergence gate under reframed p0-01 framework. + +## Related + +- **p0-39** AI tier-progression (done) — lifted tier-1 ceiling to tier-2; this objective lifts to tier-3+. diff --git a/src/simulator/crates/mc-ai/src/tactical/thresholds.rs b/src/simulator/crates/mc-ai/src/tactical/thresholds.rs index 825b77e9..b8b455a0 100644 --- a/src/simulator/crates/mc-ai/src/tactical/thresholds.rs +++ b/src/simulator/crates/mc-ai/src/tactical/thresholds.rs @@ -75,10 +75,11 @@ pub fn derived_defense(axes: &BTreeMap) -> i32 { /// to capital-assault commitment. Aggressive clans commit earlier; cautious /// clans wait for real superiority. /// -/// Range: `axis=1` → 1.60 (very cautious), `axis=5` → 1.25 (baseline, -/// matches historical hardcoded value), `axis=10` → 1.10 (rush-happy). +/// Range: `axis=1` → 1.80 (very cautious), `axis=5` → 1.50 (post-p0-37+39 +/// tempo baseline), `axis=10` → 1.15 (rush-happy). Baseline raised 2026-04-18 +/// from 1.25 so games reach T250+ — tier-3+ tech chains need the runway. pub fn dominance_factor(axes: &BTreeMap) -> f32 { - lerp_axis(axis(axes, "aggression"), 1.60, 1.25, 1.10) + lerp_axis(axis(axes, "aggression"), 1.80, 1.50, 1.15) } /// Hex radius within which a unit bypasses stray-unit chasing to march on @@ -190,7 +191,7 @@ mod tests { #[test] fn dominance_factor_baseline_matches_historical() { let a = axes(&[("aggression", 5)]); - assert!((dominance_factor(&a) - 1.25).abs() < 1e-6); + assert!((dominance_factor(&a) - 1.50).abs() < 1e-6); } #[test] @@ -241,7 +242,7 @@ mod tests { #[test] fn empty_axes_match_historical_baseline() { let empty: BTreeMap = BTreeMap::new(); - assert!((dominance_factor(&empty) - 1.25).abs() < 1e-6); + assert!((dominance_factor(&empty) - 1.50).abs() < 1e-6); assert_eq!(capital_approach_hex(&empty), 16); assert!((retreat_hp_fraction(&empty) - 0.40).abs() < 1e-6); assert_eq!(defensive_chase_range(&empty), 12); @@ -303,8 +304,8 @@ mod tests { let insane_high = axes(&[("aggression", 999)]); let df_low = dominance_factor(&insane_low); let df_high = dominance_factor(&insane_high); - assert!(df_low >= 1.09 && df_low <= 1.61); - assert!(df_high >= 1.09 && df_high <= 1.61); + assert!(df_low >= 1.14 && df_low <= 1.81); + assert!(df_high >= 1.14 && df_high <= 1.81); } #[test]