M2: set17.toml game constants + validating loader
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
42
backend/tests/test_constants.py
Normal file
42
backend/tests/test_constants.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from tft.constants.loader import load_constants, validate
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def c():
|
||||||
|
return load_constants(17)
|
||||||
|
|
||||||
|
|
||||||
|
def test_loads_and_validates(c):
|
||||||
|
assert c["set"] == 17
|
||||||
|
|
||||||
|
|
||||||
|
def test_shop_odds_shape(c):
|
||||||
|
assert len(c["shop"]["odds"]) == c["xp"]["max_level"]
|
||||||
|
for row in c["shop"]["odds"]:
|
||||||
|
assert sum(row) == 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_xp_reaches_max_level(c):
|
||||||
|
assert len(c["xp"]["to_next"]) == c["xp"]["max_level"] - 1
|
||||||
|
assert all(x > 0 for x in c["xp"]["to_next"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_interest_and_streaks(c):
|
||||||
|
g = c["gold"]
|
||||||
|
assert g["interest_cap"] >= 1
|
||||||
|
streaks = g["streak_gold"]
|
||||||
|
assert streaks == sorted(streaks)
|
||||||
|
assert all(gold > 0 for _, gold in streaks)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_set_raises():
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
load_constants(99)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_catches_bad_odds(c):
|
||||||
|
c["shop"]["odds"][0] = [99, 0, 0, 0, 0]
|
||||||
|
with pytest.raises(ValueError, match="sum to 99"):
|
||||||
|
validate(c)
|
||||||
0
backend/tft/constants/__init__.py
Normal file
0
backend/tft/constants/__init__.py
Normal file
37
backend/tft/constants/loader.py
Normal file
37
backend/tft/constants/loader.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CONSTANTS_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_constants(set_number: int) -> dict:
|
||||||
|
path = CONSTANTS_DIR / f"set{set_number}.toml"
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"no constants for set {set_number}: create {path} "
|
||||||
|
f"(copy the previous set's file and adjust from patch notes)"
|
||||||
|
)
|
||||||
|
with path.open("rb") as f:
|
||||||
|
c = tomllib.load(f)
|
||||||
|
validate(c)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def validate(c: dict) -> None:
|
||||||
|
for level, row in enumerate(c["shop"]["odds"], start=1):
|
||||||
|
if sum(row) != 100:
|
||||||
|
raise ValueError(f"shop odds for level {level} sum to {sum(row)}, not 100")
|
||||||
|
if len(row) != 5:
|
||||||
|
raise ValueError(f"shop odds for level {level} must have 5 cost tiers")
|
||||||
|
if len(c["pool"]["sizes"]) != 5:
|
||||||
|
raise ValueError("pool sizes must have 5 cost tiers")
|
||||||
|
xp = c["xp"]
|
||||||
|
if len(xp["to_next"]) != xp["max_level"] - 1:
|
||||||
|
raise ValueError(
|
||||||
|
f"xp.to_next needs {xp['max_level'] - 1} entries, has {len(xp['to_next'])}"
|
||||||
|
)
|
||||||
|
if len(c["shop"]["odds"]) != xp["max_level"]:
|
||||||
|
raise ValueError("shop odds must have one row per level")
|
||||||
|
streaks = c["gold"]["streak_gold"]
|
||||||
|
if streaks != sorted(streaks):
|
||||||
|
raise ValueError("streak_gold must be sorted by streak length")
|
||||||
77
backend/tft/constants/set17.toml
Normal file
77
backend/tft/constants/set17.toml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Handgepflegte Spielregeln für Set 17 (Space Gods).
|
||||||
|
# Quellen: LoL-Wiki, Esports Tales (Stand Patch 17.7).
|
||||||
|
# Werte mit TODO gegen Patch-Notes / In-Game prüfen.
|
||||||
|
|
||||||
|
set = 17
|
||||||
|
|
||||||
|
[shop]
|
||||||
|
# Odds pro Level: [1-cost, 2-cost, 3-cost, 4-cost, 5-cost], Summe = 100.
|
||||||
|
# TODO: Quellen widersprechen sich für Level 5-9 (Patch 17.1b änderte Odds).
|
||||||
|
odds = [
|
||||||
|
[100, 0, 0, 0, 0], # Level 1
|
||||||
|
[100, 0, 0, 0, 0], # Level 2
|
||||||
|
[75, 25, 0, 0, 0], # Level 3
|
||||||
|
[55, 30, 15, 0, 0], # Level 4
|
||||||
|
[45, 33, 20, 2, 0], # Level 5
|
||||||
|
[30, 40, 25, 5, 0], # Level 6
|
||||||
|
[19, 30, 40, 10, 1], # Level 7
|
||||||
|
[17, 24, 32, 24, 3], # Level 8
|
||||||
|
[15, 18, 25, 30, 12],# Level 9
|
||||||
|
[5, 10, 20, 40, 25], # Level 10
|
||||||
|
]
|
||||||
|
reroll_cost = 2
|
||||||
|
slots = 5
|
||||||
|
|
||||||
|
[pool]
|
||||||
|
# Kopien pro Unit im geteilten Pool, nach Kosten.
|
||||||
|
sizes = [30, 25, 18, 10, 9]
|
||||||
|
copies_for_2star = 3
|
||||||
|
copies_for_3star = 9
|
||||||
|
|
||||||
|
[xp]
|
||||||
|
# XP zum nächsten Level, Index 0 = Level 1 -> 2.
|
||||||
|
to_next = [2, 2, 6, 10, 20, 36, 60, 68, 68]
|
||||||
|
passive_per_round = 2
|
||||||
|
buy_amount = 4
|
||||||
|
buy_cost = 4
|
||||||
|
max_level = 10
|
||||||
|
|
||||||
|
[gold]
|
||||||
|
base_income = 5
|
||||||
|
# Runden 1-2 bis 2-1 reduziert:
|
||||||
|
early_income = { "1-2" = 2, "1-3" = 2, "1-4" = 3, "2-1" = 4 }
|
||||||
|
interest_per_10 = 1
|
||||||
|
interest_cap = 5
|
||||||
|
win_bonus = 1
|
||||||
|
# Streak-Gold: [min_streak, gold]
|
||||||
|
streak_gold = [[3, 1], [5, 2], [6, 3]]
|
||||||
|
starting_gold = 0
|
||||||
|
|
||||||
|
[damage]
|
||||||
|
# Spielerschaden = stage_base[stage] + per_surviving_unit * überlebende Units.
|
||||||
|
# TODO: gegen aktuelle Werte prüfen.
|
||||||
|
stage_base = [0, 0, 1, 2, 8, 15, 30, 40] # Index 0 = Stage 1
|
||||||
|
per_surviving_unit = 2
|
||||||
|
player_hp = 100
|
||||||
|
|
||||||
|
[rounds]
|
||||||
|
# Stage 1: Opening-Carousel + 3 PvE-Runden.
|
||||||
|
stage1 = ["carousel", "pve", "pve", "pve"]
|
||||||
|
# Stages 2+: 7 Runden, Carousel bei x-4, PvE bei x-7.
|
||||||
|
stage_n = ["pvp", "pvp", "pvp", "carousel", "pvp", "pvp", "pve"]
|
||||||
|
augment_rounds = ["2-1", "3-2", "4-2"]
|
||||||
|
|
||||||
|
[loot]
|
||||||
|
# Vereinfachtes Loot: PvE-Drops als [components, gold].
|
||||||
|
stage1_pve = [1, 1] # pro Stage-1-PvE-Runde
|
||||||
|
stage_n_pve = [1, 2] # pro x-7-PvE-Runde
|
||||||
|
carousel = [1, 0] # Carousel gibt 1 Item-Komponente (+ Unit)
|
||||||
|
|
||||||
|
[stars]
|
||||||
|
hp_multiplier = 1.8
|
||||||
|
ad_multiplier = 1.5
|
||||||
|
|
||||||
|
[combat]
|
||||||
|
# p_win = 1 / (1 + exp(-(score_a - score_b) / tau)); tau wird in M6 kalibriert.
|
||||||
|
tau = 0.15
|
||||||
|
variance = 0.08
|
||||||
Reference in New Issue
Block a user