File size: 5,961 Bytes
d0c158f c58d3eb d0c158f c58d3eb d0c158f 81070c7 d0c158f 81070c7 d0c158f 81070c7 d0c158f 81070c7 d0c158f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | 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()
|