M2: set17.toml game constants + validating loader

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
team3
2026-07-23 00:26:53 +02:00
parent bd7cd93b48
commit 9e51c92b34
4 changed files with 156 additions and 0 deletions

View 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)

View File

View 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")

View 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