import pytest from tft.constants.loader import load_constants from tft.model import artifact as artifact_mod from tft.sim.autoplay import play_afk, play_econ from tft.sim.game import Game, InvalidAction from tests.test_sim import make_static @pytest.fixture(scope="module") def art(): static = make_static() static["meta"]["patch"] = "test" return artifact_mod.build(static) @pytest.fixture(scope="module") def cfg(): return load_constants(17) def test_afk_game_terminates_last(art, cfg): placements = [play_afk(Game(art, cfg, seed=s)) for s in range(20)] assert all(1 <= p <= 8 for p in placements) assert sum(placements) / len(placements) > 6.5 def test_econ_beats_afk(art, cfg): afk = [play_afk(Game(art, cfg, seed=s)) for s in range(30)] econ = [play_econ(Game(art, cfg, seed=1000 + s)) for s in range(30)] assert sum(econ) / len(econ) < sum(afk) / len(afk) def test_same_seed_same_outcome(art, cfg): assert play_econ(Game(art, cfg, seed=7)) == play_econ(Game(art, cfg, seed=7)) def test_buy_and_merge(art, cfg): game = Game(art, cfg, seed=1) game.gold = 100 api = "U1_0" unit = {"api_name": api, "stars": 1, "items": []} game.bench = [dict(unit), dict(unit)] game.pool.take(api, 2) game.shop = [api] * 5 game.buy(0) assert len(game.bench) == 1 assert game.bench[0]["stars"] == 2 def test_sell_refunds_pool_and_gold(art, cfg): game = Game(art, cfg, seed=1) game.gold = 100 game.shop[0] = "U1_0" before_pool = game.pool.available("U1_0") game.buy(0) gold_after_buy = game.gold game.sell("bench", len(game.bench) - 1) assert game.gold == gold_after_buy + 1 assert game.pool.available("U1_0") == before_pool def test_board_cap_is_level(art, cfg): game = Game(art, cfg, seed=1) game.bench = [{"api_name": "U1_0", "stars": 1, "items": []} for _ in range(3)] game.level = 2 game.move("bench", 0) game.move("bench", 0) with pytest.raises(InvalidAction, match="board full"): game.move("bench", 0) def test_invalid_actions_raise(art, cfg): game = Game(art, cfg, seed=1) game.gold = 0 with pytest.raises(InvalidAction): game.reroll() with pytest.raises(InvalidAction): game.buy_xp() with pytest.raises(InvalidAction): game.sell("bench", 99) def test_shop_lock_survives_step(art, cfg): game = Game(art, cfg, seed=2) game.toggle_lock() before = list(game.shop) game.step() assert game.shop == before def test_carousel_never_a_planning_round(art, cfg): game = Game(art, cfg, seed=5) seen = [] while not game.over: seen.append(game.round["kind"]) game.step() assert "carousel" not in seen assert seen[0] == "pve" # Spiel startet bei 1-2 def test_carousel_grants_unit_and_loot(art, cfg): game = Game(art, cfg, seed=5) assert len(game.bench) == 1 # Unit vom 1-1-Carousel assert len(game.items) == 1 # Item-Komponente vom Carousel def _pool_total(game, api): n = game.pool.available(api) for p in game.players: for u in p.board + p.bench: if u["api_name"] == api: n += 3 ** (u["stars"] - 1) return n def test_pool_conservation(art, cfg): for seed in (1, 2, 3): game = Game(art, cfg, seed=seed) initial = {api: cfg["pool"]["sizes"][art["static"]["units"][api]["cost"] - 1] for api in art["static"]["units"]} while not game.over: game.step() for api, total in initial.items(): assert _pool_total(game, api) == total, f"pool leak: {api}" def test_bots_start_with_one_unit(art, cfg): game = Game(art, cfg, seed=7) for b in game.bots: assert len(b.board) + len(b.bench) == 1 # nur die Carousel-Unit assert all(u["stars"] == 1 for u in b.board + b.bench) def test_streak_resets_on_win(art, cfg): game = Game(art, cfg, seed=1) game.player.streak = -3 # direkter Formel-Test wie in step(): won = True streak = max(game.player.streak, 0) + 1 if won else 0 assert streak == 1 def test_pairing(): import random from tft.sim.game import pair_players from tft.sim.player import PlayerState def mk(n): return [PlayerState(name=f"P{i}", archetype="fast8", hp=100, gold=0, level=2) for i in range(n)] rng = random.Random(1) for n in (8, 5, 3, 2): players = mk(n) pairs, odd, ghost_src = pair_players(players, rng) paired = {p.name for a, b in pairs for p in (a, b)} if odd: assert odd.name not in paired assert ghost_src is not odd assert len(paired) + 1 == n else: assert len(paired) == n # kein Wiederholungsgegner bei >2 players = mk(8) rng = random.Random(2) for _ in range(10): pairs, _, _ = pair_players(players, rng) for a, b in pairs: assert a.last_opponent == b.name def test_ghost_source_takes_no_damage(art, cfg): game = Game(art, cfg, seed=3) game.bots[0].hp = 0 game.bots[0].placement = 8 game.bots[0].board, game.bots[0].bench = [], [] # 7 Lebende -> jede PvP-Runde hat einen Ghost-Kampf; HP-Summe der # Ghost-Quelle darf nur durch ihren echten Kampf sinken (kein Doppelschaden). hp_before = {p.name: p.hp for p in game.players} while game.round["kind"] != "pvp": game.step() game.step() drops = sum(1 for p in game.players if p.hp < hp_before[p.name]) assert drops <= 4 # max. 3 Paar-Verlierer + 1 Ghost-Verlierer def test_pve_freezes_streak_and_pays_no_win_bonus(art, cfg): game = Game(art, cfg, seed=4) # Stage 1: drei PvE-Runden -> Streak bleibt 0 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, laut VOD: 1-3 Lvl 2, 1-4 Lvl 3, 2-1 Lvl 3 (2/6), 2-5 Lvl 4 (2/10).""" game = Game(art, cfg, seed=6) assert game.player.level == 1 and game.player.xp == 0 # 1-1 gibt keine XP 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"] == (1, 0) assert checks["1-3"] == (2, 0) assert checks["1-4"] == (3, 0) assert checks["2-1"] == (3, 2) assert checks["2-5"] == (4, 2) assert checks["3-2"] == (5, 0)