Реализация новой рейтинговой системы (#23) #75

Merged
NotBigGhost merged 5 commits from issue-23-rating-system into dev 2026-09-14 23:38:20 +03:00
Collaborator

Что сделано

Рейтинговая система из утверждённого docs/rating/rating-system.md (#22) реализована целиком: многопользовательский Elo с множителем отрыва. Параметры: шкала 1500/400, K 64 → 16 за 20 партий, вес стола G(N), темп / цели / миры, близость по типу победы, clamp [0.5, 2].

Движок. scoring.py — порт rate_match из эталона simulate.py.

Витрины. Топ, профиль, главная и статистика группы больше не агрегируются в SQL: они проигрывают завершённые партии по порядку played_at, finished_at, id.

  • Цепочки две: общая и групповая.
  • Правка прошлой партии пересчитывает всё после неё.
  • «Лучшая партия» определяется по ΔR, лучшая/худшая фракция — по среднему S − E.

Ввод.

  • В форме завершения, правке результатов и админке: раунд окончания и цели/миры у каждого игрока (всё необязательно, уходит в общий черновик).
  • В настройках группы — галочка «9 раундов при 5–6 игроках». Она снимается в партию при старте.
  • Причина last_standing ставится сама, когда невыбывший игрок один; сервер проверяет это правило.
  • Несогласованный ввод даёт предупреждения, отправку они не блокируют.

Отображение.

  • Общий топ: без столбца «Поб», «Очки» → «Рейтинг», рейтинг — целое число.
  • Справка переписана.

Уведомлений игрокам и пояснения на странице топа здесь нет: систему оповещений делаете вы, до релиза рейтинга.

Коммиты

  • e58b4f6 Рейтинг: движок Elo с множителем отрыва и тесты на примеры документа
  • 8aa6bb7 Рейтинг: раунд, цели, миры, правило 9 раундов и причина last_standing
  • 6ea4a67 Рейтинг: витрины статистики на проигрывании истории
  • cc64bb4 Рейтинг: ввод деталей партии и предупреждения в формах
  • adc47be Рейтинг: топ, профиль и справка под новую шкалу

Проверки

  • .\.venv\Scripts\python.exe -m pytest — 189 passed. Новое:
    • все 15 примеров раздела 6 с числами документа;
    • все ΔR трёх синтетических сезонов (полные партии и история без деталей) совпадают с simulate.py до 1e-9;
    • монотонность и сумма-ноль;
    • валидация раунда, миров выбывших и last_standing в финише, правке игрока и админа;
    • снимок правила 9 раундов;
    • пересчёт после правки прошлой партии;
    • групповая цепочка;
    • лучшая партия по ΔR;
    • одна загрузка истории на главной.
  • npm run build (tsc + vite) — зелёный; schema.d.ts перегенерирован из живого OpenAPI.
  • Миграция 0014, копия dev-БД (без CHECK у matches):
    • число строк до и после совпадает, FK и индексы на месте;
    • повторный запуск ничего не меняет;
    • бэкфилл проставил last_standing пяти партиям с одним выжившим.
  • Миграция 0014, свежая БД со схемой origin/dev (с CHECK), с участниками, вложением и черновиком:
    • matches пересоздана с ck_match_status и расширенным ck_match_win_reason;
    • дочерние строки на месте, integrity_check и foreign_key_check чистые;
    • повторный запуск после stamp 0013 ничего не меняет;
    • downgrade возвращает прежнюю схему.
  • Сквозной HTTP-сценарий через vite-прокси на копии БД:
    • снимок 9 раундов у партии на 5 игроков;
    • черновик с целями и мирами;
    • 422 «по целям» при одном выжившем;
    • завершение last_standing с мирами выбывших = 0;
    • пересчёт топа, главной и группы;
    • смена галочки группы не трогает начатую партию.
  • Логика фронта: finishWarnings (все пять видов предупреждений, пустые поля, ничья за 1-е, last_standing) и reasonForSurvivors проверены на скомпилированном esbuild коде.
  • Не проверено: ручной прогон интерфейса в браузере — расширение Chrome было не подключено. Вёрстку новых полей (цели/миры в PlaceEditor, блок «Итог партии», топ без столбца побед) стоит посмотреть глазами, в том числе на ширине телефона.

Что ломается (Compat/Breaking)

  • Числа и порядок топа. Около 1500 вместо около 50, это другая величина. Данные не трогаются: рейтинг вычисляется из истории.
  • API.
    • OverallStats.score / LeaderboardEntry.score → целый рейтинг; FactionStat.score → средний S − E × 100.
    • Новое значение win_reason = last_standing.
    • Партия с одним невыбывшим принимается только с last_standing, иначе 422 с понятным текстом.
    • PATCH /groups/{id} принимает {name?, nine_rounds_rule?}, старое тело {name} валидно.
  • БД (миграция 0014).
    • Добавляются nullable end_round, objectives, worlds и nine_rounds_rule со значением по умолчанию 0.
    • CHECK причины победы расширяется, если он в БД есть.
    • Бэкфилл last_standing.
    • Старые партии считаются без новых полей (раздел 4.8).
    • Пересоздание matches отказывает при PRAGMA foreign_keys=ON: иначе DROP удалил бы участников каскадом. Штатный alembic upgrade head из entrypoint.sh эту PRAGMA не включает.

Отклонения от плана

  • В 0014 CHECK заменяется только там, где он уже есть. Оказалось, что в старых БД (как dev) CHECK у matches нет вовсе. Пересоздавать таблицу ради добавления CHECK было бы лишним риском.
  • Причина победы, раунд окончания и предупреждения вынесены в общий компонент MatchOutcomeFields: он используется в форме завершения и в правке результатов.
  • Ручная проверка в браузере не выполнена (см. «Проверки»).

Closes #23

🤖 Generated with Claude Code

https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd

## Что сделано Рейтинговая система из утверждённого `docs/rating/rating-system.md` (#22) реализована целиком: многопользовательский Elo с множителем отрыва. Параметры: шкала 1500/400, K 64 → 16 за 20 партий, вес стола `G(N)`, темп / цели / миры, близость по типу победы, `clamp [0.5, 2]`. **Движок.** `scoring.py` — порт `rate_match` из эталона `simulate.py`. **Витрины.** Топ, профиль, главная и статистика группы больше не агрегируются в SQL: они проигрывают завершённые партии по порядку `played_at, finished_at, id`. - Цепочки две: общая и групповая. - Правка прошлой партии пересчитывает всё после неё. - «Лучшая партия» определяется по ΔR, лучшая/худшая фракция — по среднему `S − E`. **Ввод.** - В форме завершения, правке результатов и админке: раунд окончания и цели/миры у каждого игрока (всё необязательно, уходит в общий черновик). - В настройках группы — галочка «9 раундов при 5–6 игроках». Она снимается в партию при старте. - Причина `last_standing` ставится сама, когда невыбывший игрок один; сервер проверяет это правило. - Несогласованный ввод даёт предупреждения, отправку они не блокируют. **Отображение.** - Общий топ: без столбца «Поб», «Очки» → «Рейтинг», рейтинг — целое число. - Справка переписана. Уведомлений игрокам и пояснения на странице топа здесь **нет**: систему оповещений делаете вы, до релиза рейтинга. ## Коммиты - `e58b4f6` Рейтинг: движок Elo с множителем отрыва и тесты на примеры документа - `8aa6bb7` Рейтинг: раунд, цели, миры, правило 9 раундов и причина last_standing - `6ea4a67` Рейтинг: витрины статистики на проигрывании истории - `cc64bb4` Рейтинг: ввод деталей партии и предупреждения в формах - `adc47be` Рейтинг: топ, профиль и справка под новую шкалу ## Проверки - `.\.venv\Scripts\python.exe -m pytest` — **189 passed**. Новое: - все 15 примеров раздела 6 с числами документа; - все ΔR трёх синтетических сезонов (полные партии и история без деталей) совпадают с `simulate.py` до 1e-9; - монотонность и сумма-ноль; - валидация раунда, миров выбывших и `last_standing` в финише, правке игрока и админа; - снимок правила 9 раундов; - пересчёт после правки прошлой партии; - групповая цепочка; - лучшая партия по ΔR; - одна загрузка истории на главной. - `npm run build` (tsc + vite) — зелёный; `schema.d.ts` перегенерирован из живого OpenAPI. - **Миграция 0014, копия dev-БД** (без CHECK у `matches`): - число строк до и после совпадает, FK и индексы на месте; - повторный запуск ничего не меняет; - бэкфилл проставил `last_standing` пяти партиям с одним выжившим. - **Миграция 0014, свежая БД со схемой `origin/dev`** (с CHECK), с участниками, вложением и черновиком: - `matches` пересоздана с `ck_match_status` и расширенным `ck_match_win_reason`; - дочерние строки на месте, `integrity_check` и `foreign_key_check` чистые; - повторный запуск после `stamp 0013` ничего не меняет; - `downgrade` возвращает прежнюю схему. - **Сквозной HTTP-сценарий** через vite-прокси на копии БД: - снимок 9 раундов у партии на 5 игроков; - черновик с целями и мирами; - 422 «по целям» при одном выжившем; - завершение `last_standing` с мирами выбывших = 0; - пересчёт топа, главной и группы; - смена галочки группы не трогает начатую партию. - **Логика фронта:** `finishWarnings` (все пять видов предупреждений, пустые поля, ничья за 1-е, `last_standing`) и `reasonForSurvivors` проверены на скомпилированном esbuild коде. - **Не проверено:** ручной прогон интерфейса в браузере — расширение Chrome было не подключено. Вёрстку новых полей (цели/миры в `PlaceEditor`, блок «Итог партии», топ без столбца побед) стоит посмотреть глазами, в том числе на ширине телефона. ## Что ломается (`Compat/Breaking`) - **Числа и порядок топа.** Около 1500 вместо около 50, это другая величина. Данные не трогаются: рейтинг вычисляется из истории. - **API.** - `OverallStats.score` / `LeaderboardEntry.score` → целый рейтинг; `FactionStat.score` → средний `S − E` × 100. - Новое значение `win_reason = last_standing`. - Партия с одним невыбывшим принимается только с `last_standing`, иначе 422 с понятным текстом. - `PATCH /groups/{id}` принимает `{name?, nine_rounds_rule?}`, старое тело `{name}` валидно. - **БД (миграция 0014).** - Добавляются nullable `end_round`, `objectives`, `worlds` и `nine_rounds_rule` со значением по умолчанию 0. - CHECK причины победы расширяется, если он в БД есть. - Бэкфилл `last_standing`. - Старые партии считаются без новых полей (раздел 4.8). - Пересоздание `matches` отказывает при `PRAGMA foreign_keys=ON`: иначе `DROP` удалил бы участников каскадом. Штатный `alembic upgrade head` из `entrypoint.sh` эту PRAGMA не включает. ## Отклонения от плана - В 0014 CHECK заменяется только там, где он уже есть. Оказалось, что в старых БД (как dev) CHECK у `matches` нет вовсе. Пересоздавать таблицу ради добавления CHECK было бы лишним риском. - Причина победы, раунд окончания и предупреждения вынесены в общий компонент `MatchOutcomeFields`: он используется в форме завершения и в правке результатов. - Ручная проверка в браузере не выполнена (см. «Проверки»). Closes #23 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
Agent added 5 commits 2026-09-14 22:51:02 +03:00
scoring.py получает движок из docs/rating/rating-system.md: ожидание пары,
K 64 → 16 за 20 партий, вес стола G(N), множитель отрыва (темп, цели, миры,
clamp [0.5, 2]) и близость по типу победы, включая last_standing. rate_match
и replay — чистые функции без БД; replay отдаёт рейтинги без округления,
ΔR и результат относительно ожидания по каждой партии.

Тесты: 15 примеров раздела 6 с числами документа, совпадение констант
и всех ΔR сезона с эталоном simulate.py (полные партии и история без деталей),
монотонность и сумма-ноль. League Points пока остаётся в модуле — витрины
переводятся отдельным коммитом. #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
Модели и миграция 0014: groups.nine_rounds_rule, снимок правила в matches,
matches.end_round, match_participants.objectives/worlds. CHECK причины победы
расширяется, только если он в БД есть (старые БД без CHECK таблицу не
пересоздают); пересоздание matches отказывает при PRAGMA foreign_keys=ON,
иначе DROP унёс бы участников каскадом. Бэкфилл: last_standing у завершённых
партий с одним невыбывшим. Проверено на копии dev-БД и на схеме origin/dev
с CHECK: строки, CHECK, FK и индексы на месте, повтор и downgrade работают.

API и валидация: end_round от 1 до лимита раундов партии (9 при хоумруле
и 5+ игроках), у выбывшего миров 0, last_standing ровно при одном невыбывшем —
в завершении и правке (игрока и админа); черновик хранит раунд, цели и миры
без проверки правила. MatchRead отдаёт end_round, снимок правила и max_rounds.
PATCH /groups/{id} принимает nine_rounds_rule вместе с названием. #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
SQL-агрегат League Points (SCORED_CTE, сглаженное среднее) заменён
проигрыванием завершённых партий по порядку played_at, finished_at, id:
load_history — один запрос на партии с участниками, scoring.replay считает
рейтинг. Цепочки две: общая (все партии) и групповая (только партии группы,
K — по партиям внутри группы). Правка или удаление прошлой партии
пересчитывает всё после неё без отдельной логики.

Топ, профиль, главная и статистика группы берут игры, победы, винрейт
и среднее место из той же истории; score — рейтинг целым числом, сортировка
по неокруглённому. Главная грузит историю один раз. Лучшая партия — наибольший
ΔR в общей цепочке, лучшая/худшая фракция — средний S − E. League Points
из scoring.py удалён.

Тесты: дуэль новичков ±32, ранжирование после 10 партий, пересчёт после
правки прошлой партии, отдельная групповая цепочка, лучшая партия по ΔR,
одна загрузка истории на главной. #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
Форма завершения и правка результатов (MatchDetailPage): у каждого игрока
в PlaceEditor поля «цели» и «миры» на конец партии (у выбывшего миры
заблокированы нулём), общий блок «Итог партии» — причина победы и раунд
окончания из max_rounds партии. Всё новое уходит в общий черновик.
Причина «последний выживший» ставится сама при одном невыбывшем, выбор
заблокирован; вернули второго — причина сбрасывается и её нужно выбрать.
То же в админской правке партии.

Предупреждения (domain/finishWarnings.ts, отправку не блокируют): тип победы
и цели/миры лидеров не согласуются, досрочный конец без N целей у победителя,
у соперника целей больше, чем у победителя. Настройки группы: переключатель
«9 раундов при 5–6 игроках» через PATCH /groups/{id}. schema.d.ts
перегенерирован. #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
Общий топ: столбец «Поб» и сортировка по победам убраны, чтобы четырёхзначный
рейтинг помещался в строку; «Очки» → «Рейтинг» (и в карточке профиля).
Справка переписана под Elo: шкала 1500/400, ожидание и формула ΔR, K новичка,
вес стола, множитель отрыва и близость по типу победы, необязательность
раунда, целей и миров, «последний выживший», лимит 8/9 раундов, примеры
из документа, общий и групповой рейтинг, лучшая партия по приросту.
В документе рейтинга статус — утверждено и реализовано. #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
NotBigGhost merged commit 171aebb6f8 into dev 2026-09-14 23:38:20 +03:00
NotBigGhost deleted branch issue-23-rating-system 2026-09-14 23:38:20 +03:00
Sign in to join this conversation.