from __future__ import annotations from world_simulator.config import GameConfig, NpcConfig, ServerConfig, SimulationConfig, WorldConfig from world_simulator.domain import Vec3 from world_simulator.simulation.connectors.base import NpcDirective, TickPlan from world_simulator.simulation.mechanics import distance_between from world_simulator.simulation.spawning import create_world from world_simulator.simulation.tick import advance_world, apply_tick_plan def test_world_spawns_configured_npc_count() -> None: world = create_world(_config(npc_count=12)) assert world.terrain.kind == "plain_green" assert world.terrain.width == 80 assert len(world.npcs) == 12 assert world.npcs[0].id == "npc-001" assert world.npcs[0].health == 100 assert 5 <= world.npcs[0].attack_damage <= 15 def test_world_spawning_is_deterministic() -> None: first = create_world(_config(npc_count=5)) second = create_world(_config(npc_count=5)) assert first.to_dict() == second.to_dict() def test_tick_advances_world_without_leaving_terrain() -> None: world = create_world(_config(npc_count=5)) advance_world(world) assert world.tick == 1 assert all(-40 <= npc.position.x <= 40 for npc in world.npcs) assert all(-40 <= npc.position.z <= 40 for npc in world.npcs) assert {npc.intention for npc in world.npcs} == {"walking"} assert all(len(npc.memory) == 1 for npc in world.npcs) def test_attack_damages_target_inside_one_block_radius() -> None: world = create_world(_config(npc_count=2)) attacker, target = world.npcs attacker.position = Vec3(x=0.0, y=0.0, z=0.0) target.position = Vec3(x=1.0, y=0.0, z=0.0) attacker.attack_damage = 10 attacker.god_directive = "Kill Boris." apply_tick_plan( world, 1, TickPlan( source="test", directives=[ NpcDirective( npc_id=attacker.id, action="strike", target_npc_id=target.id, ) ], ), ) assert 85 <= target.health <= 90 assert attacker.intention == "attacking Boris" assert "damage" in target.memory[-1].text def test_attack_outside_one_block_radius_has_no_effect() -> None: world = create_world(_config(npc_count=2)) attacker, target = world.npcs attacker.position = Vec3(x=0.0, y=0.0, z=0.0) target.position = Vec3(x=12.0, y=0.0, z=0.0) attacker.god_directive = "Kill Boris." starting_distance = distance_between(attacker, target) apply_tick_plan( world, 1, TickPlan( source="test", directives=[ NpcDirective( npc_id=attacker.id, action="strike", target_npc_id=target.id, ) ], ), ) assert target.health == 100 assert distance_between(attacker, target) < starting_distance assert attacker.intention == "approaching Boris to attack" def test_talk_adds_memory_inside_three_block_radius() -> None: world = create_world(_config(npc_count=2)) speaker, listener = world.npcs speaker.position = Vec3(x=0.0, y=0.0, z=0.0) listener.position = Vec3(x=0.0, y=0.0, z=3.0) apply_tick_plan( world, 1, TickPlan( source="test", directives=[ NpcDirective( npc_id=speaker.id, action="speak", target_npc_id=listener.id, message="Hold this position.", ) ], ), ) assert speaker.intention == "talking to Boris" assert "Hold this position." in speaker.memory[-1].text assert "Hold this position." in listener.memory[-1].text def test_memory_keeps_recent_entries_and_summarizes_older_events() -> None: world = create_world(_config(npc_count=2)) speaker, listener = world.npcs speaker.position = Vec3(x=0.0, y=0.0, z=0.0) listener.position = Vec3(x=0.0, y=0.0, z=3.0) for tick in range(1, 8): apply_tick_plan( world, tick, TickPlan( source="test", directives=[ NpcDirective( npc_id=speaker.id, action="speak", target_npc_id=listener.id, message=f"Message {tick}.", ) ], ), ) assert len(speaker.memory) == 5 assert speaker.memory_summary is not None assert "Ada arrived as a citizen." in speaker.memory_summary assert "Message 1." in speaker.memory_summary assert "Message 7." in speaker.memory[-1].text def test_dead_npc_stays_in_place_and_cannot_act() -> None: world = create_world(_config(npc_count=2)) dead, target = world.npcs dead.position = Vec3(x=0.0, y=0.0, z=0.0) dead.health = -1 apply_tick_plan( world, 1, TickPlan( source="test", directives=[ NpcDirective( npc_id=dead.id, action="strike", target_npc_id=target.id, ) ], ), ) assert dead.position == Vec3(x=0.0, y=0.0, z=0.0) assert dead.intention == "dead" assert target.health == 100 def test_two_npcs_can_walk_to_same_destination_in_one_tick() -> None: world = create_world(_config(npc_count=2)) first, second = world.npcs first.position = Vec3(x=-2.0, y=0.0, z=2.0) second.position = Vec3(x=6.0, y=0.0, z=2.0) apply_tick_plan( world, 1, TickPlan( source="test", directives=[ NpcDirective( npc_id=first.id, action="move", target=Vec3(x=2.0, y=0.0, z=2.0), ), NpcDirective( npc_id=second.id, action="move", target=Vec3(x=2.0, y=0.0, z=2.0), ), ], ), ) assert first.position == Vec3(x=2.0, y=0.0, z=2.0) assert second.position == Vec3(x=2.0, y=0.0, z=2.0) assert first.intention == "walking" assert second.intention == "walking" def test_npc_can_walk_into_occupied_position() -> None: world = create_world(_config(npc_count=2)) first, second = world.npcs first.position = Vec3(x=-2.0, y=0.0, z=2.0) second.position = Vec3(x=2.0, y=0.0, z=2.0) apply_tick_plan( world, 1, TickPlan( source="test", directives=[ NpcDirective( npc_id=first.id, action="move", target=Vec3(x=2.0, y=0.0, z=2.0), ) ], ), ) assert first.position == Vec3(x=2.0, y=0.0, z=2.0) assert second.position == Vec3(x=2.0, y=0.0, z=2.0) assert first.intention == "walking" def test_attack_that_drops_health_below_zero_marks_target_dead() -> None: world = create_world(_config(npc_count=2)) attacker, target = world.npcs attacker.position = Vec3(x=0.0, y=0.0, z=0.0) target.position = Vec3(x=1.0, y=0.0, z=0.0) attacker.attack_damage = 10 target.health = 5 attacker.god_directive = "Kill Boris." apply_tick_plan( world, 1, TickPlan( source="test", directives=[ NpcDirective( npc_id=attacker.id, action="strike", target_npc_id=target.id, ), NpcDirective( npc_id=target.id, action="move", target=Vec3(x=2.0, y=0.0, z=0.0), ), ], ), ) assert target.health < 0 assert target.intention == "dead" assert target.position == Vec3(x=1.0, y=0.0, z=0.0) assert target.memory[-1].text == "You died." def _config(*, npc_count: int) -> GameConfig: return GameConfig( world=WorldConfig(width=80, depth=80, terrain="plain_green", seed=42), npcs=NpcConfig(count=npc_count), simulation=SimulationConfig(tick_ms=500), server=ServerConfig(host="127.0.0.1", port=8000), )