world-simulator / tests /test_persistence.py
DeltaZN
feat: add reset world admin btn
217ea4c
Raw
History Blame Contribute Delete
4.87 kB
from __future__ import annotations
import json
from world_simulator.api.persistence import (
BackgroundSnapshotWriter,
SnapshotStore,
build_snapshot,
restore_snapshot,
world_from_dict,
)
from world_simulator.api.players import PlayerManager
from world_simulator.config import (
GameConfig,
NpcConfig,
ServerConfig,
SimulationConfig,
WorldConfig,
)
from world_simulator.simulation.connectors.base import NpcDirective, TickPlan
from world_simulator.simulation.spawning import create_world
from world_simulator.simulation.tick import apply_tick_plan
def test_world_survives_dict_round_trip() -> None:
world = create_world(_config(npc_count=8))
# Advance a few ticks so optional/nested state (goals, memory, events) is populated.
for tick in range(1, 4):
apply_tick_plan(
world,
tick,
TickPlan(
source="test",
directives=[
NpcDirective(npc_id=world.npcs[0].id, action="speak",
target_npc_id=world.npcs[1].id, message=f"hi {tick}")
],
),
)
restored = world_from_dict(world.to_dict())
assert restored.to_dict() == world.to_dict()
def test_world_survives_json_round_trip() -> None:
world = create_world(_config(npc_count=5))
encoded = json.loads(world.to_json())
restored = world_from_dict(encoded)
assert restored.to_dict() == world.to_dict()
def test_player_manager_round_trip() -> None:
players = PlayerManager()
character = players.create(name="Pilot", icon="*", country_id="qwen", created_tick=3)
character.pending_action = {"action": "move", "target": {"x": 1.0, "y": 0.0, "z": 2.0}}
character.spawned = True
restored = PlayerManager()
restored.restore(players.snapshot())
again = restored.get(character.token)
assert again.name == "Pilot"
assert again.country_id == "qwen"
assert again.npc_id == character.npc_id
assert again.spawned is True
assert again.pending_action == character.pending_action
# Counter is preserved so new ids do not collide with restored ones.
new_character = restored.create(name="Second", icon="", created_tick=5)
assert new_character.npc_id != character.npc_id
def test_snapshot_store_loads_newest_valid_slot(tmp_path) -> None:
store = SnapshotStore(tmp_path)
world = create_world(_config(npc_count=3))
store.save(build_snapshot(world, PlayerManager().snapshot()))
world.tick = 7
store.save(build_snapshot(world, PlayerManager().snapshot()))
loaded = store.load()
assert loaded is not None
assert loaded["tick"] == 7
restored_world, _ = restore_snapshot(loaded)
assert restored_world.tick == 7
def test_snapshot_store_falls_back_when_newest_slot_is_corrupt(tmp_path) -> None:
store = SnapshotStore(tmp_path)
world = create_world(_config(npc_count=3))
store.save(build_snapshot(world, PlayerManager().snapshot())) # slot 0, tick 0
world.tick = 5
store.save(build_snapshot(world, PlayerManager().snapshot())) # slot 1, tick 5
# Corrupt the newest slot (slot 1) as if a write had been torn.
(tmp_path / "world_state.1.json").write_text("{ broken", encoding="utf-8")
fresh = SnapshotStore(tmp_path)
loaded = fresh.load()
assert loaded is not None
assert loaded["tick"] == 0 # falls back to the older, intact slot
def test_snapshot_store_clear_deletes_slots(tmp_path) -> None:
store = SnapshotStore(tmp_path)
world = create_world(_config(npc_count=3))
store.save(build_snapshot(world, PlayerManager().snapshot()))
world.tick = 4
store.save(build_snapshot(world, PlayerManager().snapshot()))
store.clear()
assert not list(tmp_path.glob("world_state.*.json"))
assert store.load() is None
# Clearing again (nothing on disk) is a no-op, not an error.
store.clear()
# The next save starts the ping-pong from slot 0 again.
store.save(build_snapshot(world, PlayerManager().snapshot()))
assert (tmp_path / "world_state.0.json").exists()
def test_background_writer_persists_latest(tmp_path) -> None:
store = SnapshotStore(tmp_path)
writer = BackgroundSnapshotWriter(store)
world = create_world(_config(npc_count=2))
for tick in range(1, 6):
world.tick = tick
writer.submit(build_snapshot(world, PlayerManager().snapshot()))
writer.close()
loaded = SnapshotStore(tmp_path).load()
assert loaded is not None
assert loaded["tick"] == 5
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),
)