From 54a3e7f9be0404dae58fc2a3b0da201936f5ac3e Mon Sep 17 00:00:00 2001 From: team3 Date: Thu, 23 Jul 2026 09:41:51 +0200 Subject: [PATCH] Bot-Policies auf recherchierte Level-Timelines: Runden-Ziele, Rolldown-Runden, Slow-Roll, Not-Roll bei wenig HP Co-Authored-By: Claude Fable 5 --- backend/tests/test_game.py | 13 +++++++ backend/tft/sim/policy.py | 69 +++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/backend/tests/test_game.py b/backend/tests/test_game.py index 404409c..998e3c4 100644 --- a/backend/tests/test_game.py +++ b/backend/tests/test_game.py @@ -240,3 +240,16 @@ def test_loot_drops_components(art, cfg): comp_art = {**art, "component_pool": ["C_A", "C_B"]} game = Game(comp_art, cfg, seed=2) assert all(i in ("C_A", "C_B") for i in game.items) # Carousel-Drop + + +def test_bot_leveling_follows_targets(art, cfg): + game = Game(art, cfg, seed=12) + levels_at = {} + while not game.over and game.round["label"] != "4-3": + label = game.round["label"] + if label in ("2-6", "3-3", "4-2"): + levels_at[label] = [b.level for b in game.bots if b.alive] + game.step() + assert all(lvl >= 5 for lvl in levels_at["2-6"]) # L5 ab 2-5 + assert all(lvl >= 6 for lvl in levels_at["3-3"]) # L6 ab 3-1/3-2 + assert any(lvl >= 7 for lvl in levels_at["4-2"]) # fast8/streaker auf 7 diff --git a/backend/tft/sim/policy.py b/backend/tft/sim/policy.py index 5236966..0c6c9ee 100644 --- a/backend/tft/sim/policy.py +++ b/backend/tft/sim/policy.py @@ -1,4 +1,9 @@ -"""Scripted planning policies — drive the bots and the autoplay benchmarks.""" +"""Scripted planning policies — drive the bots and the autoplay benchmarks. + +Level-Ziele und Rolldown-Zeitpunkte nach Standard-Spielweisen (Set 17): +Baseline L4@2-1, L5@2-5, L6@3-2, L7@4-1, L8@4-5. Fast 8 spiked bei 4-2, +Streaker levelt früh und rollt klein, Reroll cappt bei 6 und slow-rollt >50. +""" import random @@ -7,18 +12,40 @@ from tft.sim.player import InvalidAction, PlayerState from tft.sim.pool import Pool ARCHETYPES = { - # fast8: früh econ, ab Stage 3 leveln, Rolldown ab Level 8. - "fast8": {"level_cap": 10, "xp_stage": 3, "econ_floor": 54, - "roll_stage": 4, "roll_level": 8, "roll_floor": 30}, - # reroll: Level-Cap 7, rollt Überschuss über 50 für Upgrades. - "reroll": {"level_cap": 7, "xp_stage": 3, "econ_floor": 50, - "roll_stage": 3, "roll_level": 7, "roll_floor": 50}, - # streaker: gibt früh aus, levelt aggressiv, wenig Reserve. - "streaker": {"level_cap": 10, "xp_stage": 2, "econ_floor": 20, - "roll_stage": 4, "roll_level": 8, "roll_floor": 20}, + "fast8": { + "level_targets": {"2-1": 4, "2-5": 5, "3-2": 6, "4-1": 7, "4-2": 8, "5-2": 9}, + "roll_rounds": {"4-2": 30, "5-2": 20}, + "roll_low_hp": 40, + "slow_roll_level": None, + }, + "streaker": { + "level_targets": {"2-1": 4, "2-5": 5, "3-1": 6, "3-5": 7, "4-5": 8, "5-5": 9}, + "roll_rounds": {"4-1": 30, "4-5": 20}, + "roll_low_hp": 50, + "slow_roll_level": None, + }, + "reroll": { + "level_targets": {"2-1": 4, "2-5": 5, "3-2": 6, "5-1": 7, "6-1": 8}, + "roll_rounds": {}, + "roll_low_hp": 40, + "slow_roll_level": 6, # ab Level 6: Gold über 50 verrollen (Slow-Roll) + }, } +def _round_key(label: str) -> tuple[int, int]: + stage, rnd = label.split("-") + return int(stage), int(rnd) + + +def _target_level(params: dict, label: str) -> int: + key = _round_key(label) + return max( + (lvl for rnd, lvl in params["level_targets"].items() if _round_key(rnd) <= key), + default=0, + ) + + def act(p: PlayerState, pool: Pool, artifact: dict, cfg: dict, rng: random.Random, rnd: dict, params: dict) -> None: """Eine Planungsrunde für einen Spieler.""" @@ -27,18 +54,28 @@ def act(p: PlayerState, pool: Pool, artifact: dict, cfg: dict, rng: random.Rando _buy_from_shop(p, pool, artifact) - stage = rnd["stage"] - while (stage >= params["xp_stage"] and p.level < params["level_cap"] - and p.gold >= params["econ_floor"] + cfg["xp"]["buy_cost"]): + label = rnd["label"] + target = _target_level(params, label) + while p.level < target and p.gold >= cfg["xp"]["buy_cost"]: try: player.buy_xp(p, cfg) except InvalidAction: break - at_target = p.level >= params["roll_level"] or p.level >= params["level_cap"] - if stage >= params["roll_stage"] and at_target: + floor = None + slow_level = params.get("slow_roll_level") + if slow_level is not None and p.level >= slow_level: + floor = 50 + if p.level >= max(params["level_targets"].values()): + floor = 50 # Endstufe erreicht: Überschuss in Upgrades rollen + if label in params["roll_rounds"]: + floor = params["roll_rounds"][label] + if rnd["stage"] >= 4 and p.hp < params["roll_low_hp"]: + floor = 0 # Not-Rolldown bei niedrigem Leben + + if floor is not None: _sell_junk(p, pool, artifact) - while p.gold > params["roll_floor"]: + while p.gold > floor: _buy_upgrades(p, pool, artifact) try: player.reroll(p, pool, cfg, rng)