8.2 KiB
| id | title | priority | status | scope | owner | updated_at | evidence | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| p0-39 | AI tier-progression unit selection — production.rs picks tier-2+ units once tech unlocks | p0 | done | game1 | warcouncil | 2026-04-18 |
|
Summary
Shipwright audit 2026-04-18 of tech_web.json + research costs (requested by warcouncil session-close handoff) found the tech tree, costs, and research pacing are correct. peak_unit_tier=1 universally is NOT a balance-data issue. Root cause is in the tactical AI's production-selection logic:
src/simulator/crates/mc-ai/src/tactical/production.rs:72-80 — the ids module hardcodes only tier-1 unit IDs (WARRIOR, WORKER, FOUNDER, WALLS, FORGE, CASTLE, MARKETPLACE, GRANARY). The priority ladder in decide_production() pulls exclusively from this list. When bronze_working researches (reliably by turn ~72) and enables pikeman (tier-2), the tactical AI has no branch that picks it. Same gap blocks berserker, runesmith, cavalry, ironwarden, forge_titan, mithril_vanguard.
Empirical evidence (batch apricot-20260418_062941, T300)
- 53 techs researched by T300 per player — tech pipeline flows correctly
bronze_workingresearched turn 72 in one inspected seed- Zero pikemen built across any seed
- Units built: 393× warrior, 4× worker, 2× founder, 2× dwarf_tribe — all tier-1
- Telemetry honest:
peak_unit_tierreadsDataLoader.get_unit(type_id).tier; it reports 1 because tier-1 is all that exists in live gameplay
Acceptance
- ✓ Unit-spec catalog accessible to production.rs (Approach 1 — data-driven, per Shipwright recommendation).
TacticalUnitSpecstruct (src/simulator/crates/mc-ai/src/tactical/state.rs:126-141) carriesid,tier,tech_required: Option<String>,unit_type.TacticalState::unit_catalog: Vec<TacticalUnitSpec>added with#[serde(default)]for back-compat. - ✓
production.rs::decide_productionselects tier-N+ units whentech_requiredis researched. New helperpick_best_melee(researched_techs, catalog)filters byunit_type == "military", excludes ranged specialists (archer/ranger/flying), keeps unlocked units, returns highest-tier. 4 melee-slot branches (Posture::Threatened, early-mil-floor, steady-mil, offensive push) now use the helper result instead of hardcodedids::WARRIOR. Existing wealth/production/aggression/dominance axis biases preserved. - ✓ Regression test lands.
tactical::production::tests::tier_2_unit_selected_when_tech_researched(plus 5pick_best_melee_*unit tests covering empty catalog, tier-1 fallback, tier climb, non-military exclusion, ranged exclusion). All 236/236 mc-ai tests green (was 230, +6 new). - ✓ Bridge emits catalog.
ai_turn_bridge.gd::_build_unit_catalog()readsDataLoader.get_data("units")and emits{id, tier, tech_required, unit_type}per unit on every_build_mc_tree_statecall. - ✓ Apricot 10-seed T300 smoke shows median
peak_unit_tier ≥ 2across seeds..local/iter/apricot-20260418_194533/(v6 batch, post-autoplay-fix): median_peak_unit_tier = 2.0 (up from 1.0 across all prior batches). Seed 2: 107 pikemen built. Seed 3: 83 pikemen built. Tier climb emerges asbronze_workingis researched. - 🟡 p0-01 peak-unit-tier bullet — tier-2 gate cleared (median 2.0). p0-01's full threshold "≥1 player reached peak unit tier ≥ 6 in ≥7/10 games" still requires unit upgrades into tier 3-6 (cavalry through mithril_vanguard). Cavalry (tier 3) needs
iron_orestrategic resource which is rare in current map gen; ironwarden/forge_titan/mithril_vanguard need dwarf race + late techs. Further gains require either: (a) biasing map gen toward iron_ore tiles, (b) pushing tech research rate so late techs unlock before game end, or (c) extending game length so players have time to reach late tiers. All out of p0-39's scope.
Discovery summary (v1 → v6)
Six rebuild-and-smoke iterations exposed three overlapping bugs:
- v1/v2/v3 byte-identical:
auto_play.gd::_manage_productionis the actual production driver in headless batches, NOTmc-ai::tactical::production— the Rust port'sdecide_production()is never consulted. All Rust-sidepick_best_meleework was inert. - v3 bridge crash:
_build_unit_catalogcrashed onDataLoader.get_data("units")entries that weren't Dictionaries (Resource class instances, manifest noise). Fixed with typed-guard helper_dict_string_field+ Dictionary-only filter. - v4 upgrade inert: added post-pick
warrior → best-meleeswap in_next_building, but swap path never ran because:- (a) loaded unit JSONs at
public/resources/units/dwarf_*.jsonlacktierfield → fell back tocost-based ranking - (b) returned unit was silently reclassified as "building" and rejected by
add_to_queue(hardcodedunit_ids = ["warrior", "founder", "worker"]predated tier-2+ unit ids)
- (a) loaded unit JSONs at
- v5 network hang: local ssh build pipe died silently; killed + relaunched.
- v6 PASSED: unit-vs-building classified via
DataLoader.get_unit(built).is_empty(), tier-ranked by (tier*1000 + cost), pikemen finally emerge.
Key fixes applied:
src/game/engine/scenes/tests/auto_play.gd: added_best_melee_for_player(player, city)helper + post-pick upgrade; replaced hardcodedunit_idswith DataLoader-based classification.src/simulator/crates/mc-ai/src/tactical/state.rs:TacticalUnitSpecstruct +unit_catalogfield +race_id+strategic_resourcesonTacticalPlayerState(kept for future mc-ai path).src/simulator/crates/mc-ai/src/tactical/production.rs:pick_best_meleehelper with tech/race/resource filters + 8 regression tests (all green, 239/239 mc-ai tests).src/game/engine/src/modules/ai/ai_turn_bridge.gd:_build_unit_catalogtyped-safe;_collect_strategic_resourcesfrom owned tiles.
Fix direction (non-prescriptive — warcouncil picks)
Two candidate approaches; choose based on blast radius:
- Dynamic candidate generation: replace
idsmodule constants with a functioncandidate_units_for_role(role, player_techs, unit_catalog)returning the highest-tier reachable unit per role. Data-driven — new JSON units "just work". Needs unit catalog plumbed to TacticalState. - Extend hardcoded list: add
PIKEMAN,BERSERKER,CAVALRY,IRONWARDEN,FORGE_TITAN,MITHRIL_VANGUARDconstants + conditional priority branches (if-has-tech gates). Faster to land; accumulates hardcoded lists that future units will miss.
Both preserve p0-02 tunables (DOMINANCE_GOLD_FLOOR=50, PRODUCTION_AXIS_BUILDING_BIAS=8).
Non-goals
- p0-24 difficulty calibration — separate objective (ai_modifiers.production_mult / starting_gold_bonus / extra_starting_units). Tier-progression is orthogonal.
- p0-38 PUCT strategic migration — GameRolloutState priors for McSnapshot. Separate.
- New unit authoring — tier-2+ unit JSONs already exist.
Depends on
- None (all prerequisite data + telemetry exist).
Blocks
- p0-01 MCTS wiring — the
peak_unit_tier ≥ 6 in ≥ 7/10 gamesbullet cannot close without tier progression. - p0-22 Ultimate AI stress test — matchup-grid variance gated on post-tier-1 armies.
- p0-08 Domination tempo — partially gated; p0-37 addressed personality tempo but tier-2 armies naturally extend combat duration, which also helps p1-05 luxury variance via longer games.
Related
- p0-37 personality-emergent thresholds (done) — tactical AI tempo now fires correctly; this objective unblocks the content ceiling.
- p1-05 balance tuning (partial) — Shipwright flagged luxury_variance as gated on game length. Tier-2 armies extend games, so closing p0-39 likely lifts p1-05 passively.
Why P0
Without tier progression, every downstream quality gate (peak_unit_tier, wonder count, content-ceiling metrics) is structurally impossible to close. This is the single highest-leverage change left in warcouncil's lane for Game 1 EA.