73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
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) |