from __future__ import annotations import json import pathlib from dataclasses import dataclass, field _SAVE_FILE = pathlib.Path(__file__).parent.parent / "saved_state.json" @dataclass class GameState: timer_state: dict = field(default_factory=lambda: { "running": False, "mode": "pomodoro", "duration_minutes": 25, "phase": "work", }) todos: list[dict] = field(default_factory=list) xp: int = 0 coins: int = 0 _next_id: int = field(default=0, repr=False) XP_PER_TODO = 10 COINS_PER_TODO = 5 XP_PER_LEVEL = 100 @property def level(self) -> int: return self.xp // self.XP_PER_LEVEL + 1 @property def xp_in_level(self) -> int: return self.xp % self.XP_PER_LEVEL def add_xp(self, amount: int) -> int: self.xp += amount _save(self) return self.xp def add_coins(self, amount: int) -> int: self.coins += amount _save(self) return self.coins def add_todo(self, task: str) -> dict: self._next_id += 1 todo = {"id": self._next_id, "task": task, "done": False} self.todos.append(todo) _save(self) return todo def complete_todo(self, todo_id: int) -> bool: """Toggle a todo's done state. Awards XP on the False→True transition.""" for t in self.todos: if t["id"] == todo_id: was_done = t["done"] t["done"] = not t["done"] if t["done"] and not was_done: self.xp += self.XP_PER_TODO self.coins += self.COINS_PER_TODO _save(self) return True return False def mark_done(self, todo_id: int) -> bool: """Force a todo to done (idempotent). Awards XP only on the first completion.""" for t in self.todos: if t["id"] == todo_id: if not t["done"]: t["done"] = True self.xp += self.XP_PER_TODO self.coins += self.COINS_PER_TODO _save(self) return True return False def remove_todo(self, todo_id: int) -> bool: before = len(self.todos) self.todos = [t for t in self.todos if t["id"] != todo_id] if len(self.todos) < before: _save(self) return True return False def _save(state: GameState) -> None: try: _SAVE_FILE.write_text( json.dumps( {"todos": state.todos, "next_id": state._next_id, "xp": state.xp, "coins": state.coins}, indent=2, ), encoding="utf-8", ) except OSError: pass def _load() -> GameState: try: data = json.loads(_SAVE_FILE.read_text(encoding="utf-8")) s = GameState(todos=data.get("todos", []), xp=data.get("xp", 0), coins=data.get("coins", 0)) s._next_id = data.get("next_id", 0) return s except (OSError, json.JSONDecodeError, KeyError): return GameState() _state = _load() def get_state() -> GameState: return _state