from __future__ import annotations import argparse from collections import Counter from pathlib import Path import sys import time REPO_ROOT = Path(__file__).resolve().parents[1] SRC_ROOT = REPO_ROOT / "src" if str(SRC_ROOT) not in sys.path: sys.path.insert(0, str(SRC_ROOT)) from world_simulator.config import ( # noqa: E402 GameConfig, NpcConfig, ServerConfig, SimulationConfig, WorldConfig, ) from world_simulator.simulation.spawning import create_world # noqa: E402 from world_simulator.simulation.overseer import scripted_overseer_controller # noqa: E402 from world_simulator.simulation.tick import advance_world # noqa: E402 def main() -> None: parser = argparse.ArgumentParser(description="Run a deterministic headless village simulation.") parser.add_argument("--mock-overseer", action="store_true", help="Enable scripted Overseer directives.") args = parser.parse_args() world = create_world( GameConfig( world=WorldConfig( width=80, depth=80, terrain="plain_green", seed=42, survival=True, ), npcs=NpcConfig(count=6), simulation=SimulationConfig(tick_ms=1), server=ServerConfig(host="127.0.0.1", port=8000), ) ) min_population = world.population cross_role_actions: list[str] = [] tick_durations: list[float] = [] overseer = scripted_overseer_controller() if args.mock_overseer else None all_debug: list[dict[str, object]] = [] mode_label = "deterministic+mock_overseer" if args.mock_overseer else "deterministic" print(f"HEADLESS_SIM start seed=42 ticks=300 mode={mode_label}") print( "HEADLESS_SIM initial " f"population={world.population} roles={dict(Counter(npc.role for npc in world.npcs))} " f"houses={len(world.houses)} beasts={len(world.beasts)}" ) for _ in range(300): before_events = len(world.event_log) started = time.perf_counter() advance_world(world, overseer=overseer) tick_durations.append(time.perf_counter() - started) min_population = min(min_population, world.population) all_debug.extend(world.last_action_debug) for trace in world.last_action_debug: action = str(trace.get("action", "")) npc_id = str(trace.get("npc_id", "")) npc = next((candidate for candidate in world.npcs if candidate.id == npc_id), None) if npc is None: continue if npc.role == "guard" and action == "gather": cross_role_actions.append(f"tick {world.tick}: guard {npc.id} gathered") if npc.role == "builder" and action == "attack": cross_role_actions.append(f"tick {world.tick}: builder {npc.id} attacked") new_events = world.event_log[before_events:] event_text = "; ".join( f"{event.tick}:{event.type}:{event.summary}" for event in new_events ) if not event_text: event_text = "no_events" print( f"tick={world.tick:03d} pop={world.population:02d} " f"status={world.game_status} events={event_text}" ) counts = Counter(event.type for event in world.event_log) role_by_id = {npc.id: npc.role for npc in world.npcs} guard_engagements = [ event for event in world.event_log if event.type == "npc_attack" and event.target_id is not None and event.target_id.startswith("beast") and role_by_id.get(event.actor_id or "") == "guard" ] assert counts["build_completed"] >= 1, "expected at least one completed house" assert counts["npc_born"] >= 1, "expected at least one reproduction" assert guard_engagements, "expected at least one guard attack against a beast" assert min_population >= 0, "population went negative" assert cross_role_actions, "expected at least one role-flexible action" assert all(duration < 0.25 for duration in tick_durations), "tick exceeded time budget" if args.mock_overseer: directive_events = [event for event in world.event_log if event.type == "directive_issued"] assert directive_events, "expected mock Overseer directives to reach the event log" assert any( trace.get("npc_id") == "npc-006" and trace.get("requested_action") == "attack" and trace.get("action") != "attack" for trace in all_debug ), "expected invalid builder attack directive to be validated/repaired" assert world.overseer_last_thoughts, "expected mock Overseer thoughts in world state" assert world.overseer_score > 0, "expected scoreboard Overseer points to update" houses_by_state = Counter(house.state for house in world.houses) print("HEADLESS_SIM summary") print(f" ticks={world.tick}") print(f" game_status={world.game_status}") print(f" final_population={world.population}") print(f" peak_population={world.peak_population}") print(f" min_population={min_population}") print(f" births={counts['npc_born']}") print(f" deaths={counts['npc_died']} causes={dict(world.deaths_by_cause)}") print(f" build_completed={counts['build_completed']}") print(f" houses={dict(houses_by_state)}") print(f" guard_beast_engagements={len(guard_engagements)}") print(f" beasts_killed={counts['beast_killed']}") if args.mock_overseer: print(f" directive_issued={counts['directive_issued']}") print(f" overseer_status={world.overseer_status}") print(f" overseer_last_tick={world.overseer_last_tick}") print(f" overseer_score={world.overseer_score}") print(f" chaos_score={world.chaos_score}") print(f" max_tick_ms={max(tick_durations) * 1000:.3f}") print("HEADLESS_SIM PASS") if __name__ == "__main__": main()