| 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 ( |
| GameConfig, |
| NpcConfig, |
| ServerConfig, |
| SimulationConfig, |
| WorldConfig, |
| ) |
| from world_simulator.simulation.spawning import create_world |
| from world_simulator.simulation.overseer import scripted_overseer_controller |
| from world_simulator.simulation.tick import advance_world |
|
|
|
|
| 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() |
|
|