"""Tests for save/load game state serialization.""" from __future__ import annotations import json import shutil from pathlib import Path import pytest from h2g2.engine.game_object import Flag from h2g2.engine.save_load import ( SAVE_DIR, SAVE_EXTENSION, _serialize_state, _restore_state, save_game, load_game, list_saves, ) @pytest.fixture(autouse=True) def _clean_save_dir(): """Ensure a clean save directory for every test.""" if SAVE_DIR.exists(): shutil.rmtree(SAVE_DIR) SAVE_DIR.mkdir(parents=True, exist_ok=True) yield if SAVE_DIR.exists(): shutil.rmtree(SAVE_DIR) # --------------------------------------------------------------------------- # list_saves # --------------------------------------------------------------------------- class TestListSaves: def test_empty_directory(self): assert list_saves() == [] def test_returns_save_names(self): (SAVE_DIR / "alpha.json").write_text("{}") (SAVE_DIR / "beta.json").write_text("{}") names = list_saves() assert "alpha" in names assert "beta" in names def test_ignores_non_json_files(self): (SAVE_DIR / "notes.txt").write_text("not a save") (SAVE_DIR / "real.json").write_text("{}") assert list_saves() == ["real"] # --------------------------------------------------------------------------- # Round-trip: serialize -> restore # --------------------------------------------------------------------------- class TestSerializeRestore: def test_scalar_state_round_trips(self, game): state, _, _ = game state.score = 42 state.moves = 17 state.verbosity = 2 state.lying_down = False state.dreaming = True state.flags["headache"] = False state.flags["beer_counter"] = 3 data = _serialize_state(state) # Mutate state to prove restore actually changes it state.score = 0 state.moves = 0 state.verbosity = 0 state.lying_down = True state.dreaming = False state.flags["headache"] = True state.flags["beer_counter"] = 0 _restore_state(state, data) assert state.score == 42 assert state.moves == 17 assert state.verbosity == 2 assert state.lying_down is False assert state.dreaming is True assert state.flags["headache"] is False assert state.flags["beer_counter"] == 3 def test_room_round_trips(self, game): state, _, _ = game bedroom = state.world.get_room("BEDROOM") state.here = bedroom data = _serialize_state(state) state.here = None _restore_state(state, data) assert state.here is bedroom def test_protagonist_round_trips(self, game): state, _, _ = game proto = state.protagonist data = _serialize_state(state) state.protagonist = None _restore_state(state, data) assert state.protagonist is proto def test_object_flags_round_trip(self, game): state, _, _ = game obj = state.world.get("TOWEL") obj.fset(Flag.WORNBIT) assert obj.fset_q(Flag.WORNBIT) data = _serialize_state(state) obj.fclear(Flag.WORNBIT) assert not obj.fset_q(Flag.WORNBIT) _restore_state(state, data) assert obj.fset_q(Flag.WORNBIT) def test_containment_round_trip(self, game): state, _, _ = game towel = state.world.get("TOWEL") proto = state.protagonist # Move towel into inventory towel.move_to(proto) assert towel.parent is proto assert towel in proto.children data = _serialize_state(state) # Move towel somewhere else to prove restore works bedroom = state.world.get_room("BEDROOM") towel.move_to(bedroom) assert towel.parent is bedroom _restore_state(state, data) assert towel.parent is proto assert towel in proto.children def test_last_action_round_trips(self, game): state, _, _ = game towel = state.world.get("TOWEL") state.l_prsa = "take" state.l_prso = towel state.l_prsi = None data = _serialize_state(state) state.l_prsa = None state.l_prso = None _restore_state(state, data) assert state.l_prsa == "take" assert state.l_prso is towel def test_clock_state_round_trips(self, game): state, _, _ = game # Find a named clock entry and modify it named = [e for e in state.clock.entries if e.name] if not named: pytest.skip("No named clock entries in test fixture") entry = named[0] original_tick = entry.tick entry.tick = 99 entry.enabled = False data = _serialize_state(state) entry.tick = 0 entry.enabled = True _restore_state(state, data) assert entry.tick == 99 assert entry.enabled is False # Restore original for other tests entry.tick = original_tick entry.enabled = True def test_identity_flag_round_trips(self, game): state, _, _ = game arthur = state.world.objects.get("ARTHUR") if arthur is None: pytest.skip("ARTHUR object not in test fixture") state.identity_flag = arthur data = _serialize_state(state) state.identity_flag = None _restore_state(state, data) assert state.identity_flag is arthur # --------------------------------------------------------------------------- # File I/O: save_game / load_game # --------------------------------------------------------------------------- class TestFileIO: def test_save_creates_file(self, game): state, _, _ = game path = save_game(state, "test1") assert Path(path).exists() assert Path(path).suffix == SAVE_EXTENSION def test_save_file_is_valid_json(self, game): state, _, _ = game save_game(state, "test1") data = json.loads((SAVE_DIR / "test1.json").read_text()) assert data["version"] == 1 assert "objects" in data assert "flags" in data def test_load_restores_state(self, game): state, _, _ = game state.score = 100 state.moves = 50 save_game(state, "mysave") state.score = 0 state.moves = 0 load_game(state, "mysave") assert state.score == 100 assert state.moves == 50 def test_load_nonexistent_raises(self, game): state, _, _ = game with pytest.raises(FileNotFoundError): load_game(state, "doesnotexist") def test_overwrite_existing_save(self, game): state, _, _ = game state.score = 10 save_game(state, "slot") state.score = 99 save_game(state, "slot") state.score = 0 load_game(state, "slot") assert state.score == 99 def test_multiple_independent_saves(self, game): state, _, _ = game state.score = 10 save_game(state, "early") state.score = 200 save_game(state, "late") load_game(state, "early") assert state.score == 10 load_game(state, "late") assert state.score == 200 def test_full_round_trip_with_mutations(self, game): state, _, _ = game proto = state.protagonist towel = state.world.get("TOWEL") # Mutate the game state.score = 42 state.moves = 15 state.lying_down = False state.flags["earth_demolished"] = True towel.move_to(proto) towel.fset(Flag.TOUCHBIT) save_game(state, "full") # Trash everything state.score = 0 state.moves = 0 state.lying_down = True state.flags["earth_demolished"] = False bedroom = state.world.get_room("BEDROOM") towel.move_to(bedroom) towel.fclear(Flag.TOUCHBIT) load_game(state, "full") assert state.score == 42 assert state.moves == 15 assert state.lying_down is False assert state.flags["earth_demolished"] is True assert towel.parent is proto assert towel.fset_q(Flag.TOUCHBIT)