from __future__ import annotations from dataclasses import dataclass from bisect import bisect_right from typing import List, Dict, Any, Optional @dataclass(frozen=True) class LevelThreshold: level: int xp: int # cumulative XP required to *reach* this level rewards: Dict[str, Any] # optional; can be empty class LevelProgression: """ Provides XP<->Level mapping. Can be built from a parametric curve or YAML-like dict. """ def __init__(self, thresholds: List[LevelThreshold]): # Normalize, unique-by-level, sorted uniq = {} for t in thresholds: uniq[t.level] = LevelThreshold(level=t.level, xp=int(t.xp), rewards=(t.rewards or {})) levels = sorted(uniq.values(), key=lambda x: x.level) if not levels or levels[0].level != 1 or levels[0].xp != 0: raise ValueError("Progression must start at level 1 with xp=0") object.__setattr__(self, "_levels", levels) object.__setattr__(self, "_xp_list", [t.xp for t in levels]) object.__setattr__(self, "_level_list", [t.level for t in levels]) @classmethod def from_curve(cls, max_level: int = 100, base_xp: int = 100, growth: float = 1.45) -> "LevelProgression": """ Exponential-ish cumulative thresholds: XP(level L) = round(base_xp * ((growth^(L-1) - 1) / (growth - 1))) Ensures level 1 => 0 XP. """ thresholds: List[LevelThreshold] = [] for L in range(1, max_level + 1): if L == 1: xp = 0 else: # cumulative sum of geometric series xp = round(base_xp * ((growth ** (L - 1) - 1) / (growth - 1))) thresholds.append(LevelThreshold(level=L, xp=xp, rewards={})) return cls(thresholds) # --------- Queries ---------- @property def max_level(self) -> int: return self._level_list[-1] def xp_for_level(self, level: int) -> int: if level < 1: return 0 if level >= self.max_level: return self._xp_list[-1] # Levels are dense and 1-indexed idx = level - 1 return self._xp_list[idx] def level_for_xp(self, xp: int) -> int: """ Find the highest level where threshold.xp <= xp. Levels are dense. """ if xp < 0: return 1 idx = bisect_right(self._xp_list, xp) - 1 if idx < 0: return 1 return self._level_list[idx] def rewards_for_level(self, level: int) -> Dict[str, Any]: if level < 2: return {} if level > self.max_level: level = self.max_level return self._levels[level - 1].rewards or {} LEVEL_CURVE = 1.25 DEFAULT_LEVEL_PROGRESSION = LevelProgression.from_curve(growth=LEVEL_CURVE)