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