diff --git a/.project/designs/design-gallery.html b/.project/designs/design-gallery.html new file mode 100644 index 00000000..972700d7 --- /dev/null +++ b/.project/designs/design-gallery.html @@ -0,0 +1,635 @@ + + + + + +Age of Dwarves — Design System Gallery + + + + + + +

Age of Dwarves

+

UI Design System · Episode 1 · Magic Civilization Series

+ + +
+
Backgrounds
+
+
bg-deepest
#171219
+
bg-menu
#0e0a17
+
bg-panel
#17121e
+
bg-list
#120e1e
+
bg-base
#1a1410
+
bg-surface
#221a14
+
bg-raised
#2a2018
+
bg-list-selected
#3f2d0d
+
btn-normal
#1f1733
+
btn-hover
#331a0d
+
btn-pressed
#472f0f
+
+
+ + +
+
Text Hierarchy
+
+
text-title
#f2d973
+
text-primary
#e0d8c8
+
text-secondary
#bfb7a6
+
text-muted
#b2b2b2
+
text-button
#e0d199
+
text-btn-hover
#ffeb80
+
text-btn-press
#ffffb3
+
+
+ + +
+
Accents & Semantic
+
Accents
+
+
accent-gold
#d9a020
+
gold-bright
#d9b33f
+
gold-press
#ffd14d
+
gold-resource
#f2d133
+
accent-science
#66bfff
+
accent-sage
#66b866
+
accent-ping
#ffd973
+
+
Semantic
+
+
✓ Positive
+
★ Golden Age
+
✗ Negative
+
⚠ Warning
+
⚔ War
+
☮ Peace
+
⇌ Trade
+
+
+ + +
+
Player Colors (Default Palette)
+
+
Blue
+
Red
+
Green
+
Yellow
+
Purple
+
Orange
+
Cyan
+
Magenta
+
Brown
+
Gray
+
Sage
+
Navy
+
+
+ + +
+
Typography
+
+
24px / title
+
Ironhold — Capital of the Stoneguard
+
+
+
16px / md
+
Research Complete: Iron Smelting Unlocked
+
+
+
15px / base
+
Production: Barracks · 8 turns remaining
+
+
+
14px / sm
+
Population 6 · Food +3 · Culture +2 per turn
+
+
+
13px / xs
+
Turn 42 · Era: Iron Age · Seed 0xDEAD
+
+
+ + +
+
Buttons
+
States
+
+ + + + + +
+
Variants
+
+ + + + + + + + +
+
+ + +
+
Panels & Lists
+
+
+
+
Tile Information
+
+
⛰ Mountain Plains
+
Owner: Stoneguard Clan · Improved
+
+
🪙+2
+
🔨+3
+
🌾+1
+
+
+
+
+
+
Barracks120 ⚒
+
Forge  ↗ 3 turns80 ⚒
+
Marketplace150 ⚒
+
Library130 ⚒
+
Bathhouse110 ⚒
+
+
+
+
+ + +
+
HUD Elements
+
Resource Stats (Top Bar)
+
+
🪙+84
+
+22
+
+7
+
🌿+12
+
War
+
Unhappy −3
+
+ +
Climate Gauges
+
+
+ Cold +
+ −2.1° +
+
+ Warm +
+ +0.4° +
+
+ Hot +
+ ⚠ Warming +
+
+ +
Notifications
+
+
+
+
Combat Victory
Warrior defeated Emberfall Scout · +12 XP
+
+
+
+
Research Complete
Iron Smelting unlocked · Choose next research
+
+
+
+
Golden Age
The Stoneguard enters a Golden Age · +2 turns
+
+
+ +
Promotions
+
+
★ Strength I
+
★★ Flanking II
+
★ City Raider
+
★★★ Combat Master
+
+
+ + +
+
Fog of War States
+
+
+
+ Visible +
+
0% fog
+
+
+
+
+ Explored +
+
+
70% black
+
+
+
+
+ Unknown +
+
+
90% black
+
+
+
+
+ Owned +
+
+
38% tint
+
+
+
+
+
+
+
+
Ping ring
+
+
+
+ + + + + diff --git a/src/simulator/crates/mc-ai/src/tactical/movement.rs b/src/simulator/crates/mc-ai/src/tactical/movement.rs index 5ad92580..ab2c32b0 100644 --- a/src/simulator/crates/mc-ai/src/tactical/movement.rs +++ b/src/simulator/crates/mc-ai/src/tactical/movement.rs @@ -261,24 +261,15 @@ enum UnitRole { } fn unit_role(unit: &TacticalUnit) -> UnitRole { - // `can_found_city` is the data-driven founder flag populated from the - // engine's per-unit JSON (via the bridge in task #3). PREFER this over - // a kind-string allow-list: clan-themed founders like `"dwarf_tribe"` - // don't literally contain "settler"/"founder" but DO have the flag. - // Letting them fall through to Military would make movement.rs move - // the settler every turn, which races settle.rs's FoundCity emission - // and perpetually rejects dispatch (observed 2026-04-18 as the - // "p1 never founds" regression in batch bvlljmfvk trace). - if unit.can_found_city { + // Use the canonical predicate so clan-themed founders (e.g. "dwarf_tribe") + // are caught by the data-driven flag rather than a literal kind-string. + // Moving a settler every turn races settle.rs's FoundCity emission and + // causes the "p1 never founds" regression (batch bvlljmfvk, 2026-04-18). + if unit.is_founder() { return UnitRole::Settler; } match unit.kind.as_str() { - "settler" | "dwarf_founder" | "founder" => UnitRole::Settler, "worker" | "engineer" => UnitRole::Worker, - // Everything else in the Age of Dwarves data set has combat stats - // — warriors, spearmen, pikemen, archers, crossbowmen, cavalry, - // scouts. Match the GDScript heuristic `attack > 0 || - // ranged_attack > 0` here via kind-based classification. _ => UnitRole::Military, } } diff --git a/src/simulator/crates/mc-ai/src/tactical/production.rs b/src/simulator/crates/mc-ai/src/tactical/production.rs index 699d389a..8826869d 100644 --- a/src/simulator/crates/mc-ai/src/tactical/production.rs +++ b/src/simulator/crates/mc-ai/src/tactical/production.rs @@ -22,7 +22,7 @@ use std::time::Instant; use crate::evaluator::ScoringWeights; use crate::mcts::XorShift64; -use super::{Action, TacticalCity, TacticalPlayerState, TacticalState}; +use super::{Action, TacticalCity, TacticalPlayerState, TacticalState, TacticalUnit}; /// Gold floor required before the AI commits to a rush-buy assault unit. /// @@ -285,7 +285,7 @@ fn pick_for_city( // founders — mirrors `city_index == 0` gate. let expansion_target = (axes.expansion as u32 / 3).clamp(1, 5); if city_count < expansion_target && city.is_capital { - let founder_count = player.units.iter().filter(|u| is_founder(&u.kind)).count() as u32; + let founder_count = player.units.iter().filter(|u| is_founder(u)).count() as u32; if founder_count == 0 { return ids::FOUNDER.into(); } @@ -415,16 +415,16 @@ fn count_military_units(player: &TacticalPlayerState) -> u32 { player .units .iter() - .filter(|u| is_military(&u.kind)) + .filter(|u| is_military(u)) .count() as u32 } -fn is_military(kind: &str) -> bool { - !matches!(kind, "founder" | "settler" | "worker") +fn is_military(unit: &TacticalUnit) -> bool { + !unit.is_founder() && unit.kind != "worker" } -fn is_founder(kind: &str) -> bool { - matches!(kind, "founder" | "settler") +fn is_founder(unit: &TacticalUnit) -> bool { + unit.is_founder() } /// Max military count across all opponents. GDScript uses total enemy diff --git a/src/simulator/crates/mc-ai/src/tactical/settle.rs b/src/simulator/crates/mc-ai/src/tactical/settle.rs index 302d168f..e0dcf13b 100644 --- a/src/simulator/crates/mc-ai/src/tactical/settle.rs +++ b/src/simulator/crates/mc-ai/src/tactical/settle.rs @@ -114,13 +114,8 @@ pub(crate) fn decide_settle( actions } -/// A settler is any unit flagged `can_found_city` by the engine's data pack. -/// Prefer the data-driven flag over string matching on `kind` — clan-themed -/// founders like `"dwarf_tribe"` wouldn't match a hardcoded id set. -/// Falls back to string match on legacy fixtures (where `can_found_city` is -/// default-false) so existing tests continue to pass. fn is_settler(unit: &TacticalUnit) -> bool { - unit.can_found_city || matches!(unit.kind.as_str(), "settler" | "founder") + unit.is_founder() } /// Return the action for a single settler, or `None` if the settler cannot diff --git a/src/simulator/crates/mc-ai/src/tactical/state.rs b/src/simulator/crates/mc-ai/src/tactical/state.rs index 1028a6a4..1792603c 100644 --- a/src/simulator/crates/mc-ai/src/tactical/state.rs +++ b/src/simulator/crates/mc-ai/src/tactical/state.rs @@ -161,6 +161,14 @@ pub struct TacticalUnit { pub patrol_order: Option>, } +impl TacticalUnit { + /// True when this unit can found a city. Checks the data-driven flag first; + /// falls back to kind-string matching for test fixtures that omit the flag. + pub fn is_founder(&self) -> bool { + self.can_found_city || matches!(self.kind.as_str(), "settler" | "founder") + } +} + /// Specification for a producible military unit — carries enough data for the /// production layer to select tier-appropriate units as tech unlocks (p0-39). /// diff --git a/src/simulator/crates/mc-ai/src/tactical/thresholds.rs b/src/simulator/crates/mc-ai/src/tactical/thresholds.rs index b8b455a0..95979ac8 100644 --- a/src/simulator/crates/mc-ai/src/tactical/thresholds.rs +++ b/src/simulator/crates/mc-ai/src/tactical/thresholds.rs @@ -75,11 +75,15 @@ 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.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. +/// Range: `axis=1` → 2.5 (very cautious), `axis=5` → 2.0 (baseline), +/// `axis=10` → 1.5 (rush-happy). Baseline raised 2026-04-26 from 1.50 to 2.0 +/// (and rush-happy floor from 1.15 → 1.5) per warcouncil cycle-3 finding: +/// games still ended T48-T121 in 50% of seeds because even rush-happy +/// blackhammer (agg=9) needed only ~1.2× superiority before committing, +/// triggering rush-domination before tier-3 tech could research. Higher +/// thresholds give losers economic + tech runway. pub fn dominance_factor(axes: &BTreeMap) -> f32 { - lerp_axis(axis(axes, "aggression"), 1.80, 1.50, 1.15) + lerp_axis(axis(axes, "aggression"), 2.5, 2.0, 1.5) } /// Hex radius within which a unit bypasses stray-unit chasing to march on @@ -191,7 +195,9 @@ mod tests { #[test] fn dominance_factor_baseline_matches_historical() { let a = axes(&[("aggression", 5)]); - assert!((dominance_factor(&a) - 1.50).abs() < 1e-6); + // Baseline raised 2026-04-26 from 1.50 → 2.0 (warcouncil cycle-3 + // rush-domination dampening). See dominance_factor() docs. + assert!((dominance_factor(&a) - 2.0).abs() < 1e-6); } #[test] @@ -242,7 +248,7 @@ mod tests { #[test] fn empty_axes_match_historical_baseline() { let empty: BTreeMap = BTreeMap::new(); - assert!((dominance_factor(&empty) - 1.50).abs() < 1e-6); + assert!((dominance_factor(&empty) - 2.0).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); @@ -304,8 +310,9 @@ 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.14 && df_low <= 1.81); - assert!(df_high >= 1.14 && df_high <= 1.81); + // Range bumped 2026-04-26 alongside dominance_factor lerp (1.5–2.5). + assert!(df_low >= 1.49 && df_low <= 2.51); + assert!(df_high >= 1.49 && df_high <= 2.51); } #[test]