Партии: «выбывшие» игроки (красный крестик) = последнее место
Выбор места при завершении партии получает опцию «✕ выбыл»; выбывшему авто-проставляется общее последнее место (ничья последнего ранга), плюс флаг 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:
@@ -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 # должен остаться хотя бы один с местом
|
||||
Reference in New Issue
Block a user