"""Итоги партии для рейтинга (#23): раунд окончания, цели и миры, правило 9 раундов, причина «последний выживший». Сервер проверяет диапазоны и явные противоречия.""" from __future__ import annotations from fastapi.testclient import TestClient from tests.conftest import add_group_member, csrf_headers, finish_match, login, start_match def _group(client: TestClient, engine, players: int) -> tuple[int, list[int], list[int]]: """Группа со всеми дополнениями и players участниками (первый — вошедший).""" me = login(client, "Хозяин") exps = [e["id"] for e in client.get("/api/expansions").json()] gid = client.post( "/api/groups", json={"name": "Группа", "expansion_ids": exps}, headers=csrf_headers(client) ).json()["id"] uids = [me["id"]] + [add_group_member(engine, gid, f"Игрок{i}") for i in range(2, players + 1)] fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()] return gid, uids, fids def _start(client: TestClient, gid: int, uids: list[int], fids: list[int]) -> dict: r = start_match( client, gid, [{"user_id": u, "faction_id": fids[i]} for i, u in enumerate(uids)] ) assert r.status_code == 200, r.text return r.json() def _finish(client: TestClient, mid: int, participants: list[dict], win_reason: str, **extra): body = {"participants": participants, "win_reason": win_reason, **extra} return client.post(f"/api/matches/{mid}/finish", json=body, headers=csrf_headers(client)) def _set_rule(client: TestClient, gid: int, enabled: bool) -> dict: r = client.patch( f"/api/groups/{gid}", json={"nine_rounds_rule": enabled}, headers=csrf_headers(client) ) assert r.status_code == 200, r.text return r.json() def test_finish_saves_round_objectives_and_worlds(client: TestClient, engine): gid, (a, b), fids = _group(client, engine, 2) match = _start(client, gid, [a, b], fids) assert match["max_rounds"] == 8 and match["end_round"] is None r = _finish( client, match["id"], [ {"user_id": a, "place": 1, "objectives": 2, "worlds": 5}, {"user_id": b, "place": 2, "objectives": 1}, # миры не указаны — так и остаётся ], "objectives", end_round=6, ) assert r.status_code == 200, r.text data = r.json() assert data["end_round"] == 6 parts = {p["user_id"]: p for p in data["participants"]} assert (parts[a]["objectives"], parts[a]["worlds"]) == (2, 5) assert (parts[b]["objectives"], parts[b]["worlds"]) == (1, None) listed = client.get(f"/api/groups/{gid}/matches").json()["items"][0]["participants"] assert {p["user_id"]: p["objectives"] for p in listed} == {a: 2, b: 1} def test_end_round_limited_by_max_rounds(client: TestClient, engine): gid, (a, b), fids = _group(client, engine, 2) match = _start(client, gid, [a, b], fids) rows = [{"user_id": a, "place": 1}, {"user_id": b, "place": 2}] assert _finish(client, match["id"], rows, "objectives", end_round=9).status_code == 422 assert _finish(client, match["id"], rows, "objectives", end_round=0).status_code == 422 assert _finish(client, match["id"], rows, "objectives", end_round=8).status_code == 200 def test_nine_rounds_rule_is_snapshotted_for_five_players(client: TestClient, engine): gid, uids, fids = _group(client, engine, 5) assert _set_rule(client, gid, True)["nine_rounds_rule"] is True assert client.get(f"/api/groups/{gid}").json()["name"] == "Группа" # имя не тронуто five = _start(client, gid, uids, fids) four = _start(client, gid, uids[:4], fids) assert (five["nine_rounds_rule"], five["max_rounds"]) == (True, 9) assert (four["nine_rounds_rule"], four["max_rounds"]) == (True, 8) # правило — только с 5 # Смена настройки группы не переписывает уже начатую партию. _set_rule(client, gid, False) assert client.get(f"/api/matches/{five['id']}").json()["max_rounds"] == 9 rows = [{"user_id": u, "place": i + 1} for i, u in enumerate(uids)] assert _finish(client, five["id"], rows, "objectives", end_round=9).status_code == 200 rows4 = [{"user_id": u, "place": i + 1} for i, u in enumerate(uids[:4])] assert _finish(client, four["id"], rows4, "objectives", end_round=9).status_code == 422 def test_last_standing_required_exactly_when_one_survivor(client: TestClient, engine): gid, (a, b, c), fids = _group(client, engine, 3) match = _start(client, gid, [a, b, c], fids) alone = [ {"user_id": a, "place": 1}, {"user_id": b, "eliminated": True}, {"user_id": c, "eliminated": True}, ] r = _finish(client, match["id"], alone, "objectives") assert r.status_code == 422 and "последний выживший" in r.json()["error"]["message"] two = [ {"user_id": a, "place": 1}, {"user_id": b, "place": 2}, {"user_id": c, "eliminated": True}, ] assert _finish(client, match["id"], two, "last_standing").status_code == 422 ok = _finish(client, match["id"], alone, "last_standing") assert ok.status_code == 200, ok.text assert ok.json()["win_reason"] == "last_standing" def test_eliminated_player_has_no_worlds(client: TestClient, engine): gid, (a, b, c), fids = _group(client, engine, 3) match = _start(client, gid, [a, b, c], fids) rows = [ {"user_id": a, "place": 1, "worlds": 7}, {"user_id": b, "place": 2, "worlds": 4}, {"user_id": c, "eliminated": True, "worlds": 2}, ] assert _finish(client, match["id"], rows, "objectives").status_code == 422 rows[2] = {"user_id": c, "eliminated": True, "objectives": 1} # миры не указаны r = _finish(client, match["id"], rows, "objectives") assert r.status_code == 200, r.text parts = {p["user_id"]: p for p in r.json()["participants"]} assert (parts[c]["worlds"], parts[c]["objectives"]) == (0, 1) def test_negative_counts_rejected(client: TestClient, engine): gid, (a, b), fids = _group(client, engine, 2) match = _start(client, gid, [a, b], fids) rows = [{"user_id": a, "place": 1, "objectives": -1}, {"user_id": b, "place": 2}] assert _finish(client, match["id"], rows, "objectives").status_code == 422 def _finished(client: TestClient, gid: int, uids: list[int], fids: list[int]) -> dict: match = _start(client, gid, uids, fids) rows = [{"user_id": u, "place": i + 1} for i, u in enumerate(uids)] r = _finish(client, match["id"], rows, "objectives") assert r.status_code == 200, r.text return r.json() def _edit_rows(match: dict, **by_user) -> list[dict]: rows = [] for p in match["participants"]: row = {"user_id": p["user_id"], "faction_id": p["faction_id"], "place": p["place"]} row.update(by_user.get(str(p["user_id"]), {})) rows.append(row) return rows def test_edit_checks_last_standing_and_round(client: TestClient, engine): gid, (a, b, c), fids = _group(client, engine, 3) match = _finished(client, gid, [a, b, c], fids) mid = match["id"] def patch(body: dict): return client.patch(f"/api/matches/{mid}", json=body, headers=csrf_headers(client)) elim = {"place": None, "eliminated": True} rows = _edit_rows(match, **{str(b): elim, str(c): elim}) assert patch({"participants": rows, "win_reason": "worlds"}).status_code == 422 # Причина не передана — сверяется с записанной («по целям»): тоже противоречие. assert patch({"participants": rows}).status_code == 422 r = patch({"participants": rows, "win_reason": "last_standing", "end_round": 5}) assert r.status_code == 200, r.text assert (r.json()["win_reason"], r.json()["end_round"]) == ("last_standing", 5) # Одна только причина: при одном выжившем вернуть «по целям» нельзя. assert patch({"win_reason": "objectives"}).status_code == 422 # Один только раунд: в пределах лимита — можно, за лимитом — нет. assert patch({"end_round": 9}).status_code == 422 assert patch({"end_round": None}).json()["end_round"] is None def test_admin_edit_saves_counts(client: TestClient, engine, make_admin): gid, (a, b), fids = _group(client, engine, 2) match = _finished(client, gid, [a, b], fids) make_admin("admin", "secret123") assert client.post( "/api/admin/auth/login", json={"username": "admin", "password": "secret123"}, headers=csrf_headers(client), ).status_code == 200 rows = _edit_rows(match, **{str(a): {"objectives": 2, "worlds": 6}}) r = client.patch( f"/api/admin/matches/{match['id']}", json={"participants": rows, "win_reason": "objectives", "end_round": 7}, headers=csrf_headers(client), ) assert r.status_code == 200, r.text parts = {p["user_id"]: p for p in r.json()["participants"]} assert (parts[a]["objectives"], parts[a]["worlds"], r.json()["end_round"]) == (2, 6, 7) def test_draft_keeps_round_and_counts(client: TestClient, engine): gid, (a, b), fids = _group(client, engine, 2) match = _start(client, gid, [a, b], fids) body = { "blocks": [[a], [b]], "win_reason": "last_standing", # черновик — незаконченный ввод, правило не проверяется "end_round": 4, "objectives": {str(a): 2}, "worlds": {str(a): 5, str(b): 3}, } r = client.put(f"/api/matches/{match['id']}/finish-draft", json=body, headers=csrf_headers(client)) assert r.status_code == 200, r.text data = client.get(f"/api/matches/{match['id']}").json()["finish_draft"]["data"] assert data["end_round"] == 4 assert data["objectives"] == {str(a): 2} assert data["worlds"] == {str(a): 5, str(b): 3} bad = client.put( f"/api/matches/{match['id']}/finish-draft", json={"worlds": {str(a): -3}}, headers=csrf_headers(client), ) assert bad.status_code == 422