From 11dc8994ff54c2364941ea9348f4104f6a88c7ec Mon Sep 17 00:00:00 2001 From: team3 Date: Thu, 23 Jul 2026 07:21:06 +0200 Subject: [PATCH] XP-System korrigiert: Level 2 gratis nach erster Welle, 1-1-Carousel-XP banken Co-Authored-By: Claude Fable 5 --- backend/tests/test_constants.py | 4 ++-- backend/tests/test_game.py | 14 ++++++++++++++ backend/tft/constants/loader.py | 6 +++--- backend/tft/constants/set17.toml | 6 +++--- backend/tft/sim/economy.py | 4 +++- backend/tft/sim/game.py | 6 ++++++ 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/backend/tests/test_constants.py b/backend/tests/test_constants.py index d8a04fc..21d1013 100644 --- a/backend/tests/test_constants.py +++ b/backend/tests/test_constants.py @@ -19,8 +19,8 @@ def test_shop_odds_shape(c): def test_xp_reaches_max_level(c): - start = c["xp"].get("start_level", 1) - assert len(c["xp"]["to_next"]) == c["xp"]["max_level"] - start + # Level 1 -> 2 ist gratis; to_next deckt 2->3 bis 9->10. + assert len(c["xp"]["to_next"]) == c["xp"]["max_level"] - 2 assert all(x > 0 for x in c["xp"]["to_next"]) diff --git a/backend/tests/test_game.py b/backend/tests/test_game.py index 9b4f7a6..df5b695 100644 --- a/backend/tests/test_game.py +++ b/backend/tests/test_game.py @@ -192,3 +192,17 @@ def test_pve_freezes_streak_and_pays_no_win_bonus(art, cfg): while game.round["stage"] == 1 and not game.over: game.step() assert game.player.streak == 0 + + +def test_natural_level_progression(art, cfg): + """Ohne XP-Kauf: Level 1 bei 1-2, Level 4 bei 2-1, Level 5 mit 6/20 bei 3-2.""" + game = Game(art, cfg, seed=6) + assert game.player.level == 1 and game.player.xp == 2 # 1-1-XP gebankt + checks = {} + while not game.over and game.round["label"] != "3-3": + checks[game.round["label"]] = (game.player.level, game.player.xp) + game.step() + assert checks["1-2"][0] == 1 + assert checks["1-3"][0] == 3 # gratis Level 2 + kaskadierte XP + assert checks["2-1"] == (4, 0) + assert checks["3-2"] == (5, 6) diff --git a/backend/tft/constants/loader.py b/backend/tft/constants/loader.py index 6d5411a..a7cc090 100644 --- a/backend/tft/constants/loader.py +++ b/backend/tft/constants/loader.py @@ -26,10 +26,10 @@ def validate(c: dict) -> None: if len(c["pool"]["sizes"]) != 5: raise ValueError("pool sizes must have 5 cost tiers") xp = c["xp"] - start = xp.get("start_level", 1) - if len(xp["to_next"]) != xp["max_level"] - start: + # Level 1 -> 2 ist gratis (erste Welle); to_next deckt 2->3 bis (max-1)->max. + if len(xp["to_next"]) != xp["max_level"] - 2: raise ValueError( - f"xp.to_next needs {xp['max_level'] - start} entries, has {len(xp['to_next'])}" + f"xp.to_next needs {xp['max_level'] - 2} entries, has {len(xp['to_next'])}" ) if len(c["shop"]["odds"]) != xp["max_level"]: raise ValueError("shop odds must have one row per level") diff --git a/backend/tft/constants/set17.toml b/backend/tft/constants/set17.toml index 9ca26fd..95c50e4 100644 --- a/backend/tft/constants/set17.toml +++ b/backend/tft/constants/set17.toml @@ -29,10 +29,10 @@ copies_for_2star = 3 copies_for_3star = 9 [xp] -# Start auf Level 1; die 2 Passiv-XP nach 1-2 heben auf Level 2. -# to_next: Index 0 = Level 1 -> 2. +# Start auf Level 1. Level 2 kommt GRATIS nach der ersten PvE-Welle (1-2), +# XP banken solange. to_next: Index 0 = Level 2 -> 3. start_level = 1 -to_next = [2, 2, 6, 10, 20, 36, 60, 68, 68] +to_next = [2, 6, 10, 20, 36, 60, 68, 68] passive_per_round = 2 buy_amount = 4 buy_cost = 4 diff --git a/backend/tft/sim/economy.py b/backend/tft/sim/economy.py index c315cc2..06cfff6 100644 --- a/backend/tft/sim/economy.py +++ b/backend/tft/sim/economy.py @@ -27,7 +27,9 @@ def xp_to_next(level: int, cfg: dict) -> int | None: xp = cfg["xp"] if level >= xp["max_level"]: return None - return xp["to_next"][level - xp.get("start_level", 1)] + if level < 2: + return None # Level 2 kommt gratis nach der ersten PvE-Welle + return xp["to_next"][level - 2] def apply_xp(level: int, xp: int, gained: int, cfg: dict) -> tuple[int, int]: diff --git a/backend/tft/sim/game.py b/backend/tft/sim/game.py index 70855c9..7f5ef2e 100644 --- a/backend/tft/sim/game.py +++ b/backend/tft/sim/game.py @@ -44,9 +44,11 @@ class Game: ] # 1-1-Carousel läuft automatisch für alle ab, dann startet 1-2. + # Die 2 Passiv-XP aus 1-1 banken (Level 2 kommt erst nach 1-2 gratis). if self.round["kind"] == "carousel": for p in self.players: self._carousel(p) + p.xp += cfg["xp"]["passive_per_round"] self.idx += 1 # Bei 1-2 gibt es noch keinen Shop — niemand hat Gold. for p in self.players: @@ -154,6 +156,10 @@ class Game: p.level, p.xp = economy.apply_xp( p.level, p.xp, self.cfg["xp"]["passive_per_round"], self.cfg ) + # Nach der ersten PvE-Welle gibt es Level 2 gratis; gebankte XP kaskadieren. + if rnd["label"] == "1-2" and p.level == 1: + p.level = 2 + p.level, p.xp = economy.apply_xp(p.level, p.xp, 0, self.cfg) self._advance()