История: изменение рейтинга на карточках партий профиля
GET /users/{id}/matches отдаёт у каждой партии rating_delta — изменение
общего рейтинга владельца истории за неё (один знак после запятой), в обоих
режимах, включая «лучшую партию»; история и рейтинг считаются одним
проигрыванием. В списке партий группы поле пустое: там непонятно, чья это
дельта.
Фронт: компактная карточка истории — второй строкой «Рейтинг: +12.3»,
подробная (MatchListView) — последней строкой, только когда дельта есть.
Рост — зелёным, падение — красным, минус типографский (formatRatingDelta).
schema.d.ts перегенерирован.
Тест: дуэль новичков ±32 в обеих историях, реванш с обратными знаками,
сумма изменений сходится с рейтингом, в списке группы дельты нет, режим
«лучшая» её отдаёт. #77
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
This commit is contained in:
@@ -498,6 +498,9 @@ class MatchListItem(BaseModel):
|
||||
overall_comment: str | None = None
|
||||
created_by: int
|
||||
participants: list[MatchListParticipant] = []
|
||||
# Изменение общего рейтинга владельца истории за эту партию (один знак после запятой).
|
||||
# Заполняется только в истории игрока (GET /users/{id}/matches); в списке группы — None.
|
||||
rating_delta: float | None = None
|
||||
|
||||
|
||||
class MatchList(BaseModel):
|
||||
|
||||
@@ -355,8 +355,12 @@ def _participants_by_match(session: Session, match_ids: list[int]) -> dict[int,
|
||||
return out
|
||||
|
||||
|
||||
def _match_items(session: Session, matches) -> list[dict]:
|
||||
"""Элементы списка партий (общее для списка группы и истории игрока)."""
|
||||
def _match_items(
|
||||
session: Session, matches, rating_deltas: dict[int, float] | None = None
|
||||
) -> list[dict]:
|
||||
"""Элементы списка партий (общее для списка группы и истории игрока).
|
||||
|
||||
rating_deltas — {match_id: ΔR} владельца истории; у списка группы их нет."""
|
||||
by_match = _participants_by_match(session, [m.id for m in matches])
|
||||
|
||||
items = []
|
||||
@@ -375,6 +379,7 @@ def _match_items(session: Session, matches) -> list[dict]:
|
||||
"overall_comment": m.overall_comment,
|
||||
"created_by": m.created_by,
|
||||
"participants": parts,
|
||||
"rating_delta": None if rating_deltas is None else rating_deltas.get(m.id),
|
||||
}
|
||||
)
|
||||
return items
|
||||
@@ -408,17 +413,19 @@ def user_match_list(
|
||||
) -> dict:
|
||||
"""История партий игрока: только ЗАВЕРШЁННЫЕ, свежие сверху.
|
||||
|
||||
best_only — одна лучшая партия: наибольший прирост рейтинга в общей цепочке
|
||||
У каждой партии — изменение общего рейтинга игрока за неё (rating_delta, один знак
|
||||
после запятой). best_only — одна лучшая партия: наибольший прирост рейтинга
|
||||
(учитывает и соперников, и ход партии); при равенстве берём более свежую."""
|
||||
history = load_history(session)
|
||||
rep = replay(history)
|
||||
mine = _user_seats(history, user_id)
|
||||
# + 0.0 превращает −0.0 (мелкий минус, округлённый до нуля) в обычный ноль.
|
||||
deltas = {m.id: round(rep.delta[(m.id, user_id)], 1) + 0.0 for m, _s in mine}
|
||||
if best_only:
|
||||
history = load_history(session)
|
||||
rep = replay(history)
|
||||
candidates = [
|
||||
(rep.delta[(m.id, user_id)], m.played_at, m.id) for m, _s in _user_seats(history, user_id)
|
||||
]
|
||||
candidates = [(rep.delta[(m.id, user_id)], m.played_at, m.id) for m, _s in mine]
|
||||
matches = [session.get(Match, max(candidates)[2])] if candidates else []
|
||||
return {
|
||||
"items": _match_items(session, matches),
|
||||
"items": _match_items(session, matches, deltas),
|
||||
"total": len(matches),
|
||||
"limit": 1,
|
||||
"offset": 0,
|
||||
@@ -440,7 +447,7 @@ def user_match_list(
|
||||
.limit(limit)
|
||||
).all()
|
||||
return {
|
||||
"items": _match_items(session, matches),
|
||||
"items": _match_items(session, matches, deltas),
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
|
||||
@@ -138,3 +138,30 @@ def test_best_match_is_biggest_rating_gain(client: TestClient, engine):
|
||||
data = client.get(f"/api/users/{me['id']}/matches").json()
|
||||
assert data["total"] == 1
|
||||
assert data["items"][0]["id"] == first["id"]
|
||||
|
||||
|
||||
def test_history_shows_rating_delta_of_its_owner(client: TestClient, engine):
|
||||
"""История игрока несёт изменение его общего рейтинга за каждую партию (#77)."""
|
||||
me = login(client, "Историк")
|
||||
gid, fids = _group(client)
|
||||
b = add_group_member(engine, gid, "Оппонент")
|
||||
_duel(client, gid, fids, me["id"], b) # новички на 1500: ±32
|
||||
_duel(client, gid, fids, b, me["id"]) # реванш: 1468 обыгрывает 1532
|
||||
|
||||
mine = client.get(f"/api/users/{me['id']}/matches").json()["items"]
|
||||
theirs = client.get(f"/api/users/{b}/matches").json()["items"]
|
||||
# Свежие сверху: реванш первым.
|
||||
assert (mine[1]["rating_delta"], theirs[1]["rating_delta"]) == (32.0, -32.0)
|
||||
assert mine[0]["rating_delta"] == -theirs[0]["rating_delta"] < 0
|
||||
# Изменения складываются в рейтинг (с точностью округления до десятых).
|
||||
score = _board(client)[me["id"]]["score"]
|
||||
assert abs(1500 + sum(i["rating_delta"] for i in mine) - score) <= 0.6
|
||||
|
||||
# В списке партий группы дельты нет — непонятно, чья она была бы.
|
||||
group_items = client.get(f"/api/groups/{gid}/matches").json()["items"]
|
||||
assert [i["rating_delta"] for i in group_items] == [None, None]
|
||||
|
||||
# Режим «лучшая партия» тоже её отдаёт.
|
||||
client.patch("/api/users/me/profile", json={"history_mode": "best"}, headers=csrf_headers(client))
|
||||
best = client.get(f"/api/users/{me['id']}/matches").json()["items"]
|
||||
assert best[0]["rating_delta"] == 32.0
|
||||
|
||||
Reference in New Issue
Block a user