chore: 工程化改造前的基线快照
在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前, 先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。 此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
This commit is contained in:
commit
a7725f8812
35 files changed
+11871
No files matched your search
+129
@@ -0,0 +1,129 @@
|
||||
"""存档读写。
|
||||
|
||||
存档是纯 JSON,方便人工检查,也方便外部工具/AI 直接读取分析。
|
||||
写入采用"临时文件 + 原子替换",避免中途崩溃导致存档损坏。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from .models import GameState
|
||||
|
||||
SAVE_VERSION = 1
|
||||
|
||||
|
||||
def default_save_path() -> str:
|
||||
"""默认存档位置:项目根目录下的 saves/save.json。"""
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.join(root, "saves", "save.json")
|
||||
|
||||
|
||||
def save_state(state: GameState, path: Optional[str] = None) -> str:
|
||||
"""把状态写入磁盘,返回实际写入路径。"""
|
||||
path = path or default_save_path()
|
||||
directory = os.path.dirname(os.path.abspath(path))
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
payload = {
|
||||
"version": SAVE_VERSION,
|
||||
"saved_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"state": state.to_dict(),
|
||||
}
|
||||
# 注意:to_dict() 里的派生字段在读取时会被忽略,这里只取原始字段
|
||||
payload["state"] = _raw_state(state)
|
||||
|
||||
tmp_path = path + ".tmp"
|
||||
with open(tmp_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(tmp_path, path)
|
||||
return path
|
||||
|
||||
|
||||
def _raw_state(state: GameState) -> Dict[str, Any]:
|
||||
"""导出可完整还原的原始字段(与 to_dict 的展示字段不同)。"""
|
||||
return {
|
||||
"day": state.day,
|
||||
"gold": state.gold,
|
||||
"level": state.level,
|
||||
"exp": state.exp,
|
||||
"ap": state.ap,
|
||||
"feed": state.feed,
|
||||
"plots": [
|
||||
{
|
||||
"index": p.index,
|
||||
"crop": p.crop,
|
||||
"growth": p.growth,
|
||||
"moisture": p.moisture,
|
||||
"planted_day": p.planted_day,
|
||||
}
|
||||
for p in state.plots
|
||||
],
|
||||
"animals": [
|
||||
{
|
||||
"id": a.aid,
|
||||
"kind": a.kind,
|
||||
"fed": a.fed,
|
||||
"progress": a.progress,
|
||||
"starve": a.starve,
|
||||
"pending": a.pending,
|
||||
}
|
||||
for a in state.animals
|
||||
],
|
||||
"market": state.market,
|
||||
"stats": {
|
||||
"harvested": state.total_harvested,
|
||||
"collected": state.total_collected,
|
||||
"earned": state.total_earned,
|
||||
"spent": state.total_spent,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_state(path: Optional[str] = None) -> Tuple[Optional[GameState], str]:
|
||||
"""读取存档。
|
||||
|
||||
返回 ``(state, message)``;失败时 ``state`` 为 None 并给出原因,
|
||||
调用方应当降级为"开新档"而不是崩溃。
|
||||
"""
|
||||
path = path or default_save_path()
|
||||
if not os.path.exists(path):
|
||||
return None, f"未找到存档 {path}"
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
return None, f"存档无法解析:{exc}"
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
return None, "存档格式错误:根节点不是对象"
|
||||
|
||||
version = payload.get("version", 0)
|
||||
if version != SAVE_VERSION:
|
||||
return None, f"存档版本不兼容(文件 v{version},当前 v{SAVE_VERSION})"
|
||||
|
||||
raw = payload.get("state")
|
||||
if not isinstance(raw, dict):
|
||||
return None, "存档缺少 state 字段"
|
||||
|
||||
try:
|
||||
state = GameState.from_dict(raw)
|
||||
except (TypeError, ValueError, KeyError) as exc:
|
||||
return None, f"存档内容异常:{exc}"
|
||||
|
||||
return state, f"已载入存档(第 {state.day} 天)"
|
||||
|
||||
|
||||
def delete_save(path: Optional[str] = None) -> bool:
|
||||
path = path or default_save_path()
|
||||
try:
|
||||
os.remove(path)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
Reference in new issue
Block a user