diff --git a/docs/rating/rating-system.md b/docs/rating/rating-system.md
index 1d6c78e..175fe5b 100644
--- a/docs/rating/rating-system.md
+++ b/docs/rating/rating-system.md
@@ -1,7 +1,7 @@
# Новая рейтинговая система
-> **Статус:** предложение на утверждение (#22). Решения владельца по открытым вопросам
-> (2026-09-14) внесены — [раздел 9](#9-решения-владельца). Реализация — #23.
+> **Статус:** утверждено (#22), реализовано в #23 (`backend/app/services/scoring.py`).
+> Решения владельца по открытым вопросам (2026-09-14) внесены — [раздел 9](#9-решения-владельца).
> **Приложение:** [`simulate.py`](simulate.py) — эталонная реализация формул, все примеры
> этого документа (с `assert`), симуляция и перебор коэффициентов. Только стандартная
> библиотека Python, вывод воспроизводим:
diff --git a/frontend/src/components/ProfileStatsCard.tsx b/frontend/src/components/ProfileStatsCard.tsx
index 496d9e2..c2df55b 100644
--- a/frontend/src/components/ProfileStatsCard.tsx
+++ b/frontend/src/components/ProfileStatsCard.tsx
@@ -35,7 +35,7 @@ export function ProfileStatsCard({
{o.avg_place ?? "—"}
-
Очки (рейтинг)
+
Рейтинг
{o.score ?? "—"}
{/* Любимая — личный выбор игрока в профиле; ниже — статистика по партиям. */}
diff --git a/frontend/src/pages/HelpPage.tsx b/frontend/src/pages/HelpPage.tsx
index e7fe227..aebfefc 100644
--- a/frontend/src/pages/HelpPage.tsx
+++ b/frontend/src/pages/HelpPage.tsx
@@ -2,8 +2,9 @@ import { ChevronDown, ChevronUp } from "lucide-react";
import { useState } from "react";
/**
- * Справка — разделы, раскрывающиеся по кнопке. Пока раздел один: «Рейтинг»
- * (как считаются очки игроков). Текст соответствует backend/app/services/scoring.py.
+ * Справка — разделы, раскрывающиеся по кнопке. Пока раздел один: «Рейтинг».
+ * Текст соответствует backend/app/services/scoring.py и docs/rating/rating-system.md:
+ * коэффициенты, пороги и примеры менять вместе с ними.
*/
export function HelpPage() {
const [showRating, setShowRating] = useState(false);
@@ -32,50 +33,113 @@ export function HelpPage() {
{showRating && (
<>
-
Очки игроков (рейтинг)
+
Рейтинг игроков
- Рейтинг считается только по завершённым партиям. Незавершённые и отменённые
- в зачёт не идут.
+ Рейтинг — оценка силы игрока относительно соперников (система Elo). Каждый начинает
+ с 1500. Разница в 400 пунктов означает шансы 10 к 1 в пользу более
+ сильного.
- За каждую партию игрок получает «очки за место» — они нормированы по числу игроков
- за столом, поэтому победа за большим столом ценится выше, чем за маленьким:
+ Считаются только завершённые партии, по порядку их игры. Правка или удаление
+ старой партии пересчитывает и все последующие.
+ Партия раскладывается на пары игроков. В каждой паре фактический результат (выше —
+ 1, поровну — 0.5, ниже — 0) сравнивается с ожидаемым по рейтингам до партии:
+
- Пока партий мало, рейтинг держится около 50 и с опытом сходится к реальному
- среднему — стабильные результаты на длинной дистанции ценятся выше короткой
- удачной серии.
+ где N — число игроков, сумма — по всем соперникам. Поэтому победа над сильным
+ приносит больше, чем над слабым, а поражение от слабого отнимает больше. Выбывшие
+ делят последнее место.
-
Пример (стол на 4 игроков)
+
От чего зависит изменение
+
+
+ K — скорость. У новичка 64, к 20-й партии плавно снижается до 16: новичок
+ быстро находит свой уровень, а рейтинг опытного игрока не скачет.
+
+
+ G — размер стола: 1 + 0.5 × (N − 2) / 4. В дуэли 1.0, вчетвером 1.25,
+ вшестером 1.5.
+
+
+ M — отрыв в паре, от 0.5 до 2. Обычная партия даёт 1. Больше, если:
+
+
+ партия закончилась раньше — для пар с победителем; обычной считается конец
+ в предпоследнем раунде;
+
+
разница целей велика относительно числа игроков;
+
разница миров велика относительно доли миров на игрока.
+
+
+
+ Тип победы — для пар с победителем, чем ближе партия, тем меньше:
+ по целям ×1, по мирам ×0.85, по пластику ×0.7, по ресурсам ×0.6,
+ последний выживший ×1.
+
+
+
+ Раунд окончания, цели и миры вводить необязательно: пропущенное считается
+ обычным значением и множитель не меняет. «Последний выживший» ставится сам, когда
+ все соперники выбыли, — отрыв по целям тогда максимальный. Лимит раундов — 8,
+ с правилом группы «9 раундов» при 5–6 игроках — 9.
+
+
+ Победитель никогда не теряет рейтинг, а последнее место никогда его не приносит.
+
+
+
+
+
Примеры (опытные игроки)
-
1-е место(4−1−0)/3 = 1.00 → 100
-
2-е место(4−2−0)/3 = 0.67 → 67
-
3-е место(4−3−0)/3 = 0.33 → 33
-
4-е место(4−4−0)/3 = 0.00 → 0
- Ничья за 1-е (двое)(4−1−0.5)/3 = 0.83 каждому
+ Дуэль 1600 и 1400: победил сильный
+ +3.8 / −3.8
+
+
+ Дуэль 1600 и 1400: победил слабый
+ +12.2 / −12.2
+
+
+ Равные, победа по целям без деталей
+ +8.0
+
+
+ Равные, победа по мирам без деталей
+ +6.8
+
+
+ Разгром на 3-м раунде (цели 2:0, миры 8:2)
+ +16.0
+
+
+ 1-е место за столом на 5 равных
+ +11.0
+
+
+ Новичок обыграл равного опытного
+ +32 / −8
+
+ Рейтинг показывается целым числом, но считается без округления.
+
+
+
+
+
Общий и групповой рейтинг
+
+ Общий рейтинг считается по всем партиям приложения, групповой — только по партиям
+ группы. Поэтому числа в общем топе и в топе группы различаются.
+
@@ -91,6 +155,7 @@ export function HelpPage() {
Винрейт (WR) — доля партий, в которых игрок занял 1-е место.
Среднее место — среднее по завершённым партиям.
+
Лучшая партия в профиле — та, что принесла больше всего рейтинга.
При равенстве рейтинга топ сортируется по: винрейту, числу игр, среднему месту
и затем по нику.
diff --git a/frontend/src/pages/OverallStatsPage.tsx b/frontend/src/pages/OverallStatsPage.tsx
index 581898b..3cdf87a 100644
--- a/frontend/src/pages/OverallStatsPage.tsx
+++ b/frontend/src/pages/OverallStatsPage.tsx
@@ -6,7 +6,9 @@ import { Spinner } from "../components/Spinner";
import type { LeaderboardEntry } from "../domain/types";
import { useLeaderboard } from "../hooks/stats";
-type SortKey = "rank" | "games" | "wins" | "win_rate" | "score";
+// Столбца побед нет: четырёхзначный рейтинг иначе не помещается в строку, а доля побед
+// уже есть в WR.
+type SortKey = "rank" | "games" | "win_rate" | "score";
function pct(v: number | null | undefined): string {
return v == null ? "—" : `${Math.round(v * 100)}%`;
@@ -50,7 +52,6 @@ export function OverallStatsPage() {
{e.nickname}{e.games}
- {e.wins}{pct(e.win_rate)}
{e.score ?? "—"}
@@ -70,9 +71,8 @@ export function OverallStatsPage() {
Игрок
-
-
+