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

Выбор места при завершении партии получает опцию «✕ выбыл»; выбывшему
авто-проставляется общее последнее место (ничья последнего ранга), плюс флаг
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
+6 -2
View File
@@ -204,7 +204,8 @@ class MatchCreate(BaseModel):
# Этап 2 (завершение): места, комментарии, причина победы.
class MatchFinishParticipant(BaseModel):
user_id: int
place: int = Field(ge=1)
place: int | None = Field(default=None, ge=1) # None у выбывшего (eliminated)
eliminated: bool = False # выбыл из партии → авто-проставится последнее место
comment: str | None = None
faction_id: int | None = None # опц. смена фракции при завершении
@@ -221,7 +222,8 @@ class MatchFinish(BaseModel):
class ParticipantInput(BaseModel):
user_id: int
faction_id: int
place: int = Field(ge=1)
place: int | None = Field(default=None, ge=1) # None у выбывшего (eliminated)
eliminated: bool = False
was_random: bool = False
comment: str | None = None
@@ -240,6 +242,7 @@ class MatchParticipantRead(BaseModel):
faction_id: int
faction_name: str
place: int | None = None
eliminated: bool = False
was_random: bool
comment: str | None = None
@@ -357,6 +360,7 @@ class MatchListParticipant(BaseModel):
faction_id: int
faction_name: str
place: int | None = None
eliminated: bool = False
was_random: bool
comment: str | None = None