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()