"""Ein ganzes Spiel durchspielen und dabei die Regeln prüfen. Die Einzeltests decken jede Funktion für sich ab. Hier geht es um das Zusammenspiel über 30 Runden: bleibt Gold nicht-negativ, verschwinden keine Units, endet das Spiel, stimmt der Pool. """ import random import pytest from tft.constants.loader import load_constants from tft.model import artifact as artifact_mod from tft.sim import player as sim_player from tft.sim.game import Game SET = 18 @pytest.fixture(scope="module") def deps(): try: return artifact_mod.load(SET), load_constants(SET) except (FileNotFoundError, KeyError): pytest.skip("kein Artefakt für Set 18 gebaut") def act(game): """Wie ein einfacher Spieler handeln: Level kaufen, Shop leeren, aufstellen. Ohne das Level-Kaufen bleibt die Bank nach wenigen Runden voll und das Spiel prüft nichts mehr — Gold staut sich, das Board wächst nicht. """ while game.player.gold >= 12: try: game.buy_xp() except Exception: break for slot in range(len(game.player.shop or [])): try: game.buy(slot) except Exception: pass for _ in range(len(game.player.bench)): try: game.move("bench", 0) except Exception: break # Volle Bank blockiert jeden weiteren Kauf. while len(game.player.bench) >= sim_player.BENCH_SIZE: game.sell("bench", 0) def play(deps, seed, steps=60): artifact, cfg = deps game = Game(artifact, cfg, seed=seed) seen = [] for _ in range(steps): if game.over: break act(game) seen.append({ "round": game.round["label"], "hp": game.player.hp, "gold": game.player.gold, "level": game.player.level, "board": len(game.player.board), "bench": len(game.player.bench), }) game.step() return game, seen def test_a_game_runs_to_the_end(deps): game, seen = play(deps, seed=1) assert len(seen) > 5 assert game.over or game.round["label"] def test_gold_never_goes_negative(deps): _, seen = play(deps, seed=2) assert all(s["gold"] >= 0 for s in seen) def test_health_only_falls(deps): _, seen = play(deps, seed=3) hps = [s["hp"] for s in seen] assert hps == sorted(hps, reverse=True) def test_the_board_never_exceeds_the_level(deps): """Mehr Units als Level wäre ein Regelbruch.""" artifact, cfg = deps game = Game(artifact, cfg, seed=4) for _ in range(40): if game.over: break act(game) assert len(game.player.board) <= game.player.level game.step() def test_the_bench_has_a_limit(deps): artifact, cfg = deps game = Game(artifact, cfg, seed=5) for _ in range(30): if game.over: break act(game) assert len(game.player.bench) <= sim_player.BENCH_SIZE game.step() def test_every_unit_sits_on_its_own_hex(deps): artifact, cfg = deps game = Game(artifact, cfg, seed=6) for _ in range(40): if game.over: break act(game) hexes = [u.get("hex") for u in game.player.board] assert len(hexes) == len(set(hexes)), "zwei Units auf demselben Feld" assert all(h is not None for h in hexes) game.step() def test_the_same_seed_gives_the_same_game(deps): """Ohne das lässt sich kein Kampf nachspielen und nichts vergleichen.""" _, first = play(deps, seed=42, steps=20) _, again = play(deps, seed=42, steps=20) assert first == again def test_players_drop_out_over_time(deps): artifact, cfg = deps game = Game(artifact, cfg, seed=7) for _ in range(60): if game.over: break game.step() alive = [p for p in game.players if p.hp > 0] assert len(alive) < len(game.players) def test_fights_produce_a_result(deps): """Jede PvP-Runde muss einen Sieger oder ein Unentschieden liefern.""" artifact, cfg = deps game = Game(artifact, cfg, seed=8) seen = 0 for _ in range(30): if game.over: break before = [p.hp for p in game.players] game.step() after = [p.hp for p in game.players] if before != after: seen += 1 assert seen > 0, "in 30 Runden hat niemand Leben verloren" def test_the_pool_stays_complete(deps): """Jede Kopie eines Champions ist entweder im Pool oder auf einem Board. Verschwinden welche, sind Käufe oder Verkäufe kaputt. Auftauchen dürfen auch keine, sonst wäre der Pool unendlich. """ from collections import Counter artifact, cfg = deps game = Game(artifact, cfg, seed=9) sizes = cfg["pool"]["sizes"] units = artifact["static"]["units"] start = {api: sizes[u["cost"] - 1] for api, u in units.items() if 1 <= u["cost"] <= len(sizes)} def total(): held = Counter() for p in game.players: for u in p.board + p.bench: held[u["api_name"]] += 3 ** (u["stars"] - 1) return {api: game.pool.available(api) + held[api] for api in start} for _ in range(30): if game.over: break act(game) wrong = {api: (start[api], now) for api, now in total().items() if now != start[api]} assert not wrong, f"Kopien verschwunden oder erfunden: {list(wrong)[:3]}" game.step() def test_a_full_bench_blocks_buying(deps): """Sonst kauft man Units ins Nichts.""" artifact, cfg = deps game = Game(artifact, cfg, seed=10) for _ in range(40): if game.over: break if len(game.player.bench) >= sim_player.BENCH_SIZE and game.player.shop: before = len(game.player.bench) with pytest.raises(Exception): game.buy(0) assert len(game.player.bench) == before return act_without_selling(game) game.step() pytest.skip("Bank wurde in 40 Runden nie voll") def act_without_selling(game): for slot in range(len(game.player.shop or [])): try: game.buy(slot) except Exception: pass def test_the_players_fight_is_recorded(deps): """Ohne Protokoll zeigt die Anzeige nichts oder den falschen Kampf.""" artifact, cfg = deps game = Game(artifact, cfg, seed=11) for _ in range(20): if game.over: break act(game) game.step() if game.last_battle: log = game.last_battle assert log["units"] and log["events"] # Der Spieler steht in der Anzeige immer auf Seite "a". mine = [u for u in log["units"] if u["side"] == "a"] assert len(mine) == len(game.player.board) or game.over return pytest.skip("in 20 Runden kam kein Kampf mit Board zustande") def test_the_player_is_always_side_a(deps): """Sonst zeichnet die Anzeige die eigenen Units beim Gegner.""" artifact, cfg = deps game = Game(artifact, cfg, seed=12) seen = 0 for _ in range(30): if game.over: break act(game) game.step() log = game.last_battle if not log: continue seen += 1 rows = [e["row"] for e in log["events"] if e["e"] == "spawn" and next(u["side"] for u in log["units"] if u["slot"] == e["u"]) == "a"] # Eigene Units starten in der eigenen Hälfte, also Reihe 0 bis 3. assert all(r < log["rows"] // 2 for r in rows), rows assert seen > 0