Выбор места при завершении партии получает опцию «✕ выбыл»; выбывшему авто-проставляется общее последнее место (ничья последнего ранга), плюс флаг eliminated для отрисовки красным крестиком. В результатах и списке партий выбывшие показываются крестиком и идут в конец. Подсчёт очков не меняется (используются обычные числовые места). Бэкенд: модель MatchParticipant.eliminated + миграция 0009 (идемпотентная); схемы (place стал необязательным, +eliminated); match_service (хелпер _resolve_finish_places, finish/update); проброс eliminated в роутерах и stats. Фронт: выбор места с крестиком в MatchDetailPage и AdminMatchEdit, отрисовка в результатах и MatchList, стиль .place-badge.eliminated; регенерация schema.d.ts. Тесты: завершение с выбывшим (последнее место, засчитан), все-выбывшие → 422. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
"""Выбывшие игроки: красный крестик = последнее место (засчитывается, без отд. «проигрыша»)."""
|
|
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, name: str = "Гр") -> int:
|
|
return client.post(
|
|
"/api/groups", json={"name": name, "expansion_ids": []}, headers=csrf_headers(client)
|
|
).json()["id"]
|
|
|
|
|
|
def _start3(client: TestClient, engine):
|
|
me = login(client, "Капитан")
|
|
gid = _group(client)
|
|
b = add_group_member(engine, gid, "Боец")
|
|
c = add_group_member(engine, gid, "Третий")
|
|
fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()]
|
|
started = start_match(
|
|
client,
|
|
gid,
|
|
[
|
|
{"user_id": me["id"], "faction_id": fids[0]},
|
|
{"user_id": b, "faction_id": fids[1]},
|
|
{"user_id": c, "faction_id": fids[2]},
|
|
],
|
|
)
|
|
assert started.status_code == 200, started.text
|
|
return me, b, c, gid, started.json()["id"]
|
|
|
|
|
|
def test_eliminated_gets_last_place(client: TestClient, engine):
|
|
me, b, c, _gid, mid = _start3(client, engine)
|
|
r = finish_match(
|
|
client,
|
|
mid,
|
|
[
|
|
{"user_id": me["id"], "place": 1},
|
|
{"user_id": b, "place": 2},
|
|
{"user_id": c, "eliminated": True}, # выбыл
|
|
],
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
parts = {p["user_id"]: p for p in r.json()["participants"]}
|
|
assert parts[c]["eliminated"] is True
|
|
assert parts[c]["place"] == 3 # survivors=2 → последнее место 3
|
|
assert parts[me["id"]]["eliminated"] is False
|
|
# Выбывший — в конце списка результатов.
|
|
assert r.json()["participants"][-1]["user_id"] == c
|
|
|
|
# Партия засчитана выбывшему как сыгранная (последнее место, 0 очков).
|
|
login(client, "Третий")
|
|
stats = client.get("/api/users/me/stats").json()
|
|
assert stats["overall"]["games"] == 1
|
|
assert stats["overall"]["wins"] == 0
|
|
|
|
|
|
def test_all_eliminated_rejected(client: TestClient, engine):
|
|
me, b, c, _gid, mid = _start3(client, engine)
|
|
r = finish_match(
|
|
client,
|
|
mid,
|
|
[
|
|
{"user_id": me["id"], "eliminated": True},
|
|
{"user_id": b, "eliminated": True},
|
|
{"user_id": c, "eliminated": True},
|
|
],
|
|
)
|
|
assert r.status_code == 422, r.text # должен остаться хотя бы один с местом
|