Рейтинг: единый рейтинг вместо группового
Групповая цепочка убрана: рейтинг у игрока один, по всем партиям приложения (решение владельца). 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
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Список игроков группы — только текущий состав (#76).
|
||||
|
||||
Удалённый из группы игрок пропадает из рейтинга группы, но его партии остаются в
|
||||
групповой цепочке: они уже повлияли на рейтинг оставшихся."""
|
||||
истории: они уже повлияли на (общий) рейтинг оставшихся."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Рейтинг в витринах (#23): Elo по упорядоченной истории, общая и групповая цепочки."""
|
||||
"""Рейтинг в витринах (#23, #80): Elo по упорядоченной истории, один рейтинг на игрока."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -100,8 +100,9 @@ def test_editing_past_match_recalculates_later_ones(client: TestClient, engine):
|
||||
assert {uid: e["score"] for uid, e in _board(client).items()} == after
|
||||
|
||||
|
||||
def test_group_rating_is_its_own_chain(client: TestClient, engine):
|
||||
"""Групповой рейтинг проигрывает только партии группы, общий — все партии."""
|
||||
def test_group_page_shows_overall_rating_with_group_stats(client: TestClient, engine):
|
||||
"""Рейтинг один на всё приложение (#80): в группе он тот же, что в общем топе,
|
||||
а игры и победы — только по партиям группы."""
|
||||
me = login(client, "Путешественник")
|
||||
g1, f1 = _group(client, "Первая")
|
||||
g2, f2 = _group(client, "Вторая")
|
||||
@@ -110,16 +111,55 @@ def test_group_rating_is_its_own_chain(client: TestClient, engine):
|
||||
_duel(client, g1, f1, me["id"], b) # в первой группе — победа
|
||||
_duel(client, g2, f2, c, me["id"]) # во второй — поражение
|
||||
|
||||
assert _board(client, f"/api/groups/{g1}/stats")[me["id"]]["score"] == 1532
|
||||
assert _board(client, f"/api/groups/{g2}/stats")[me["id"]]["score"] == 1468
|
||||
overall = _board(client)[me["id"]]["score"]
|
||||
assert overall not in (1532, 1468)
|
||||
assert overall == round(
|
||||
overall = _board(client)[me["id"]]
|
||||
assert overall["games"] == 2
|
||||
assert overall["score"] == round(
|
||||
replay([
|
||||
RatedMatch((RatedSeat(me["id"], 1), RatedSeat(b, 2)), "objectives", id=1),
|
||||
RatedMatch((RatedSeat(c, 1), RatedSeat(me["id"], 2)), "objectives", id=2),
|
||||
]).ratings[me["id"]]
|
||||
)
|
||||
first = _board(client, f"/api/groups/{g1}/stats")[me["id"]]
|
||||
second = _board(client, f"/api/groups/{g2}/stats")[me["id"]]
|
||||
assert first["score"] == second["score"] == overall["score"]
|
||||
assert (first["games"], first["wins"]) == (1, 1)
|
||||
assert (second["games"], second["wins"]) == (1, 0)
|
||||
|
||||
# Главная и профиль — общие показатели; блок активной группы — игры в группе.
|
||||
client.put("/api/users/me/active-group", json={"group_id": g2}, headers=csrf_headers(client))
|
||||
home = client.get("/api/home").json()
|
||||
assert (home["profile"]["overall"]["games"], home["profile"]["overall"]["score"]) == (
|
||||
2, overall["score"]
|
||||
)
|
||||
assert (home["active_group"]["games"], home["active_group"]["score"]) == (1, overall["score"])
|
||||
|
||||
|
||||
def test_veteran_is_not_a_newcomer_in_new_group(client: TestClient, engine):
|
||||
"""Статус «Новичок» — про надёжность рейтинга, а он общий: 10 партий где угодно
|
||||
делают игрока ранжированным и в группе, где он сыграл одну."""
|
||||
me = login(client, "Ветеран")
|
||||
g1, f1 = _group(client, "Старая")
|
||||
b = add_group_member(engine, g1, "Спарринг")
|
||||
for _ in range(10):
|
||||
_duel(client, g1, f1, me["id"], b)
|
||||
|
||||
g2, f2 = _group(client, "Новая")
|
||||
c = add_group_member(engine, g2, "Новенький")
|
||||
add_group_member(engine, g2, "Спарринг") # опытный, но в новой группе не играл
|
||||
d = add_group_member(engine, g2, "Зритель") # не играл нигде
|
||||
_duel(client, g2, f2, me["id"], c)
|
||||
|
||||
stats = client.get(f"/api/groups/{g2}/stats").json()
|
||||
ranked = {e["user_id"]: e for e in stats["leaderboard"]}
|
||||
assert ranked[me["id"]]["rank"] == 1
|
||||
assert (ranked[me["id"]]["games"], ranked[me["id"]]["rating_confirmed"]) == (1, True)
|
||||
assert ranked[me["id"]]["score"] == _board(client)[me["id"]]["score"]
|
||||
assert [e["user_id"] for e in stats["provisional"]] == [c]
|
||||
|
||||
inactive = {e["user_id"]: e for e in stats["inactive"]}
|
||||
assert inactive[b]["score"] == _board(client)[b]["score"]
|
||||
assert (inactive[b]["games"], inactive[b]["rating_confirmed"]) == (0, True)
|
||||
assert (inactive[d]["score"], inactive[d]["rating_confirmed"]) == (None, False)
|
||||
|
||||
|
||||
def test_best_match_is_biggest_rating_gain(client: TestClient, engine):
|
||||
|
||||
@@ -52,17 +52,17 @@ def test_home_loads_history_once(client: TestClient, engine, monkeypatch):
|
||||
"/api/users/me/active-group", json={"group_id": gid}, headers=csrf_headers(client)
|
||||
)
|
||||
|
||||
calls: list[int | None] = []
|
||||
calls: list[int] = []
|
||||
original = stats_service.load_history
|
||||
|
||||
def spy(session, group_id=None):
|
||||
calls.append(group_id)
|
||||
return original(session, group_id)
|
||||
def spy(session):
|
||||
calls.append(1)
|
||||
return original(session)
|
||||
|
||||
monkeypatch.setattr(stats_service, "load_history", spy)
|
||||
r = client.get("/api/home")
|
||||
assert r.status_code == 200, r.text
|
||||
assert calls == [None]
|
||||
assert len(calls) == 1
|
||||
# Главная всё ещё показывает и профиль, и блок активной группы.
|
||||
body = r.json()
|
||||
assert body["profile"]["overall"]["games"] == 2
|
||||
|
||||
Reference in New Issue
Block a user