feat(@projects/@magic-civilization): ✨ add tier_1 wild unit data and ai wiring
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
23bfd640f6
commit
c5af6dbc71
6 changed files with 39 additions and 32 deletions
|
|
@ -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<PopulationSlot>> 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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue