Files
COC_API/app/game/generators/level_progression.py

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)