258 lines
7.5 KiB
Python
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
|