Files
tft/backend/tests/test_fullgame.py
2026-09-07 07:44:09 +02:00

258 lines
7.5 KiB
Python

"""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