import pytest from fastapi.testclient import TestClient from tft.api import app as app_mod from tft.constants.loader import load_constants from tft.model import artifact as artifact_mod from tests.test_sim import make_static @pytest.fixture def client(monkeypatch): static = make_static() static["meta"]["patch"] = "test" art = artifact_mod.build(static) cfg = load_constants(17) monkeypatch.setattr(app_mod, "_load_game_deps", lambda: (art, cfg)) return TestClient(app_mod.app) def test_full_game_via_api(client): state = client.post("/api/game", params={"seed": 3}).json() game_id = state["game_id"] assert state["round"]["label"] == "1-2" # 1-1-Carousel läuft automatisch ab assert state["player"]["hp"] == 100 assert len(state["opponents"]) == 7 steps = 0 while not state["over"]: if state["augment_offer"]: client.post(f"/api/game/{game_id}/action", json={"action": "pick_augment", "choice": 0}) affordable = [i for i, s in enumerate(state["shop"]) if s and s["cost"] <= state["player"]["gold"]] if affordable: client.post(f"/api/game/{game_id}/action", json={"action": "buy", "slot": affordable[0]}) resp = client.post(f"/api/game/{game_id}/action", json={"action": "next"}) assert resp.status_code == 200 state = resp.json() steps += 1 assert steps < 100 assert 1 <= state["placement"] <= 8 def test_invalid_action_is_400(client): game_id = client.post("/api/game", params={"seed": 1}).json()["game_id"] resp = client.post(f"/api/game/{game_id}/action", json={"action": "sell", "where": "bench", "idx": 99}) assert resp.status_code == 400 resp = client.post(f"/api/game/{game_id}/action", json={"action": "dance"}) assert resp.status_code == 400 assert client.post("/api/game/nope/action", json={"action": "next"}).status_code == 404 def test_state_shape(client): state = client.post("/api/game", params={"seed": 5}).json() for key in ("round", "player", "board", "bench", "shop", "traits", "opponents", "items", "over", "log", "shop_odds", "shop_locked"): assert key in state assert sum(state["shop_odds"]) == 100 assert state["player"]["off"] == 0.0 assert state["player"]["def"] == 0.0 assert all(o["board"] is not None for o in state["opponents"]) def test_upgrade_hint(): from tft.api.app import upgrade_hint from tft.sim.game import Game static = make_static() static["meta"]["patch"] = "t" game = Game(artifact_mod.build(static), load_constants(17), seed=1) assert upgrade_hint(game, "U1_0") == 0 game.bench = [{"api_name": "U1_0", "stars": 1, "items": []} for _ in range(2)] assert upgrade_hint(game, "U1_0") == 2 game.bench = [{"api_name": "U1_0", "stars": 2, "items": []} for _ in range(2)] + \ [{"api_name": "U1_0", "stars": 1, "items": []} for _ in range(2)] assert upgrade_hint(game, "U1_0") == 3 game.bench = [{"api_name": "U1_0", "stars": 3, "items": []}] assert upgrade_hint(game, "U1_0") == 0 @pytest.fixture def group_client(monkeypatch): static = make_static() static["meta"]["patch"] = "test" art = artifact_mod.build(static) a, b = list(art["static"]["units"])[:2] # Schlüssel sind "api@itemzahl" — die Karte zeigt die Items mit an. # Schlüssel sind "api@sterne@itemzahl". Zwei Gruppen teilen sich # denselben Carry — nur die bessere darf durchkommen. art["groups"] = { "2": {f"{a}@2@3|{b}@2@0": {"edge": 1.2, "boards": 400}, f"{a}@2@3|{b}@1@1": {"edge": 0.9, "boards": 700}, f"{a}@1@0|{b}@2@3": {"edge": 0.4, "boards": 900}}, "boards": 5000, } cfg = load_constants(17) monkeypatch.setattr(app_mod, "_load_game_deps", lambda: (art, cfg)) return TestClient(app_mod.app) def test_groups_endpoint(group_client): """Der Gruppen-Reiter braucht Name, Kosten und Icon je Unit.""" r = group_client.get("/api/groups?size=2&limit=3") assert r.status_code == 200 body = r.json() assert body["size"] == 2 and body["boards"] == 5000 assert 0 < len(body["groups"]) <= 3 first = body["groups"][0] assert len(first["units"]) == 2 assert all({"name", "cost", "icon", "splash", "items", "stars", "traits", "tier"} <= set(u) for u in first["units"]) assert [u["items"] for u in first["units"]] == [3, 0] assert [u["stars"] for u in first["units"]] == [2, 2] # Je Carry nur eine Gruppe: a@3 kommt einmal vor, dann b@3. carries = [g["units"][0]["name"] for g in body["groups"]] assert len(carries) == len(set(carries)) # Nach Vorsprung sortiert, absteigend. edges = [g["edge"] for g in body["groups"]] assert edges == sorted(edges, reverse=True) assert group_client.get("/api/groups?size=2&limit=1").json()["groups"] == [first] def test_groups_without_data(client): # Ohne trainiertes Artefakt gibt es nichts zu zeigen — 404 statt leerer Liste. assert client.get("/api/groups?size=2").status_code == 404 def test_groups_unknown_size(group_client): assert group_client.get("/api/groups?size=7").status_code == 404 def test_roles_endpoint(monkeypatch): """Beste Unit je Rolle und Kostenstufe, mit den Items dieses Champions.""" static = make_static() static["meta"]["patch"] = "test" art = artifact_mod.build(static) api = next(a for a, u in art["static"]["units"].items() if "Tank" in (u.get("role") or "")) item = next(iter(art["static"]["items"])) art["groups"] = {"solo": {f"{api}@2@3": {"edge": 0.5, "boards": 300}}, "unit_items": {f"{api}|{item}": {"edge": 0.3, "boards": 400}, f"{api}|schlecht": {"edge": -0.1, "boards": 400}}, "boards": 9000} art["unit_tiers"] = {api: {"tier": "S", "strength": 0.4, "boards": 1100}} monkeypatch.setattr(app_mod, "_load_game_deps", lambda: (art, load_constants(17))) body = TestClient(app_mod.app).get("/api/roles").json() tank = next(r for r in body["roles"] if r["label"] == "Tank") entry = next(c for c in tank["costs"] if c["unit"]) # Buchstabe und Zahl kommen aus derselben Quelle. assert entry["unit"]["tier"] == "S" assert entry["unit"]["edge"] == 0.4 # Items haengen am Champion, Negatives ist keine Empfehlung. assert [i["edge"] for i in entry["unit"]["items"]] == [0.3] assert {"name", "cost", "traits", "splash"} <= set(entry["unit"]) # Kostenstufen ohne Daten bleiben leer statt zu raten. assert any(c["unit"] is None for c in tank["costs"]) def test_roles_without_data(client): assert client.get("/api/roles").status_code == 404