nodes/save_io.pyΒΆ
Part of Klondike Solitaire.
1"""JSON save/load for the live GameState.
2
3The engine's `simvx.core.save_manager.SaveManager` walks `Property(persist=True)`
4descriptors. The Klondike state lives in mutable Python lists (deck order, move
5history) -- not natural Property values -- so the payload is serialised by hand
6to JSON, which also keeps the save file human-readable. The crash-safe write
7itself is the engine's `simvx.core.io.atomic_write_text`.
8"""
9
10from __future__ import annotations
11
12import json
13from pathlib import Path
14
15from simvx.core.io import atomic_write_text
16
17from .game_state import GameState
18
19DEFAULT_SLOT = "klondike"
20
21
22def save_dir() -> Path:
23 """Where save files live. Resolved per call so tests can redirect it via cwd.
24
25 Nothing is created here: `atomic_write_text` makes the directory when the
26 first save is written, so merely asking whether a save exists leaves no
27 trace on disk.
28 """
29 return Path.cwd() / "saves"
30
31
32def save_path(slot: str = DEFAULT_SLOT) -> Path:
33 return save_dir() / f"{slot}.json"
34
35
36def has_save(slot: str = DEFAULT_SLOT) -> bool:
37 """Whether a save file exists, without paying to parse it."""
38 return save_path(slot).exists()
39
40
41def save_game(state: GameState, slot: str = DEFAULT_SLOT) -> Path:
42 """Write the save, keeping one rotated `.bak` of the previous one.
43
44 The rotation copies rather than moves, so the live save stays valid until
45 `atomic_write_text` swaps the new payload in.
46 """
47 target = save_path(slot)
48 if target.exists():
49 target.with_suffix(".json.bak").write_bytes(target.read_bytes())
50 return atomic_write_text(target, json.dumps(state.to_dict(), indent=2))
51
52
53def load_game(slot: str = DEFAULT_SLOT) -> GameState | None:
54 p = save_path(slot)
55 if not p.exists():
56 return None
57 try:
58 data = json.loads(p.read_text("utf-8"))
59 except (json.JSONDecodeError, OSError):
60 return None
61 try:
62 return GameState.from_dict(data)
63 except (KeyError, ValueError, TypeError):
64 return None
65
66
67def delete_save(slot: str = DEFAULT_SLOT) -> bool:
68 p = save_path(slot)
69 if p.exists():
70 p.unlink()
71 return True
72 return False