Групповая цепочка убрана: рейтинг у игрока один, по всем партиям приложения (решение владельца). load_history всегда грузит всю историю, одно проигрывание на запрос. Страница группы: игры, победы, винрейт и среднее место — по партиям группы, рейтинг — общий; «Новичок» тоже по общему числу партий (новое поле rating_confirmed), поэтому опытный игрок в новой группе ранжирован. Участники, не игравшие в группе, показывают общий рейтинг. Главная и профиль — общие показатели; в блоке активной группы игры по группе, рейтинг общий. У profile_stats убран неиспользуемый group_id. Фронт: цвет рейтинга в списках берётся из rating_confirmed; справка — «Один рейтинг на всё приложение». Документ рейтинга: раздел 8 и решение 7 в разделе 9. Тесты: рейтинг в группах равен общему при групповых играх и победах, главная и блок активной группы, ветеран в новой группе ранжирован, «Ещё не играли» с общим рейтингом. #80 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
58 KiB
Новая рейтинговая система
Статус: утверждено (#22), реализовано в #23 (
backend/app/services/scoring.py). Решения владельца по открытым вопросам (2026-09-14) внесены — раздел 9. Приложение:simulate.py— эталонная реализация формул, все примеры этого документа (сassert), симуляция и перебор коэффициентов. Только стандартная библиотека Python, вывод воспроизводим:python docs/rating/simulate.py # примеры + сравнение систем (≈1.5 мин) python docs/rating/simulate.py --grid # примеры + подбор коэффициентов (≈7 мин)
Коротко
Сейчас рейтинг — среднее очков за место. Он не видит ни силы соперников, ни хода партии, а первое место в дуэли и за столом на пятерых стоит одинаково.
Предлагается многопользовательский Elo с множителем отрыва:
- Разбор на дуэли. Партия раскладывается на пары игроков. Каждая пара — дуэль, исход которой сравнивается с ожидаемым по разнице рейтингов: победа над сильным даёт больше, чем над слабым.
- Множитель отрыва. Размер изменения зависит от того, как партия сыграна: на каком раунде закончилась, какой отрыв по целям и мирам, какой тип победы. Размер стола тоже влияет, но слабее темпа.
- Шкала — классический Elo. Старт 1500, разница 400 пунктов — шансы 10:1. Пример из задачи «60 против 40» в ней — 1600 против 1400.
- Монотонность. Победитель никогда не теряет рейтинг, последнее место никогда не приносит.
- Старая история. Пересчитывается по тем же формулам: у партий без новых полей признаки берутся нейтральными.
На синтетической лиге система лучше текущей и чистого Elo во всех сценариях, где отрыв действительно связан с силой игроков. Например, после 10 партий она правильнее упорядочивает игроков: ρ = 0.807 против 0.769 у текущей. Там, где отрыв — чистый шум, она уступает чистому Elo около 0.1 п.п. точности. Подробности — в разделе 7.
Содержание
- Как считается сейчас и что с этим не так
- Исходные данные: правила игры
- Выбор модели
- Формулы
- Требования → решения
- Примеры расчётов
- Проверка на симуляции
- Что потребуется в реализации (#23)
- Решения владельца
1. Как считается сейчас и что с этим не так
backend/app/services/scoring.py. За партию на N игроков участник получает League Points:
\text{points} = \frac{N - \text{place} - (\text{tie}-1)/2}{N-1}
1-е место — 1.0, последнее — 0.0; равные места делят очки. Рейтинг — сглаженное среднее с 10 «виртуальными» партиями по 0.5, в топ попадают игроки от 10 партий:
\text{score} = \frac{10 \cdot 0.5 + \sum \text{points}}{10 + \text{games}} \cdot 100
Проблемы на цифрах:
| # | Проблема | Пример |
|---|---|---|
| 1 | Сила соперника не учитывается. | Победа над лидером топа и над новичком — одинаковые 1.0. Кто играет в основном со слабыми, копит рейтинг быстрее. В симуляции с тремя группами разной силы текущая система упорядочивает игроков с ρ = 0.525, предложенная — с 0.644 (раздел 7). |
| 2 | Размер стола не учитывается. | 1-е место в дуэли — 1.0, 1-е место за столом на пятерых — тоже 1.0, хотя обыграны четверо. |
| 3 | Ход партии не учитывается. | Разгром на 3-м раунде (2:0 по целям, 8:2 по мирам) и победа на тай-брейке по мирам в конце 8-го раунда — одинаковые 1.0. |
| 4 | Рейтинг помнит всю историю с одним весом. | Игрок, проигравший первые 10 дуэлей и выигравший следующие 10, имеет (5 + 10) / 30 · 100 = 50 — как середняк, хотя сейчас сильнее всех. В сценарии «рост» симуляции ρ после 10 партий у текущей системы 0.732, у предложенной 0.774. |
2. Исходные данные: правила игры
По справочнику (стр. 8, 11, 16) и уточнениям владельца в #22.
Победа. Игрок, собравший маркеры целей в количестве, равном числу игроков N, побеждает. Цели собираются в фазе обновления, поэтому партия заканчивается на границе раунда. Если к концу последнего раунда никто не набрал N, побеждает тот, у кого больше целей.
Тай-брейки (от менее близкой партии к более близкой) — это и есть типы победы в приложении:
Тип (win_reason) |
Когда | Близость партии |
|---|---|---|
objectives — по целям |
больше всех целей | обычная |
worlds — по мирам |
цели поровну, больше дружественных миров | близкая |
plastic — по пластику |
цели и миры поровну, больше отрядов на поле | очень близкая |
resources — по ресурсам |
всё выше поровну; домашнее правило | самая близкая |
last_standing — последний выживший |
все соперники устранены (нет миров); новая причина (решение владельца, раздел 9) | разгром |
Если поровну вообще всё, победа общая (в приложении — ничья за 1-е место).
Лимит раундов R_max. По правилам 8. Домашнее правило: за столом на 5–6 игроков
играется 9 раундов. Правило включается галочкой в настройках группы.
Миры. В тексте #22 они названы «системами». По справочнику система — это целый тайл из четырёх областей, а область с планетой — мир. Дальше используется термин справочника. Размер поля и «честная доля» миров на игрока:
| N | Поле | Тайлов | Миров на поле (×2.2) | Миров на игрока W(N)/N |
|---|---|---|---|---|
| 2 | 2×3 | 6 | 13.2 | 6.60 |
| 3 | 3×3 | 9 | 19.8 | 6.60 |
| 4 | 3×4 | 12 | 26.4 | 6.60 |
| 5 | 4×4 | 16 | 35.2 | 7.04 |
| 6 | 4×5 | 20 | 44.0 | 7.33 |
3. Выбор модели
Что нужно от модели:
- учитывать силу соперников (треб. 3);
- работать для столов на 2–6 игроков с ничьими и выбывшими;
- принимать дополнительные признаки партии (треб. 1, 2, 4, 5);
- считаться вручную — чтобы игроки могли проверить, почему рейтинг изменился именно так;
- работать на малых данных: десятки игроков и сотни партий;
- обходиться без внешних зависимостей: вся реализация — пара десятков строк.
| Модель | Сила соперника | 2–6 игроков | Признаки партии | Ручной расчёт | Вывод |
|---|---|---|---|---|---|
| League Points (сейчас) | нет | да | только место | да | не выполняет треб. 1–5 |
| Elo, парный | да | да, через пары | через множитель K | да | база |
| Glicko-2 | да, плюс неопределённость | только через пары, как Elo; нужны рейтинговые периоды | нет | тяжело | сложность без выигрыша на наших объёмах |
| TrueSkill / OpenSkill | да, плюс неопределённость | да, нативно | нет — только порядок мест | нет | не принимает отрыв без самодельных надстроек |
Выбран парный Elo с множителем отрыва. Отрыв умножает K, а фактический результат пары остаётся 1/0.5/0. Можно было бы вшить отрыв в сам результат (например, близкая победа = 0.6), но тогда фаворит, выигравший близко, терял бы рейтинг за победу. Множитель K сохраняет монотонность: победа всегда в плюс, отрыв влияет только на размер.
4. Формулы
4.1. Шкала
Стартовый рейтинг R₀ = 1500, масштаб D = 400: разница 400 пунктов означает шансы 10:1. Это классическая шкала Elo (решение владельца, раздел 9).
С нынешним топом (около 50) она не совпадает. Для сопоставления чисел в этом документе
используется соответствие R = 10·score + 1000: 50 ↔ 1500, 60 ↔ 1600. Пример «60 против 40»
из #22 — это 1600 против 1400. Формулы от выбора шкалы не зависят. Если пересчитать рейтинги
по этому соответствию, а D и K умножить на 10, ожидания, порядок игроков и качество
прогноза не изменятся — в 10 раз вырастут только изменения рейтинга и его разброс.
simulate.py это проверяет: на одних и тех же сезонах обе шкалы дают одинаковые точность,
Brier и ρ.
4.2. Ожидаемый результат пары
E_{ab} = \frac{1}{1 + 10^{(R_b - R_a)/D}}
E_ab — вероятность, что a окажется выше b. При 1600 против 1400 — 0.760, при равных — 0.500.
4.3. Фактический результат пары
S_ab = 1, если a занял место выше b; 0.5, если места равны; 0 — если ниже.
Выбывшие делят последнее место, как и сейчас (match_service._resolve_finish_places).
4.4. Изменение рейтинга
\Delta R_i = \frac{K_i \cdot G(N)}{N-1} \sum_{j \ne i} M_{ij} \, (S_{ij} - E_{ij})
Все рейтинги в формуле — до партии. Деление на N − 1 приводит сумму по соперникам
к «средней дуэли», размер стола затем добавляется явно через G(N).
4.5. Коэффициент K — скорость изменения
K_i = K_{\min} + (K_{\max} - K_{\min}) \cdot \max\!\left(0,\; 1 - \frac{n_i}{n_K}\right)
n_i — сколько завершённых партий игрок сыграл до этой. Новичок стартует с K_max = 64,
к 20-й партии K линейно спускается до K_min = 16. Так новичок быстро находит свой
уровень, а рейтинг опытного игрока не скачет от одной партии.
Эта схема заменяет нынешние «10 виртуальных партий». Минимум партий для топа
(MIN_GAMES = 10) сохраняется. К 10-й партии предложенная система упорядочивает игроков
лучше текущей: ρ 0.807 против 0.769 в сценарии «сигнал» (раздел 7).
4.6. Вес размера стола (треб. 2)
G(N) = 1 + w_N \cdot \frac{N-2}{4}, \qquad w_N = 0.5
G = 1.0 для дуэли, 1.25 для четверых, 1.5 для шестерых.
4.7. Множитель отрыва пары
Признаки пары (a — выше или наравне с b), каждый нормирован в [0, 1]:
| Признак | Формула | Для каких пар | Типичное значение μ |
|---|---|---|---|
темп τ (треб. 1) |
(R_max − раунд) / (R_max − 1) |
только пары с победителем | 1/(R_max − 1) — конец в предпоследнем раунде |
отрыв по целям o (треб. 4) |
max(0, цели_a − цели_b) / N |
все | 0.5 |
отрыв по мирам w (треб. 4) |
min(1, max(0, миры_a − миры_b) / (W(N)/N)) |
все | 0.5 |
Для пар с равными местами разница берётся по модулю. При победе last_standing отрыв
победителя по целям считается равным 1: все соперники устранены, сколько бы маркеров
у них ни было.
A_{ab} = 1 + w_\tau (\tau - \mu_\tau) + w_o (o - 0.5) + w_w (w - 0.5)
M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
- Центрирование. Благодаря вычитанию μ партия с типичными признаками получает
M = 1— то есть обычный Elo. Разгром поднимает множитель, близкая партия его снижает. - Ограничение.
clampставит страховку: одна партия не может весить больше чем вдвое или меньше чем вдвое против обычной. - Близость по типу победы
c(треб. 5) действует только на пары с победителем: тип победы описывает борьбу за первое место, а не за второе или третье.
win_reason |
objectives |
worlds |
plastic |
resources |
last_standing |
|---|---|---|---|---|---|
c |
1.00 | 0.85 | 0.70 | 0.60 | 1.00 |
4.8. Партии без новых полей
У партий из истории (и у любых, где поле не заполнено) нет раунда, целей или миров.
Недостающий признак подставляется типичным значением μ: его слагаемое в A равно нулю.
Тип победы в истории есть, поэтому близость c работает всегда. Для старой партии формула
сводится к чистому Elo с учётом размера стола и типа победы. Заполнять историю задним числом
не обязательно.
4.9. Свойства
- Монотонность. 1-е место без ничьей даёт
S − E > 0во всех парах, аM > 0— значит, рейтинг растёт. Последнее место без ничьей всегда уменьшает рейтинг. - Сумма-ноль.
M_ab = M_ba, поэтому при равных K сумма изменений за партию равна нулю и рейтинг не раздувается. Когда K разные (новичок и ветеран), сумма не нулевая — это сделано намеренно (пример 7). В симуляции среднее по лиге за 300 партий сдвигается не больше чем на 0.8 пункта, в сценарии «рост» — на −7 при разбросе силы игроков ±140. - Ограниченность. Изменение за партию не больше
K·G·2.0: 32 у ветерана в дуэли, 128 у новичка в дуэли, 192 у новичка за столом на шестерых. Это теоретические пределы для разгромной победы, которой почти никто не ждал (E ≈ 0); против равных — вдвое меньше. - Детерминизм. Рейтинг — функция упорядоченной истории партий. Пересчёт с нуля всегда даёт тот же результат.
4.10. Коэффициенты
| Параметр | Значение | Откуда |
|---|---|---|
R₀, D |
1500, 400 | шкала (4.1), решение владельца |
K_max, K_min, n_K |
64, 16, 20 | перебор (7.4): выигрыш на «сигнале» без потерь на «шуме» |
w_N (стол) |
0.5 | требование «слабее темпа»; в переборе 0.25–0.5 равноценны |
w_τ (темп) |
1.0 | требование 1 («значительно ценнее»); в переборе безопасен до 1.0, вред — с 2.0 |
w_o (цели) |
0.5 | требование 4; равен весу миров, пока нет данных, что один из признаков информативнее (7.4) |
w_w (миры) |
0.5 | требование 4; в переборе 0.5 — лучший вес единственного признака отрыва |
c (близость) |
1 / 0.85 / 0.7 / 0.6 / 1 | требование 5, экспертная оценка по порядку тай-брейков, утверждена владельцем; симуляция не подтверждает и не опровергает (7.5) |
clamp |
[0.5, 2.0] | страховка от выбросов |
5. Требования → решения
| Требование из #22 | Механизм | Эффект (ветеран против равного, дуэль) |
|---|---|---|
| 1. Темп: 4 цели за 2 раунда ценнее, чем к концу 8-го | признак τ, вес 1.0 — самый большой из весов |
победа на 3-м раунде +11.2, на 8-м +5.5 — вдвое (пример 2) |
| 2. Размер стола, но слабее темпа | G(N), вес 0.5 |
1-е место: дуэль +8.0, пятеро +11.0, шестеро +12.0 — до ×1.5, меньше, чем ×2 у темпа (примеры 3, 6) |
| 3. Разница рейтингов с соперником | ожидание E |
1600 побеждает 1400: +3.8; 1400 побеждает 1600: +12.2 — втрое больше (пример 1) |
| 4. Цели и миры на конец партии | признаки o, w |
стол на 4: пары с выбывшими весят 1.25–1.5, пара лидеров — 0.7 (пример 5) |
| 5. Тип победы | близость c |
без деталей: по целям +8.0 → по мирам +6.8 → по пластику +5.6 → по ресурсам +4.8 (пример 4) |
| Веса параметров (из треб. 2) | единый множитель M с весами |
весь диапазон по отрыву: от +3.4 (самая близкая партия) до +16.0 (разгром) — ×4.7 (пример 4) |
6. Примеры расчётов
Все игроки опытные (40 партий, K = 16), если не сказано иное. Числа совпадают
с выводом simulate.py — скрипт проверяет их через assert. Промежуточные значения
здесь округлены. В приложении рейтинг показывается целым числом, а в расчёте хранится
без округления (раздел 8).
Пример 1. Дуэль 1600 против 1400 (треб. 3)
Партия из старой истории: только места и тип objectives, так что M = 1.
E(A выше B) = 1 / (1 + 10^(−200/400)) = 1 / (1 + 0.316) = 0.760.- 1a. Побеждает сильный A:
ΔR_A = 16 · 1 · (1 − 0.760) = +3.84, у B −3.84. - 1b. Побеждает слабый B:
ΔR_B = 16 · 1 · (1 − 0.240) = +12.16, у A −12.16.
Неожиданная победа приносит втрое больше ожидаемой.
Пример 2. Быстрая и медленная победа (треб. 1)
Равные (1500 и 1500), дуэль на поле 2×3, победа по целям 2:1, миры 5:4. Разница — только раунд.
| Раунд 3 (2a) | Раунд 8 (2b) | |
|---|---|---|
темп τ = (8 − r)/7 |
0.714 | 0.000 |
w_τ(τ − 1/7) |
+0.571 | −0.143 |
цели o = 1/2 → 0.5·(0.5 − 0.5) |
0 | 0 |
миры w = 1/6.6 = 0.152 → 0.5·(0.152 − 0.5) |
−0.174 | −0.174 |
A = M |
1.397 | 0.683 |
ΔR_A = 16 · M · (1 − 0.5) |
+11.18 | +5.46 |
Пример 3. Размер стола (треб. 2)
Все по 1500, партии без деталей (M = 1).
- 3a. Дуэль:
ΔR_1 = 16 · 1 · 0.5 = +8.00. - 3b. Стол на 5:
G(5) = 1 + 0.5·3/4 = 1.375, множитель перед суммой —16·1.375/4 = 5.5.
| Место | Σ (S − E) по 4 соперникам | ΔR | Сейчас (League Points) |
|---|---|---|---|
| 1 | 4·0.5 = 2.0 | +11.00 | 1.00 |
| 2 | −0.5 + 3·0.5 = 1.0 | +5.50 | 0.75 |
| 3 | 0 | 0.00 | 0.50 |
| 4 | −1.0 | −5.50 | 0.25 |
| 5 | −2.0 | −11.00 | 0.00 |
Пример 4. Тип победы и близость партии (треб. 5)
Равные, дуэль.
| Вариант | τ | o | w | A | c | M | ΔR победителя |
|---|---|---|---|---|---|---|---|
| по целям, без деталей (= 3a) | μ | μ | μ | 1.000 | 1.00 | 1.000 | +8.00 |
| 4a: по мирам, без деталей | μ | μ | μ | 1.000 | 0.85 | 0.850 | +6.80 |
| 4b: по пластику, без деталей | μ | μ | μ | 1.000 | 0.70 | 0.700 | +5.60 |
| 4c: по ресурсам, без деталей | μ | μ | μ | 1.000 | 0.60 | 0.600 | +4.80 |
| 4d: по мирам на 8-м раунде, цели 2:2, миры 6:5 | 0 | 0 | 0.152 | 0.433 → 0.5 | 0.85 | 0.425 | +3.40 |
| 4e: разгром — 3-й раунд, цели 2:0, миры 8:2 | 0.714 | 1 | 0.909 | 2.026 → 2.0 | 1.00 | 2.000 | +16.00 |
В 4d и 4e сработала страховка clamp.
Пример 5. Стол на 4 с выбывшими (треб. 4)
Партия закончилась на 7-м раунде по целям. G(4) = 1.25, множитель перед суммой —
16·1.25/3 = 6.67, честная доля миров — 6.6.
| Игрок | Рейтинг | Место | Цели | Миры |
|---|---|---|---|---|
| A | 1550 | 1 | 4 | 8 |
| B | 1500 | 2 | 3 | 7 |
| C | 1480 | 3 (выбыл) | 1 | 0 |
| D | 1450 | 3 (выбыл) | 0 | 0 |
Темп τ = 1/7 совпадает с типичным, его слагаемое равно 0.
| Пара | S | E | o | w | A = M | M·(S − E) |
|---|---|---|---|---|---|---|
| A–B | 1 | 0.571 | 1/4 | 1/6.6 = 0.152 | 1 − 0.125 − 0.174 = 0.701 | 0.300 |
| A–C | 1 | 0.599 | 3/4 | 1 | 1 + 0.125 + 0.25 = 1.375 | 0.551 |
| A–D | 1 | 0.640 | 1 | 1 | 1 + 0.25 + 0.25 = 1.500 | 0.540 |
| B–C | 1 | 0.529 | 2/4 | 1 | 1 + 0 + 0.25 = 1.250 | 0.589 |
| B–D | 1 | 0.571 | 3/4 | 1 | 1.375 | 0.589 |
| C–D | 0.5 | 0.543 | 1/4 (по модулю) | 0 | 1 − 0.125 − 0.25 = 0.625 | −0.027 |
ΔR_A = 6.67 · (0.300 + 0.551 + 0.540)= +9.27ΔR_B = 6.67 · (−0.300 + 0.589 + 0.589)= +5.85ΔR_C = 6.67 · (−0.551 − 0.589 − 0.027)= −7.78ΔR_D = 6.67 · (−0.540 − 0.589 + 0.027)= −7.35
Итого: A и B близки друг к другу по целям и мирам, поэтому эта пара весит 0.7. Отрыв обоих от выбывших огромный — эти пары весят 1.25–1.5. C и D поделили место, но C сильнее по рейтингу, поэтому немного уступает D.
Пример 6. Стол на 6 и хоумрул 9 раундов
Все по 1500, партия закончилась на 8-м раунде, других деталей нет.
G(6) = 1.5, множитель перед суммой — 16·1.5/5 = 4.8.
6a: хоумрул включён, R_max = 9 |
6b: хоумрул выключен, R_max = 8 |
|
|---|---|---|
темп τ |
(9 − 8)/8 = 0.125 | (8 − 8)/7 = 0 |
типичный μ_τ |
1/8 = 0.125 | 1/7 = 0.143 |
M пар с победителем |
1.000 — обычная партия | 0.857 — затянутая |
| ΔR по местам 1…6 | +12.00, +7.20, +2.40, −2.40, −7.20, −12.00 | +10.29, +7.54, +2.74, −2.06, −6.86, −11.66 |
Конец на 8-м раунде при лимите 9 — это «предпоследний раунд», то есть типичная партия. При лимите 8 — затянутая партия, и победа весит меньше. Поэтому лимит раундов снимается в партию при её создании (раздел 8).
Пример 7. Новичок против ветерана
Оба по 1500; A — новичок (0 партий, K = 64), B — 40 партий (K = 16). A побеждает.
ΔR_A = 64 · 0.5 = +32, ΔR_B = 16 · (−0.5) = −8.
О силе новичка ещё ничего не известно, поэтому его рейтинг двигается быстро. Ветеран теряет как за обычное поражение от равного.
7. Проверка на симуляции
7.1. Модель лиги
У настоящих партий пока нет раундов, целей и миров, поэтому система проверяется на синтетической лиге, где «истинная» сила игроков известна.
| Параметр | Значение |
|---|---|
| Игроки | 12 со старта, ещё по 2 на 1/3 и 2/3 сезона; активность у каждого своя (0.5–1.5) |
| Сезон | 300 партий; хоумрул 9 раундов включён в половине сезонов |
| Размер стола | 2 — 45%, 3 — 25%, 4 — 20%, 5 — 7%, 6 — 3% |
| Сила | θ ~ N(0, 150); в шкале рейтинга ≈ ±140 |
| Производительность в партии | θ + N(0, 225) — кубы, карты, ошибки; места — по производительности |
| Детали партии | из отрыва производительности: раунд (чем больше отрыв, тем раньше конец), цели, миры, тип победы, выбывание. Получается 81% побед по целям, 10% по мирам, 4% по пластику, 1.4% по ресурсам, 3% последний выживший; чаще всего конец на 7–8 раунде |
Сценарии:
| Сценарий | Что проверяет |
|---|---|
| сигнал | базовый: отрыв связан с разницей сил |
| шум | места те же, но величина отрыва случайна и с силой не связана — сколько система теряет, если признаки ничего не говорят |
| клубы | три группы разной силы (−150 / 0 / +150), 90% партий внутри своей — умеет ли общий рейтинг «сшить» группы |
| рост | новички стартуют слабее на 0–200 и догоняют с опытом (×1/e за 15 партий) — успевает ли рейтинг за ростом игрока |
7.2. Метрики
- Точность — доля пар без ничьих во второй половине сезона, где до партии рейтинг выше у занявшего место выше. Потолок — тот же прогноз по истинной силе.
- Brier —
(1 − E)²по тем же парам, меньше — лучше. Показывает, насколько честны сами вероятности; есть только у Elo-систем. - ρ — ранговая корреляция Спирмена рейтинга на конец сезона с истинной силой (игроки с 10+ партиями).
- ρ@k — то же сразу после k-й партии игрока: как быстро рейтинг «находит» игрока.
- RMSE — ошибка рейтинга относительно истинной силы в пунктах шкалы.
- Наклон — регрессия рейтинга на истинную силу: 1.0 — разброс честный, меньше — рейтинги сжаты к середине.
- |ΔR| — средний модуль изменения за партию у игроков с 20+ партиями (волатильность). У League Points рейтинг в шкале 0–100, его |ΔR| приведён к шкале 1500 умножением на 10 (соответствие из 4.1).
Сравнение идёт на одних и тех же сезонах (парные разности). Коэффициенты подбирались на других сезонах (7.4), так что это проверка вне выборки подбора.
7.3. Результаты: 200 сезонов на сценарий
«Без новых полей» — предложенная система на той же истории, но без раунда, целей и миров: так будет считаться история, накопленная до #23.
Сигнал (потолок точности 0.6843)
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | |ΔR| |
|---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6683 | — | 0.913 | 0.660 | 0.769 | 0.854 | — | — | 6.91 |
| Elo, чистый | 0.6691 | 0.2089 | 0.911 | 0.656 | 0.770 | 0.852 | 47.4 | 0.81 | 5.24 |
| Предложенная | 0.6721 | 0.2077 | 0.929 | 0.711 | 0.807 | 0.881 | 41.9 | 0.95 | 5.95 |
| Предложенная, без новых полей | 0.6701 | 0.2085 | 0.916 | 0.677 | 0.782 | 0.864 | 47.4 | 0.79 | 5.90 |
Клубы (потолок 0.6308)
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | |ΔR| |
|---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6045 | — | 0.525 | 0.303 | 0.391 | 0.463 | — | — | 8.06 |
| Elo, чистый | 0.6063 | 0.2341 | 0.632 | 0.326 | 0.440 | 0.535 | 102.6 | 0.40 | 5.71 |
| Предложенная | 0.6102 | 0.2336 | 0.644 | 0.356 | 0.463 | 0.555 | 100.3 | 0.47 | 6.53 |
| Предложенная, без новых полей | 0.6074 | 0.2332 | 0.633 | 0.336 | 0.440 | 0.534 | 103.3 | 0.38 | 6.41 |
Рост (потолок 0.6864)
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | |ΔR| |
|---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6688 | — | 0.915 | 0.631 | 0.732 | 0.824 | — | — | 6.91 |
| Elo, чистый | 0.6687 | 0.2086 | 0.916 | 0.632 | 0.733 | 0.825 | 48.0 | 0.81 | 5.25 |
| Предложенная | 0.6726 | 0.2076 | 0.929 | 0.685 | 0.774 | 0.851 | 42.7 | 0.95 | 5.96 |
| Предложенная, без новых полей | 0.6697 | 0.2081 | 0.922 | 0.651 | 0.750 | 0.835 | 47.6 | 0.79 | 5.91 |
Шум (потолок 0.6894)
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | |ΔR| |
|---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6737 | — | 0.916 | 0.656 | 0.761 | 0.848 | — | — | 6.89 |
| Elo, чистый | 0.6739 | 0.2070 | 0.917 | 0.659 | 0.765 | 0.851 | 46.1 | 0.82 | 5.23 |
| Предложенная | 0.6727 | 0.2076 | 0.908 | 0.643 | 0.750 | 0.844 | 48.6 | 0.81 | 5.91 |
| Предложенная, без новых полей | 0.6743 | 0.2070 | 0.919 | 0.667 | 0.775 | 0.856 | 47.4 | 0.78 | 5.88 |
Парные разности (среднее ± стандартная ошибка по 200 сезонам):
| Сценарий | Brier: предложенная − чистый Elo | Точность: предложенная − чистый Elo | Точность: предложенная − сейчас |
|---|---|---|---|
| сигнал | −0.0011 ± 0.0001 | +0.30 ± 0.06 п.п. | +0.37 ± 0.07 п.п. |
| клубы | −0.0005 ± 0.0002 | +0.38 ± 0.08 п.п. | +0.57 ± 0.10 п.п. |
| рост | −0.0010 ± 0.0001 | +0.39 ± 0.07 п.п. | +0.37 ± 0.06 п.п. |
| шум | +0.0006 ± 0.0001 | −0.12 ± 0.06 п.п. | −0.10 ± 0.07 п.п. |
Выводы:
- Если отрыв отражает силу (а требования #22 исходят именно из этого), предложенная система лучше обеих альтернатив по всем метрикам качества. Сильнее всего она выигрывает в скорости: после 5 партий ρ = 0.71 против 0.66 у текущей, после 10 — 0.81 против 0.77. Рейтинг меньше сжат к середине (наклон 0.95 против 0.81): сильные игроки быстрее отрываются от середняков.
- Сила соперников — главное преимущество Elo над текущей системой. В «клубах» текущая система упорядочивает игроков заметно хуже (ρ 0.525 против 0.644): чемпион слабой группы у неё стоит рядом с чемпионом сильной.
- Если отрыв — шум, предложенная система теряет 0.1 п.п. точности и 0.0006 Brier — цена лишней волатильности. Это худший из рассмотренных случаев: в остальных сценариях она чистому Elo не уступает.
- История без новых полей считается как чистый Elo с учётом стола и типа победы. По точности и Brier она не хуже чистого Elo ни в одном сценарии.
- Абсолютные разности точности малы (доли процента), потому что партия Forbidden Stars сама по себе сильно случайна: даже знание истинной силы угадывает порядок пары лишь в 68% случаев. Все системы близки к этому потолку. Качество рейтинга лучше видно по ρ@k и RMSE, чем по точности.
7.4. Подбор коэффициентов
simulate.py --grid перебирает коэффициенты на других 40 сезонах каждого сценария.
Критерий — средний Brier, меньше — лучше. Разница в 0.0001 — примерно граница шума.
Этап 1. K чистого Elo (все четыре сценария). Спуск K за 20 партий лучше, чем за 10. Выгоден высокий K новичка и низкий K ветерана.
K_max \ K_min (спуск за 20 партий) |
16 | 24 | 32 |
|---|---|---|---|
| 48 | 0.21697 | 0.21708 | 0.21775 |
| 64 | 0.21649 | 0.21675 | 0.21751 |
| 96 | 0.21639 | 0.21669 | 0.21747 |
| 128 | 0.21682 | 0.21703 | 0.21775 |
| 160 | 0.21748 | 0.21755 | 0.21819 |
Лучший вариант со спуском за 10 партий — 0.21676 (96 / 24). Для «чистого Elo» в сравнении 7.3 взяты K = 96 / 16 / 20. Чистый Elo не использует миры, поэтому поле 2×3 на этот этап не повлияло.
Этап 2. Веса отрыва при K этапа 1: 162 комбинации (w_N ∈ {0, 0.25, 0.5};
w_τ, w_o, w_w ∈ {0, 0.5, 1}; близость да/нет); сценарии «сигнал», «клубы», «рост».
Без множителя (чистый Elo с K этапа 1): Brier 0.21846, на «шуме» 0.21017. Лучшие комбинации:
| Brier | Brier «шум» | w_N |
w_τ |
w_o |
w_w |
близость |
|---|---|---|---|---|---|---|
| 0.21797 | 0.20991 | 0.25 | 0 | 0 | 0.5 | да |
| 0.21801 | 0.20985 | 0.5 | 0 | 0 | 0.5 | да |
| 0.21802 | 0.21009 | 0 | 0 | 0 | 0.5 | да |
| 0.21804 | 0.21005 | 0.25 | 0.5 | 0 | 0.5 | да |
| 0.21804 | 0.20994 | 0.25 | 0 | 0 | 0.5 | нет |
| … | ||||||
| 0.21995 | худшая комбинация |
Лучшие комбинации выигрывают у отсутствия множителя около 0.0005, худшая проигрывает 0.0015. Оптимум очень пологий: первые десять вариантов умещаются в 0.0001.
Перебор оставляет один признак отрыва из трёх. Это ожидаемо: в генераторе раунд, цели и миры выводятся из одного и того же отрыва производительности, второй признак не добавляет информации и лишь увеличивает разброс. Реальная игра так не устроена: в ней ранний конец, счёт целей и контроль миров — разные стороны партии. Поэтому веса признаков заданы требованиями #22 в пределах безопасной зоны (этап 4), а не взяты из вершины перебора.
Этап 3. K для предложенных весов. Множитель в среднем чуть больше 1, поэтому K нужен меньше, чем у чистого Elo. Спуск за 20 партий:
K_max |
K_min |
Brier (сигнальные) | Brier «шум» | Brier с поправкой на автокорреляцию |
|---|---|---|---|---|
| 48 | 12 | 0.21759 | 0.21063 | 0.21755 |
| 48 | 16 | 0.21778 | 0.21049 | 0.21773 |
| 64 | 12 | 0.21779 | 0.21026 | 0.21771 |
| 64 | 16 | 0.21797 | 0.21022 | 0.21788 |
| 80 | 16 | 0.21838 | 0.21028 | 0.21826 |
- Выбор 64 / 16. Против чистого Elo он даёт −0.00049 на сигнальных сценариях и +0.00005 на «шуме». Вариант 48 / 12 выигрывает больше (−0.00087), но на «шуме» проигрывает +0.00046. Выбран вариант, который не теряет, если гипотеза ТЗ о значении отрыва не подтвердится.
- Поправка на автокорреляцию (FiveThirtyEight: фаворит закономерно побеждает с большим отрывом, и без поправки его рейтинг раздувается) даёт около 0.0001. В формулу она не включена: лишняя сложность для ручного расчёта при нулевом эффекте.
Этап 4. Чувствительность. Меняется один параметр, остальные — как в предложении (Brier 0.21797, «шум» 0.21022). ρ@10 здесь — среднее по трём сценариям, включая «клубы», поэтому оно ниже, чем в 7.3.
| Параметр | Значение | Brier (сигнальные) | ρ@10 | Brier «шум» |
|---|---|---|---|---|
w_N |
0 / 0.25 / 0.5 / 1.0 | 0.21803 / 0.21796 / 0.21797 / 0.21819 | 0.663 / 0.668 / 0.674 / 0.680 | 0.21072 / 0.21041 / 0.21022 / 0.21011 |
w_τ |
0 / 0.25 / 0.5 / 1.0 / 2.0 | 0.21771 / 0.21775 / 0.21781 / 0.21797 / 0.21826 | 0.672 / 0.672 / 0.673 / 0.674 / 0.667 | 0.20988 / 0.20995 / 0.21003 / 0.21022 / 0.21050 |
w_o |
0 / 0.25 / 0.5 / 1.0 / 2.0 | 0.21767 / 0.21778 / 0.21797 / 0.21838 / 0.21892 | 0.666 / 0.668 / 0.674 / 0.674 / 0.671 | 0.21003 / 0.21010 / 0.21022 / 0.21042 / 0.21073 |
w_w |
0 / 0.25 / 0.5 / 1.0 / 2.0 | 0.21781 / 0.21785 / 0.21797 / 0.21827 / 0.21873 | 0.667 / 0.671 / 0.674 / 0.673 / 0.669 | 0.21003 / 0.21011 / 0.21022 / 0.21037 / 0.21054 |
c |
нет / предложенная / вдвое сильнее | 0.21799 / 0.21797 / 0.21799 | 0.673 / 0.674 / 0.672 | 0.21016 / 0.21022 / 0.21031 |
Итог:
- Веса в диапазоне 0–1 безопасны: изменение вдвое сдвигает Brier не больше чем на 0.00041
(
w_o0.5 → 1.0). Вред начинается с 2.0 — поэтому ни один вес не выше 1. - Вес размера стола полезен: от 0 до 0.5 растёт ρ@10 и падает Brier на «шуме».
- Близость по типу победы в симуляции нейтральна (±0.00002).
7.5. Что симуляция доказывает и что нет
-
Доказывает: формулы корректны и устойчивы, не раздувают рейтинг, быстро сходятся, правильно «сшивают» группы разной силы. Выбранные веса лежат в пологой области: изменение любого веса вдвое в любую сторону сдвигает Brier не больше чем на 0.00041. Если признаки партии окажутся бесполезны, потеря мала.
-
Не доказывает:
- что в реальных партиях Forbidden Stars ранний конец, отрыв по целям и мирам связаны с разницей сил так, как заложено в генераторе. Это допущение, на котором стоит и само ТЗ;
- какой из признаков отрыва (раунд, цели, миры) информативнее: в генераторе все три выводятся из одного отрыва и дублируют друг друга.
Проверить это можно только на реальных данных после внедрения #23 (раздел 8, «Калибровка»).
-
Коэффициенты близости
cсимуляция не подтверждает и не опровергает: при записанных целях и мирах тип победы почти не добавляет информации. Значения — экспертная оценка по порядку тай-брейков. Их главная роль — старые партии, где тип победы — единственный признак хода игры.
8. Что потребуется в реализации (#23)
Изменение ломающее (Compat/Breaking): у всех игроков меняются числа рейтинга и,
вероятно, порядок в топе.
Данные (миграция 0014_*)
| Где | Поле | Тип | Смысл |
|---|---|---|---|
groups |
nine_rounds_rule |
bool, default false | галочка «9 раундов при 5–6 игроках» |
matches |
nine_rounds_rule |
bool, default false | снимок настройки группы при создании партии: смена настройки не должна переписывать историю (пример 6) |
matches |
end_round |
int NULL, 1 ≤ end_round ≤ R_max |
раунд, в котором партия закончилась |
match_participants |
objectives |
int NULL, ≥ 0 | маркеры целей на конец партии |
match_participants |
worlds |
int NULL, ≥ 0 | дружественные миры на конец партии; у выбывшего 0 |
matches.win_reason |
+ last_standing |
CHECK | новая причина победы (раздел 9) |
R_max в партии не хранится, а вычисляется: 9, если nine_rounds_rule и N ≥ 5, иначе 8.
Число участников может поменяться при правке партии, а снимок правила — нет.
Миграция идемпотентна, как 0003: ALTER только при отсутствии столбца, render_as_batch
для CHECK. Новые столбцы задним числом не заполняются: NULL — это «нет данных», и формулы
его учитывают (4.8).
Бэкфилл нужен только для last_standing. У завершённых партий, где невыбывший участник
ровно один, миграция ставит win_reason = last_standing. Тогда старые партии не нарушают
правило из раздела «Ввод» при правке, а в рейтинге считаются так же, как новые: отрыв
победителя по целям = 1 (4.7). Бэкфилл идемпотентен: повторный запуск ничего не меняет.
Ввод
- Форма завершения (
MatchDetailPage,match_service.finish_match, черновикMatchFinishDraft) и админская правка (AdminMatchEdit): раунд окончания и у каждого участника цели и миры. Поля необязательные — пропуск лучше выдумки. - Серверная валидация — только диапазоны и явные противоречия: у выбывшего миры = 0;
раунд ≤
R_max. Подсказки о согласованности (типworldsпри неравных целях лидеров и т.п.) лучше показывать предупреждением, а не отказом. - Настройки группы: галочка рядом с дополнениями (
PUT /groups/{id}/expansionsили отдельныйPATCH). - Причина
last_standing(решение владельца):- правило — «невыбывший участник ровно один» ⇔
win_reason = last_standing; - в форме завершения и в админской правке причина проставляется автоматически, как только все участники, кроме одного, отмечены выбывшими. Выбор причины при этом заблокирован;
- вернули второго невыбывшего — причина сбрасывается, её нужно выбрать заново;
- в выпадающем списке причин
last_standingнет: выбрать её вручную нельзя; - сервер проверяет то же правило в
finish_matchи при правке результатов: несовпадение — ошибка валидации. Черновик формы (PUT /matches/{id}/finish-draft) правило не проверяет, он хранит незаконченный ввод.
- правило — «невыбывший участник ровно один» ⇔
Отображение
- Рейтинг показывается целым числом; в расчёте значения хранятся без округления, иначе ошибка округления накапливается по цепочке партий.
- Общий топ (
OverallStatsPage.tsx): столбец «Поб» и сортировка по победам убираются, чтобы четырёхзначный рейтинг поместился в строку; «Очки» переименовываются в «Рейтинг». Для единообразия — «Очки (рейтинг)» вProfileStatsCard.tsxи заголовок «Очки игроков (рейтинг)» вHelpPage.tsx. - Подпись причины
last_standingв карточке партии и истории — «последний выживший».
Расчёт
- Рейтинг — функция упорядоченной истории, поэтому он пересчитывается проигрыванием
завершённых партий по порядку (
played_at,finished_at,id), а не агрегатом SQL. Данных мало: сотни партий, микросекунды на пару. Существующий принцип «считается вживую» сохраняется, кэш можно ввести позже с инвалидацией по уже существующим SSE-событиям. - Одна цепочка (решение владельца 2026-09-15, #80): рейтинг у игрока один — по всем партиям приложения, K — по всем его партиям. Отдельного группового рейтинга нет: на странице группы игры, победы, винрейт и среднее место считаются по партиям группы, а рейтинг и статус «Новичок» — общие. Первая версия реализации (#23) держала две цепочки, общую и групповую, — это оказалось неинтуитивно (раздел 9).
- Правка или удаление прошлой партии автоматически меняет всё после неё: при пересчёте с нуля отдельной логики не нужно.
- Эталон —
rate_matchвsimulate.py. Примеры из раздела 6 стоит перенести в тесты бэкенда как есть. - Смежные метрики на старых очках места:
- «лучшая партия» в профиле (
user_match_list(best_only)) → партия с наибольшим ΔR; - «лучшая/худшая фракция» → средний
S − Eна фракции: насколько игрок на ней выступает выше ожидания, без привязки к рейтингу; win_rate,avg_place, «форма» — без изменений.
- «лучшая партия» в профиле (
Что ломается для пользователей
- Числа рейтинга у всех меняются: шкала другая (около 1500 вместо около 50), и это другая величина — не «средний процент очков», а сила относительно соперников.
- Порядок в топе может измениться — в первую очередь у тех, кто играл в основном со слабыми или сильными соперниками.
- Рейтинг новичка после одной партии меняется заметно сильнее, чем раньше: +32 за обычную победу над равным, до +64 за разгром (теоретический предел — 128).
- В общем топе пропадает столбец побед (win rate остаётся).
- Предложение: разовое уведомление всем игрокам и короткое пояснение «как считается рейтинг» на странице топа.
Калибровка после внедрения
Когда наберётся ~100 партий с заполненными раундом, целями и мирами:
- типичные значения
μзаменить средними по реальным партиям; - повторить перебор весов из
simulate.pyна реальной истории: критерий — Brier прогноза следующей партии; - проверить главное допущение: есть ли у ранних побед и большого отрыва связь с последующими результатами игроков.
Коэффициенты — константы в одном модуле (как сейчас scoring.py): калибровка — это правка
констант и пересчёт, без миграций.
Справка
Формула текущего рейтинга и пороги продублированы текстом на странице справки
(frontend/src/pages/HelpPage.tsx). При реализации #23 её нужно переписать под новую
систему: шкала, от чего зависит изменение рейтинга, MIN_GAMES, причина last_standing.
9. Решения владельца
Первая версия документа выносила шесть вопросов на решение. Ответы владельца — комментарий к PR #65 (2026-09-14). Там же и в #22 уточнено поле дуэли: 2×3, а не 2×2 — исправлено в разделе 2, примерах и симуляции.
| # | Вопрос | Решение | Что изменилось в документе |
|---|---|---|---|
| 1 | Шкала отображения: «Elo/10» (старт 50) или классические 1500 | 1500. В общем топе убрать столбец побед, «Очки» переименовать в «Рейтинг» | 4.1 и все числа примеров и таблиц; раздел 8, «Отображение» |
| 2 | Коэффициенты близости по типам победы (1 / 0.85 / 0.7 / 0.6 / 1) | Согласованы | 4.10 — отмечены как утверждённые |
| 3 | Новая причина победы last_standing |
Добавить. Ставится автоматически, когда невыбывший ровно один, и не меняется, пока невыбывших меньше двух; в списке выбора её нет | раздел 2; раздел 8 — «Данные» (бэкфилл) и «Ввод» |
| 4 | Ввод миров на конец партии | Оставить | без изменений: w_w = 0.5 |
| 5 | Затухание за неактивность | Не добавлять | без изменений |
| 6 | Минимум партий для топа | Оставить 10 | без изменений: MIN_GAMES = 10 |
| 7 | Групповой рейтинг отдельной цепочкой (после внедрения, #80, 2026-09-15) | Убрать. Рейтинг единый; на странице группы — показатели по партиям группы, на главной и в профиле — общие | раздел 8, «Расчёт» |
Открытых вопросов по предложению не осталось. Калибровка коэффициентов на реальных данных — после внедрения #23 (раздел 8, «Калибровка»).