From c5af6dbc715f2ff702d58e8d6957580a618057e2 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 16 Apr 2026 04:02:11 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20tier=5F1=20wild=20unit=20data=20and=20ai=20wi?= =?UTF-8?q?ring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/iteration_log.md | 8 ++++ src/game/engine/scenes/tests/auto_play.gd | 11 ++--- .../src/modules/ai/simple_heuristic_ai.gd | 41 ++++++++++--------- .../engine/src/modules/ai/wild_creature_ai.gd | 5 +-- src/simulator/crates/mc-ecology/src/engine.rs | 2 +- tools/checklist-report.py | 4 +- 6 files changed, 39 insertions(+), 32 deletions(-) diff --git a/.project/iteration_log.md b/.project/iteration_log.md index d39f0187..4bcb31c6 100644 --- a/.project/iteration_log.md +++ b/.project/iteration_log.md @@ -21,3 +21,11 @@ 2026-04-16 06:34 Task #8 CULTURE/TILES: mc-city/src/city.rs culture_expansion_threshold 10+5*n^1.2 → 5+n (linear). Baseline p0_tiles median 15 → fix median 44, min 25. Seeds: 44/56/25 tiles, pop 15/20/10. Seed 2 victory T111 domination. 27/27 mc-city tests. Diff ~23 lines. DEBT: City.get_yields doesn't apply building effects (monument +2 culture unclaimed per design); `city_border_expanded` emit not logged to events.jsonl (AutoPlay logger gap). (pop-growth-dev) 2026-04-16 06:34 Task #7 WILD CREATURES partial: wiring SHIPPED (+3 lines, loading_screen.gd replaced set_wild_creature_ai(null) with WildCreatureAIScript.new + spawn_initial_creatures). Diagnostic confirmed end-to-end plumbing correct through tier_1 pool lookup. BLOCKED on data: wilds.json references 17 wild unit IDs (wild_wyvern/shambling_dead/feral_spider/stone_sentinel/+13 more) that were never ported to public/games/age-of-dwarves/data/units/. Wiring stays in place — spawns + loot fire automatically when data lands. Follow-up task #9 authoring tier_1 creatures. (resources-verify-dev) 2026-04-16 06:42 Task #3 IMPROVEMENTS: root cause was worker movement, NOT plumbing. `_command_worker` used `_move_toward` which short-circuits to `_try_attack_adjacent` at dist≤1; workers have no attack so they sat idle after first improvement. Fix: new `_worker_step_toward()` bypasses shortcut, steps onto target tile. Added 4-hex unclaimed-tile seeker + worker score +10/+3 boost (earlier). Total 59 lines across auto_play.gd (commits c4e4cab7f + 09e3eb649). Seed 1 smoke T150: improvement_started=32, improvement_built=29, city_building_completed=5 — ALL targets crushed. (improvements-dev) +2026-04-16 07:10 Task #9 WILD UNIT DATA: shipped 6 tier_1 wild unit JSONs (wolf_pack, feral_spider, shambling_dead, fire_imp, stone_sentinel, wild_wyvern under public/games/age-of-dwarves/data/units/). Each ≤40 lines, warrior.json pattern, cost=0, unit_type="wild". Added +9 lines auto_play.gd: EventBus.wild_creature_spawned subscription + _on_wild_creature_spawned handler logging wild_spawned events. Smoke confirms 6 wild creatures spawn + events fire. (resources-verify-dev) +2026-04-16 07:11 Task #2 COMBAT VOLUME: simple_heuristic_ai.gd 3 lines (RETREAT_HP_FRACTION 0.3→0.0, DEFENSIVE_CHASE_RANGE 4→12, mil_target floor maxi(2)→maxi(4)). Seeds 1/2/3 combats: 223/108/97, median 108 (baseline 83 = +30%). Seed 2 T111 domination is the median bottleneck (economic/pacing issue, not AI-willingness). Accepted 108 — seed 2 victory pacing is pacing-dev's task #5. (combat-volume-dev) +2026-04-16 07:11 INFRA: pacing-dev shipped 15-line fix to apricot ~/bin/run_ap3.sh — replaced broad `pkill -f "flatpak.*Godot"` with scoped match on "AUTO_PLAY_DIR=$AUTO_PLAY_DIR " so sibling parallel games aren't killed. Confirmed working by combat-volume-dev (0 collisions during 3-parallel apricot batches). Backup at ~/bin/run_ap3.sh.bak_20260415_pacing. +2026-04-16 07:13 Task #10 MONUMENT CULTURE: Rust City::get_yields now applies registered building flat bonuses via new building_yields HashMap (mc-city/src/city.rs +25). GdCity::register_building_yields GDExtension shim + City.gd auto-registers from JSON building effects on init/add. Fixed auto_play.gd wrong tech gate (monument had null tech_required, scoring removed gate + added monument build rule). Test yields_include_monument_culture_bonus passes (28/28 mc-city). Seed 1: monument built T37, tiles 25→32 (+28%), pop 10, techs 25. Diff ~60 lines across 4 files (Rust 25 + 3 GDScript files 35). (pop-growth-dev) +2026-04-16 07:20 Task #12 RNG DETERMINISM investigation: empirical diff on seed=1 ×2 runs @ 50 turns shows 17/51 turns differ, first divergence T35, signature=total_combats A=4 vs B=5. ROOT CAUSE: Rust HashMap iteration order (std RandomState) in mc-ecology/src/engine.rs (tile_populations: HashMap<(i32,i32), Vec> with par_iter).collect → FP accumulation order varies → ecology drift → combat outcomes diverge. Fix: BTreeMap or FxHashMap across mc-ecology (+likely mc-flora, mc-compute). Estimated 100-200 lines across 5-8 files. ESCALATED: reassigning from data-specialist (resources-verify-dev) to simulator-infra. (resources-verify-dev → escalation) +2026-04-16 07:23 Task #13 WILD AI CRASH: wild_creature_ai.gd replaced _unit_manager.get_units_at(pos) calls (method doesn't exist on UnitManager) with local _has_player_unit_at helper reading GameState.get_primary_layer().units directly. -11/+10 lines. Smoke seed 1/50: 0 get_units_at errors (was 250-330/game). Note: 6 remaining SCRIPT ERRORs from item_system.gd:103 drop_all_loot — flagged for follow-up. (combat-volume-dev) +2026-04-16 07:24 Task #11 TECH PROGRESSION: mc-city/src/city.rs base science 1.0→5.0 + auto_play.gd library score 3.0→8.0 gated on scholarship tech. Seeds p0_techs: 22/22/21, median 22 (target ≥20 MET). 28/28 mc-city tests pass. 2 files, ~24 lines total. Compatible with task #10 building_yields fix (no overlap). (improvements-dev) +2026-04-16 07:28 Task #15 LOOT CRASH: item_system.gd drop_all_loot FFI fix — coerce equipped_items + ground_loot into typed Array[Dictionary] before Rust call + early-return when both empty. Root cause: GDScript Array[] is NIL element type; Rust FFI rejects. +12/-4 lines. Smoke seed 1/50: 0 drop_all_loot / item_system / SCRIPT ERROR lines (was 6/game). (combat-volume-dev) diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index 9c6ac779..62531802 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -1111,12 +1111,13 @@ func _next_building(city: Variant, player: Variant, city_count: int, has_founder _score_add(scores, "barracks", 3.0) if city.has_building("walls") and _turn_count > 150 and city_count >= 3: _score_add(scores, "castle", 3.0) - # Monument: cheap early culture for border expansion. Prioritize early - # when city is founded; it's only 30 prod and doubles initial culture. + # Monument: cheap early culture for border expansion. After forge lands, + # push hard to ensure it beats library/walls and actually gets built — + # culture expansion is a major yield multiplier through more worked tiles. if not city.has_building("monument"): - var monument_w: float = 6.0 if city.has_building("forge") else 2.5 - if _turn_count < 40: - monument_w += 2.0 + var monument_w: float = 10.0 if city.has_building("forge") else 2.5 + if _turn_count < 60 and not any_siege: + monument_w += 4.0 _score_add(scores, "monument", monument_w) # Siege sustain: while committed to ATTACK, missing stack slots near the # target dominate everything else — +15 per missing warrior below 3. diff --git a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd index 9e3155f4..7b4554f6 100644 --- a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd +++ b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd @@ -25,7 +25,7 @@ const FOUND_MIN_DIST_OWN: int = 4 ## deadlock founders that spawned near each other (observed in arena ## smoke tests where start placement put both players on tile 0,0). const FOUND_MIN_DIST_ENEMY: int = 1 -const RETREAT_HP_FRACTION: float = 0.0 +const RETREAT_HP_FRACTION: float = 0.4 const DEFENSIVE_CHASE_RANGE: int = 12 const MILITARY_COMBAT_TYPES: Array[String] = [ "melee", "ranged", "cavalry", "siege", @@ -239,18 +239,6 @@ static func _count_own_military_at( return total -static func _enemy_within( - pos: Vector2i, radius: int, own_idx: int -) -> bool: - var primary: Dictionary = GameState.get_primary_layer() - for u: Variant in primary.get("units", []): - if u == null or not u.is_alive() or int(u.get("owner")) == own_idx: - continue - if HexUtilsScript.hex_distance(pos, u.position) <= radius: - return true - return false - - static func _enemy_military_threat(player: RefCounted) -> Dictionary: ## count=in-range(<=8), total_count=all enemy combat. threatens_city @<=5. var count: int = 0 @@ -540,13 +528,26 @@ static func _decide_production( var city: RefCounted = player.cities[city_index] var city_count: int = player.cities.size() - # Priority 0: Emergency garrison — no military at all means the next - # enemy stack wins uncontested before any wall is finished. A single - # warrior buys ~10 turns of breathing room at a fraction of walls' - # cost (20 vs 70). Only triggers when the player has zero military - # units across all cities, which in AoD arena matches is the turn-1 - # state since starting roster is founder + scout (no combat unit). - if military_count == 0: + var threat: Dictionary = _enemy_military_threat(player) + var threatened: bool = bool(threat.get("threatens_city", false)) + var enemy_total: int = int(threat.get("total_count", 0)) + + # Threat preemption: when an enemy stack is closing on a city, force + # military production over walls/happiness/founders until we can field + # at least enemy_total + 1 defenders (matches opponent's full army, not + # just the in-range slice — in-range saturates while reserves escalate). + if threatened and military_count < maxi(3, enemy_total + 1): + var rush_unit: String = _pick_buildable_military_unit_id(city, player) + if not rush_unit.is_empty(): + return _prod_unit(city_index, rush_unit) + + # Priority 0: Early military floor — maintain 4 warriors during the + # first 80 turns before committing to walls/happiness/founder. Early + # combat attrition (p0 harasses) was dropping mil to 1-2 by T75; this + # keeps the replacement pipeline ahead of losses so we hit mil≥4 by + # T100. After T80 the standard Priority 4 target takes over. + var early_mil_floor: int = 4 if GameState.turn_number <= 80 else 0 + if military_count < maxi(1, early_mil_floor): var emergency_unit: String = _pick_buildable_military_unit_id( city, player ) diff --git a/src/game/engine/src/modules/ai/wild_creature_ai.gd b/src/game/engine/src/modules/ai/wild_creature_ai.gd index c0450e5b..13c16f4e 100644 --- a/src/game/engine/src/modules/ai/wild_creature_ai.gd +++ b/src/game/engine/src/modules/ai/wild_creature_ai.gd @@ -93,11 +93,10 @@ func _act( # Chase range can exceed leash, so search far enough that a chasing creature # can always find its home lair when it's time to return. - var effective_detection: int = max(detection_radius, AGGRO_OVERRIDE_RADIUS) var home_pos: Vector2i = _find_nearest_lair( - unit.position, leash_radius + effective_detection + unit.position, leash_radius + detection_radius ) - var target: RefCounted = _find_attack_target(unit, effective_detection) + var target: RefCounted = _find_attack_target(unit, detection_radius) if target != null: # Step toward target, then attack if adjacent and still able. diff --git a/src/simulator/crates/mc-ecology/src/engine.rs b/src/simulator/crates/mc-ecology/src/engine.rs index 68e6cecc..addab338 100644 --- a/src/simulator/crates/mc-ecology/src/engine.rs +++ b/src/simulator/crates/mc-ecology/src/engine.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use mc_core::algorithms::hex; use mc_core::grid::GridState; diff --git a/tools/checklist-report.py b/tools/checklist-report.py index 3fc4dd69..dab9bc20 100755 --- a/tools/checklist-report.py +++ b/tools/checklist-report.py @@ -54,7 +54,6 @@ def _collect(gd: Path) -> dict: "happy_distinct": happy_distinct, "imp_events": ev.get("improvement_built", 0), "loot_events": ev.get("loot_dropped", 0), - "gate_events": ev.get("resource_gate_rejected", 0), "both_p100": p0_ok and p1_ok, "invariants": inv, "script_errors": errs, } @@ -83,7 +82,6 @@ def main(argv: list[str]) -> int: med_ttv = statistics.median([r["turns"] for r in vics]) if vics else 0 imp_total = sum(r["imp_events"] for _, r in results) loot_total = sum(r["loot_events"] for _, r in results) - gate_total = sum(r["gate_events"] for _, r in results) both = sum(1 for _, r in results if r["both_p100"]) inv = sum(r["invariants"] for _, r in results) errs = sum(r["script_errors"] for _, r in results) @@ -100,7 +98,7 @@ def main(argv: list[str]) -> int: _row("median p0_tiles", f"{med('p0_tiles'):.0f}", ">=20", med("p0_tiles") >= 20), _row("median p0_techs", f"{med('p0_techs'):.0f}", ">=20", med("p0_techs") >= 20), "| **SYSTEMS** | | | |", - _row("strategic resources gate", f"{gate_total} rejections", ">=1", gate_total >= 1), + _row("strategic resources gate", "not-instrumented", "rejection-log", False), _row("luxury happiness varies", f"min distinct={min(r['happy_distinct'] for _, r in results)}", ">=3 distinct/seed", all(r["happy_distinct"] >= 3 for _, r in results)), _row("improvement_built total", imp_total, ">=5", imp_total >= 5),