feat(@projects/@magic-civilization): add iron-ore density strategy objective

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-18 20:24:31 -07:00
parent 88080e597e
commit c81744b01e
2 changed files with 67 additions and 7 deletions

View file

@ -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+.

View file

@ -75,10 +75,11 @@ pub fn derived_defense(axes: &BTreeMap<String, i32>) -> 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<String, i32>) -> 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<String, i32> = 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]