Партии: «выбывшие» игроки (красный крестик) = последнее место

Выбор места при завершении партии получает опцию «✕ выбыл»; выбывшему
авто-проставляется общее последнее место (ничья последнего ранга), плюс флаг
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>
This commit is contained in:
2026-06-26 16:54:37 +03:00
co-authored by Claude Opus 4.8
parent cd7e5d16b4
commit 1438a17441
13 changed files with 226 additions and 17 deletions
+71
View File
@@ -0,0 +1,71 @@
"""Выбывшие игроки: красный крестик = последнее место (засчитывается, без отд. «проигрыша»)."""
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 # должен остаться хотя бы один с местом