From 1dc3c3f21576d3f9b210a985e00387f12d130798 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 18 Apr 2026 17:56:24 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20race/resource=20filtering=20to=20melee=20unit?= =?UTF-8?q?=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../crates/mc-ai/src/tactical/production.rs | 7 +++++- .../crates/mc-ai/src/tactical/state.rs | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/simulator/crates/mc-ai/src/tactical/production.rs b/src/simulator/crates/mc-ai/src/tactical/production.rs index 1c9176aa..54facf08 100644 --- a/src/simulator/crates/mc-ai/src/tactical/production.rs +++ b/src/simulator/crates/mc-ai/src/tactical/production.rs @@ -202,7 +202,12 @@ fn pick_for_city( // Tier-progression unit selection (p0-39). Highest-tier buildable military // unit for this player; falls back to `warrior` when the catalog is empty // (fixtures predating p0-39) or no higher-tier gate is met. - let melee_id = pick_best_melee(&player.researched_techs, unit_catalog).unwrap_or(ids::WARRIOR); + let melee_id = pick_best_melee( + &player.researched_techs, + player.race_id.as_deref(), + &player.strategic_resources, + unit_catalog, + ).unwrap_or(ids::WARRIOR); let early_mil_floor = if turn <= EARLY_MIL_FLOOR_CUTOFF_TURN { EARLY_MIL_FLOOR } else { diff --git a/src/simulator/crates/mc-ai/src/tactical/state.rs b/src/simulator/crates/mc-ai/src/tactical/state.rs index 36cdfa14..ba818466 100644 --- a/src/simulator/crates/mc-ai/src/tactical/state.rs +++ b/src/simulator/crates/mc-ai/src/tactical/state.rs @@ -94,6 +94,18 @@ pub struct TacticalPlayerState { /// Diplomatic relations per opponent slot. `<0` war, `0` peace, `>0` /// friend. Self-slot is 0. pub relations: Vec, + /// Race id (e.g. `"dwarf"`, `"human"`). `None` for fixtures predating + /// race-gated unit selection. Consumed by + /// `tactical::production::pick_best_melee` to filter units whose + /// `race_required` doesn't match. + #[serde(default)] + pub race_id: Option, + /// Strategic resource ids the player currently controls (tiles they own + /// that provide `iron_ore`, `horses`, etc.). Consumed by + /// `tactical::production::pick_best_melee` to filter units whose + /// `requires_resource` isn't available. + #[serde(default)] + pub strategic_resources: Vec, /// Clan personality axes on the `1..=10` scale (neutral = 5). Consumed by /// `tactical::thresholds` for personality-emergent posture / retreat / /// chase / siege thresholds (p0-37). Empty map = baseline (axis=5 for @@ -152,6 +164,18 @@ pub struct TacticalUnitSpec { /// Unit-type classification mirroring `units/*.json::unit_type`: /// `"military"` | `"worker"` | `"founder"` | `"scout"` | … pub unit_type: String, + /// Strategic resource gate — unit buildable only when the player owns at + /// least one tile providing this resource id (e.g. `"iron_ore"` for + /// cavalry). `None` means no resource requirement. Filtered by + /// `tactical::production::pick_best_melee` to avoid queueing units the + /// engine's strategic-gate check will reject. + #[serde(default)] + pub requires_resource: Option, + /// Race gate — unit buildable only when the player's race matches this id + /// (e.g. `"dwarf"` for berserker / ironwarden / forge_titan). `None` + /// means no race restriction. + #[serde(default)] + pub race_required: Option, } /// A city.