From c0cd303eb003df75640edf484e9ebc7378bb0ba5 Mon Sep 17 00:00:00 2001 From: team3 Date: Thu, 23 Jul 2026 06:25:23 +0200 Subject: [PATCH] Regeln nach Recherche korrigiert: Schadensformel, Odds L8/L9, Start-Level 2, Streak ab 2, Verkaufsregel Co-Authored-By: Claude Fable 5 --- backend/tests/test_constants.py | 3 ++- backend/tests/test_sim.py | 4 ++-- backend/tft/api/app.py | 4 ++-- backend/tft/constants/loader.py | 5 +++-- backend/tft/constants/set17.toml | 23 ++++++++++++----------- backend/tft/sim/economy.py | 6 +++--- backend/tft/sim/game.py | 9 +++++++-- 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/backend/tests/test_constants.py b/backend/tests/test_constants.py index 8a07c51..d8a04fc 100644 --- a/backend/tests/test_constants.py +++ b/backend/tests/test_constants.py @@ -19,7 +19,8 @@ def test_shop_odds_shape(c): def test_xp_reaches_max_level(c): - assert len(c["xp"]["to_next"]) == c["xp"]["max_level"] - 1 + start = c["xp"].get("start_level", 1) + assert len(c["xp"]["to_next"]) == c["xp"]["max_level"] - start assert all(x > 0 for x in c["xp"]["to_next"]) diff --git a/backend/tests/test_sim.py b/backend/tests/test_sim.py index 8f84b84..2a6cf46 100644 --- a/backend/tests/test_sim.py +++ b/backend/tests/test_sim.py @@ -78,8 +78,8 @@ def test_streak_gold(cfg): def test_apply_xp_levels_up(cfg): - level, xp = economy.apply_xp(1, 0, 4, cfg) - assert level == 3 # 1->2 costs 2, 2->3 costs 2 + level, xp = economy.apply_xp(2, 0, 8, cfg) + assert level == 4 # 2->3 kostet 2, 3->4 kostet 6 assert xp == 0 level, _ = economy.apply_xp(9, 0, 1000, cfg) assert level == cfg["xp"]["max_level"] diff --git a/backend/tft/api/app.py b/backend/tft/api/app.py index 6a57391..a766aee 100644 --- a/backend/tft/api/app.py +++ b/backend/tft/api/app.py @@ -6,6 +6,7 @@ from pydantic import BaseModel from tft import paths from tft.model.score import active_trait_tiers +from tft.sim import economy from tft.sim.game import Game, InvalidAction app = FastAPI(title="tft-sim") @@ -150,8 +151,7 @@ def serialize(game: Game) -> dict: ], "player": { "hp": game.hp, "gold": game.gold, "level": game.level, "xp": game.xp, - "xp_needed": game.cfg["xp"]["to_next"][game.level - 1] - if game.level < game.cfg["xp"]["max_level"] else None, + "xp_needed": economy.xp_to_next(game.level, game.cfg), "streak": game.streak, "score": round(game.player_score(), 1), }, diff --git a/backend/tft/constants/loader.py b/backend/tft/constants/loader.py index 0f41e8f..6d5411a 100644 --- a/backend/tft/constants/loader.py +++ b/backend/tft/constants/loader.py @@ -26,9 +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"] - if len(xp["to_next"]) != xp["max_level"] - 1: + start = xp.get("start_level", 1) + if len(xp["to_next"]) != xp["max_level"] - start: raise ValueError( - f"xp.to_next needs {xp['max_level'] - 1} entries, has {len(xp['to_next'])}" + f"xp.to_next needs {xp['max_level'] - start} 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 d1e1138..2711771 100644 --- a/backend/tft/constants/set17.toml +++ b/backend/tft/constants/set17.toml @@ -6,7 +6,7 @@ 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). +# Level 1-7 mehrquellig bestätigt; L8/L9 = Zwei-Quellen-Mehrheit (metabot, redeemertft). odds = [ [100, 0, 0, 0, 0], # Level 1 [100, 0, 0, 0, 0], # Level 2 @@ -15,8 +15,8 @@ odds = [ [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 + [15, 20, 32, 30, 3], # Level 8 + [10, 17, 25, 33, 15],# Level 9 [5, 10, 20, 40, 25], # Level 10 ] reroll_cost = 2 @@ -29,8 +29,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] +# Spiel startet auf Level 2. XP zum nächsten Level, Index 0 = Level 2 -> 3. +start_level = 2 +to_next = [2, 6, 10, 20, 36, 60, 68, 68] passive_per_round = 2 buy_amount = 4 buy_cost = 4 @@ -43,15 +44,15 @@ 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]] +# Streak-Gold: [min_streak, gold] — ab 2er-Streak +1 (tft-lab, op.gg) +streak_gold = [[2, 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 +# Spielerschaden = stage_base[stage] + 1 pro überlebender Unit (flach, seit Set 11). +# Mehrquellig bestätigt: tft.ninja, op.gg, dotesports. +stage_base = [0, 2, 5, 8, 10, 12, 17, 17] # Index 0 = Stage 1 +per_surviving_unit = 1 player_hp = 100 [rounds] diff --git a/backend/tft/sim/economy.py b/backend/tft/sim/economy.py index 069e33d..c315cc2 100644 --- a/backend/tft/sim/economy.py +++ b/backend/tft/sim/economy.py @@ -24,10 +24,10 @@ def round_income(gold: int, streak: int, won: bool, round_label: str, cfg: dict) def xp_to_next(level: int, cfg: dict) -> int | None: - to_next = cfg["xp"]["to_next"] - if level >= cfg["xp"]["max_level"]: + xp = cfg["xp"] + if level >= xp["max_level"]: return None - return to_next[level - 1] + return xp["to_next"][level - xp.get("start_level", 1)] 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 f81d807..ca606cd 100644 --- a/backend/tft/sim/game.py +++ b/backend/tft/sim/game.py @@ -28,7 +28,7 @@ class Game: self.hp = cfg["damage"]["player_hp"] self.gold = cfg["gold"]["starting_gold"] - self.level = 1 + self.level = cfg["xp"].get("start_level", 1) self.xp = 0 self.board: list[dict] = [] self.bench: list[dict] = [] @@ -133,7 +133,12 @@ class Game: u = units.pop(idx) copies = 3 ** (u["stars"] - 1) self.pool.put_back(u["api_name"], copies) - self.gold += self.artifact["static"]["units"][u["api_name"]]["cost"] * copies + cost = self.artifact["static"]["units"][u["api_name"]]["cost"] + refund = cost * copies + # Ab 2 Sternen und Kosten >= 2 gilt -1 Gold; 1-Coster immer voller Wert. + if cost > 1 and u["stars"] >= 2: + refund -= 1 + self.gold += refund self.items.extend(u["items"]) def reroll(self) -> None: