Download data/save_system.py from cereeenn120/Hogwarts_SBox_AI-RPG: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/cereeenn120/Hogwarts_SBox_AI-RPG/resolve/main/data/save_system.py
- Command line
-
hf download hf://spaces/cereeenn120/Hogwarts_SBox_AI-RPG/data/save_system.py
-
curl -L -o save_system.py https://huggingface.co/spaces/cereeenn120/Hogwarts_SBox_AI-RPG/resolve/main/data/save_system.py
10.3 kB
| """ | |
| save_system.py β Local JSON save/load system. | |
| v2: Browser download / upload ile local file-based save. | |
| HF Dataset baΔΔ±mlΔ±lΔ±ΔΔ± kaldΔ±rΔ±ldΔ±. | |
| Multi-save doΔal olarak destekleniyor β oyuncu istediΔi kadar | |
| .json dosyasΔ± saklayabilir, istediΔini yΓΌkleyebilir. | |
| Save dosyasΔ± adΔ±: {karakter}_{house}_lv{level}_{zaman}.json | |
| Save formatΔ±: | |
| { | |
| "version": 2, | |
| "saved_at": "ISO timestamp", | |
| "character_name": "...", | |
| "house": "...", | |
| "year": N, | |
| "level": N, | |
| "state": { ... full player state ... }, | |
| "history": [ ... chatbot history ... ] | |
| } | |
| """ | |
| import copy | |
| import json | |
| import logging | |
| import os | |
| import re | |
| import tempfile | |
| import uuid | |
| from datetime import datetime | |
| from game.logging_config import event, get_logger, state_fields | |
| from game.player import safe_player_name | |
| from game.presentation import narrative_choices_allowed | |
| from systems.festival_system import init_festivals | |
| from systems.interaction_system import init_interaction | |
| from systems.journal_system import init_journal | |
| from systems.location_system import init_location | |
| from systems.quest_system import init_quests | |
| from systems.time_system import init_time | |
| SAVE_VERSION = 3 | |
| logger = get_logger("save") | |
| def export_save(state, history): | |
| """Return a portable JSON document without creating a UI or a file.""" | |
| return { | |
| "version": SAVE_VERSION, "saved_at": datetime.now().isoformat(), | |
| "character_name": safe_player_name(state.get("name")), | |
| "house": state.get("house"), "year": state.get("year"), | |
| "level": state.get("level", 1), | |
| "state": copy.deepcopy(state), "history": copy.deepcopy(history), | |
| } | |
| def restore_save(document): | |
| """Validate and migrate a save on a private copy; never mutate a live game.""" | |
| if not isinstance(document, dict): | |
| raise ValueError("Choose a valid Hogwarts JSON save.") | |
| if "version" in document and document["version"] not in (1, 2, SAVE_VERSION): | |
| raise ValueError("This save version is not supported.") | |
| state = copy.deepcopy(document.get("state", document)) | |
| history = copy.deepcopy(document.get("history", [])) | |
| if not isinstance(state, dict) or not isinstance(state.get("name"), str): | |
| raise ValueError("The save is missing a character.") | |
| if state.get("house", "Gryffindor") not in {"Gryffindor", "Slytherin", "Ravenclaw", "Hufflepuff"}: | |
| raise ValueError("Invalid house in save.") | |
| if type(state.get("year", 3)) is not int or not 1 <= state.get("year", 3) <= 7: | |
| raise ValueError("Invalid school year in save.") | |
| for key in ("strength", "agility", "intelligence", "charisma", "willpower", "xp", "level", | |
| "credits", "unspent_stat_points", "light_dark", "order_chaos", "trouble_level", | |
| "reputation", "house_points", "_state_revision"): | |
| if key in state and type(state[key]) is not int: | |
| raise ValueError("Invalid numeric field: " + key) | |
| for key in ("traits", "aspirations", "discovered_locations"): | |
| if key in state and (not isinstance(state[key], list) or not all(isinstance(v, str) for v in state[key])): | |
| raise ValueError("Invalid list: " + key) | |
| for key in ("memory", "detention", "quests", "relationships", "dynamic_npcs", "known_spells", | |
| "journal", "interaction", "_ui_cache", "action_clock", "social_daily", "schedule_results"): | |
| if key in state and not isinstance(state[key], dict): | |
| raise ValueError("Invalid object: " + key) | |
| for key in ("inventory", "world_events", "_enrichment"): | |
| if key in state and (not isinstance(state[key], list) or not all(isinstance(v, dict) for v in state[key])): | |
| raise ValueError("Invalid entries: " + key) | |
| if not isinstance(history, list) or not all( | |
| isinstance(row, (list, tuple)) and len(row) == 2 | |
| and all(isinstance(v, str) or v is None for v in row) for row in history | |
| ): | |
| raise ValueError("Invalid story history.") | |
| # A loaded game must not attach to another game's outstanding background jobs. | |
| state["id"] = str(uuid.uuid4()) | |
| state.pop("_enrichment", None) | |
| try: | |
| _backfill_state(state) | |
| from game.action_engine import sync_mode | |
| sync_mode(state) | |
| except (TypeError, KeyError, AttributeError, ValueError, IndexError) as exc: | |
| raise ValueError("The save contains invalid game data.") from exc | |
| state["_ui_notice"] = "Game restored. Your turn β choose an action below." | |
| return state, history | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SAVE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def prepare_save_download(state_json: str, history_json) -> str: | |
| """ | |
| Oyun state'ini + chatbot geΓ§miΕini bir JSON dosyasΔ±na yazar. | |
| Gradio gr.File output iΓ§in dosya yolunu dΓΆndΓΌrΓΌr. | |
| app.py'deki save_wrapper tarafΔ±ndan Γ§aΔrΔ±lΔ±r. | |
| state_json : json.dumps(state) β string | |
| history_json : json.dumps(history) veya doΔrudan liste | |
| DΓΆndΓΌrΓΌr : YazΔ±lan dosyanΔ±n absolute path'i | |
| """ | |
| state = json.loads(state_json) | |
| if isinstance(history_json, str): | |
| history = json.loads(history_json) if history_json else [] | |
| else: | |
| history = history_json or [] | |
| event(logger, logging.DEBUG, "save.started", history_turns=len(history), **state_fields(state)) | |
| # Dosya adΔ±: gΓΌvenli, okunabilir | |
| state["name"] = safe_player_name(state.get("name")) | |
| char_slug = re.sub(r"[^a-z0-9]+", "_", state["name"].lower()).strip("_") or "player" | |
| house_slug = re.sub(r"[^a-z0-9]+", "_", state.get("house", "unknown").strip().lower()).strip("_") | |
| level = state.get("level", 1) | |
| ts = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| filename = f"{char_slug}_{house_slug}_lv{level}_{ts}.json" | |
| save_data = export_save(state, history) | |
| save_dir = os.path.join(tempfile.gettempdir(), "hogwarts_saves") | |
| os.makedirs(save_dir, exist_ok=True) | |
| file_path = os.path.join(save_dir, filename) | |
| with open(file_path, "w", encoding="utf-8") as f: | |
| json.dump(save_data, f, ensure_ascii=False, indent=2) | |
| size_kb = os.path.getsize(file_path) / 1024 | |
| event(logger, logging.INFO, "save.completed", filename=filename, size_kb=round(size_kb, 1), save_version=SAVE_VERSION, **state_fields(state)) | |
| return file_path | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LOAD | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HELPERS β BACKFILL & FORMAT | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _backfill_state(state: dict) -> None: | |
| """ | |
| Eski save dosyalarΔ±ndaki eksik alanlarΔ± gΓΌvenli biΓ§imde doldurur. | |
| Δ°dempotent β zaten var olan alanlarΔ± ezmez. | |
| """ | |
| state["name"] = safe_player_name(state.get("name")) | |
| # Repair missing canonical avatars in saves made before bundled art existed. | |
| if state.get("is_canonical_character") and not ( | |
| state.get("avatar") and os.path.isfile(state["avatar"]) | |
| ): | |
| from game.vlm_engine import get_static_npc_image | |
| state["avatar"] = get_static_npc_image(state.get("canonical_npc_id")) | |
| state.setdefault("id", str(uuid.uuid4())) | |
| original_keys = set(state) | |
| defaults = { | |
| "strength": 5, "agility": 5, "intelligence": 5, | |
| "charisma": 5, "willpower": 5, | |
| "light_dark": 0, "order_chaos": 0, "trouble_level": 0, | |
| "reputation": 0, "house_points": 0, | |
| "house": "Gryffindor", "year": 3, | |
| "traits": [], "aspirations": [], | |
| "credits": 150, "inventory": [], | |
| "relationships": {}, "dynamic_npcs": {}, "known_spells": {}, | |
| "xp": 0, "level": 1, "unspent_stat_points": 0, | |
| "quests": {}, | |
| "detention": { | |
| "active": False, "assigned_by": None, "reason": None, | |
| "turns_remaining": 0, "total_count": 0, | |
| }, | |
| "memory": { | |
| "long_term": [], "summary": "", | |
| "turns_since_summary": 0, "summary_interval": 5, | |
| }, | |
| } | |
| for key, default in defaults.items(): | |
| if key not in state: | |
| state[key] = default | |
| # Alt-alan backfill | |
| mem = state.setdefault("memory", {}) | |
| mem.setdefault("long_term", []) | |
| mem.setdefault("summary", "") | |
| mem.setdefault("turns_since_summary", 0) | |
| mem.setdefault("summary_interval", 5) | |
| det = state.setdefault("detention", {}) | |
| det.setdefault("active", False) | |
| det.setdefault("assigned_by", None) | |
| det.setdefault("reason", None) | |
| det.setdefault("turns_remaining", 0) | |
| det.setdefault("total_count", 0) | |
| # Eksik sistemleri baΕlat | |
| if not state.get("time") or not state.get("schedule"): | |
| init_time(state, state.get("year", 3)) | |
| init_journal(state) | |
| init_quests(state) | |
| init_festivals(state) | |
| init_location(state) # idempotent β won't overwrite existing location | |
| init_interaction(state) | |
| state.pop("_interaction_instruction", None) | |
| # UI cache backfill (eski save'lerde yoksa) | |
| state.setdefault("_ui_cache", { | |
| "choices": [""] * 5, | |
| "scene_path": None, | |
| "npc_path": None, | |
| "item_path": None, | |
| }) | |
| if not narrative_choices_allowed(state): | |
| state["_ui_cache"]["choices"] = [""] * 5 | |
| added = sorted(set(state) - original_keys) | |
| event(logger, logging.INFO if added else logging.DEBUG, "load.state_backfilled", added_fields=added, **state_fields(state)) | |