From e7323cc3c4bf8d30c6e0fc99b1fe30b6f8b9d1f9 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Sat, 18 Jul 2026 17:16:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0:=20=D0=BB=D1=8E=D0=B1=D0=B8=D0=BC=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=84=D1=80=D0=B0=D0=BA=D1=86=D0=B8=D1=8F=20=E2=80=94?= =?UTF-8?q?=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=BD=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D1=83=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D1=83=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=BE=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Партии, где фракция выпала рандомом (was_random), в счётчик любимой не идут: рандомная раздача о предпочтениях не говорит. Если игрок ни разу не выбирал фракцию сам — любимой нет вовсе. Лучшая/худшая фракции и остальная статистика считаются по всем партиям, как раньше. Co-Authored-By: Claude Fable 5 --- backend/app/services/stats_service.py | 14 +++++-- backend/tests/conftest.py | 11 ++++- backend/tests/test_profile.py | 59 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/backend/app/services/stats_service.py b/backend/app/services/stats_service.py index 66b43d5..687dcfc 100644 --- a/backend/app/services/stats_service.py +++ b/backend/app/services/stats_service.py @@ -34,6 +34,7 @@ scored AS ( m.group_id AS group_id, m.played_at AS played_at, mp.place AS place, + mp.was_random AS was_random, m.player_count AS player_count, ({MATCH_POINTS_SQL}) AS points, CASE WHEN mp.place = 1 THEN 1 ELSE 0 END AS is_win @@ -140,8 +141,9 @@ def _faction_breakdown(session: Session, user_id: int, group_id: int | None) -> SUM(s.is_win) AS wins, AVG(CAST(s.is_win AS FLOAT)) AS win_rate, AVG(s.place) AS avg_place, - AVG(s.points) * 100 AS score -- фракции: чистое среднее (служебная метрика - -- «лучшая/худшая», сглаживание задавило бы её к 50) + AVG(s.points) * 100 AS score, -- фракции: чистое среднее (служебная метрика + -- «лучшая/худшая», сглаживание задавило бы её к 50) + SUM(CASE WHEN s.was_random THEN 0 ELSE 1 END) AS chosen_games FROM scored s JOIN factions f ON f.id = s.faction_id JOIN expansions e ON e.id = f.expansion_id @@ -166,6 +168,9 @@ def _faction_breakdown(session: Session, user_id: int, group_id: int | None) -> "win_rate": _round(r["win_rate"] or 0.0, 4), "avg_place": _round(r["avg_place"], 2), "score": _round(r["score"], 1), + # Служебное (в API-схему не входит): партии, где фракция выбрана + # вручную, — для «любимой фракции» рандомные раздачи не в счёт. + "chosen_games": int(r["chosen_games"] or 0), } ) return out @@ -197,7 +202,10 @@ def profile_stats(session: Session, user_id: int, group_id: int | None = None) - qualified = [f for f in factions if f["games"] >= FACTION_MIN_GAMES] best = max(qualified, key=lambda f: (f["score"] or 0)) if qualified else None worst = min(qualified, key=lambda f: (f["score"] or 0)) if qualified else None - most_played = max(factions, key=lambda f: f["games"]) if factions else None + # Любимая — по числу партий, где игрок выбрал фракцию САМ (was_random=false): + # рандомная раздача о предпочтениях не говорит. Нет ручных выборов — нет любимой. + chosen = [f for f in factions if f["chosen_games"] > 0] + most_played = max(chosen, key=lambda f: f["chosen_games"]) if chosen else None return { "user_id": user_id, "overall": overall, diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index dc9bbfa..27cc48f 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -140,8 +140,15 @@ def finish_match( def create_finished_match( client: TestClient, group_id: int, players: list[dict], win_reason: str = "objectives" ) -> dict: - """players: [{user_id, faction_id, place, comment?}] → завершённая партия (для статистики).""" - roster = [{"user_id": p["user_id"], "faction_id": p["faction_id"]} for p in players] + """players: [{user_id, faction_id, place, comment?, was_random?}] → завершённая партия.""" + roster = [ + { + "user_id": p["user_id"], + "faction_id": p["faction_id"], + "was_random": p.get("was_random", False), + } + for p in players + ] started = start_match(client, group_id, roster) assert started.status_code == 200, started.text results = [ diff --git a/backend/tests/test_profile.py b/backend/tests/test_profile.py index 72195ec..fb17dbb 100644 --- a/backend/tests/test_profile.py +++ b/backend/tests/test_profile.py @@ -124,3 +124,62 @@ def test_leaderboard_includes_avatar_url(client: TestClient, engine, monkeypatch headers=csrf_headers(client), ) assert "/avatar?v=" in entry_for(me["id"])["avatar_url"] # после загрузки — ссылка + + +def test_most_played_faction_ignores_random(client: TestClient, engine): + """Любимая фракция — только по сознательному выбору: рандомные раздачи не в счёт.""" + me = login(client, "Фракционер") + exps = [e["id"] for e in client.get("/api/expansions").json()] + gid = client.post( + "/api/groups", json={"name": "Группа", "expansion_ids": exps}, headers=csrf_headers(client) + ).json()["id"] + p2 = add_group_member(engine, gid, "Партнёр") + fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()] + + # Дважды играл фракцией A по рандому, один раз фракцией B — по своему выбору. + for _ in range(2): + create_finished_match( + client, + gid, + [ + {"user_id": me["id"], "faction_id": fids[0], "place": 1, "was_random": True}, + {"user_id": p2, "faction_id": fids[1], "place": 2}, + ], + ) + create_finished_match( + client, + gid, + [ + {"user_id": me["id"], "faction_id": fids[2], "place": 1}, + {"user_id": p2, "faction_id": fids[1], "place": 2}, + ], + ) + + stats = client.get("/api/users/me/stats").json() + assert stats["most_played_faction"]["faction_id"] == fids[2] # B, хоть партий с A больше + + # У соперника все фракции выбраны вручную — любимая считается как раньше. + login(client, "Партнёр") + stats2 = client.get("/api/users/me/stats").json() + assert stats2["most_played_faction"]["faction_id"] == fids[1] + + +def test_most_played_faction_absent_when_all_random(client: TestClient, engine): + """Если игрок ни разу не выбирал фракцию сам — любимой нет вовсе.""" + me = login(client, "Рандомщик") + exps = [e["id"] for e in client.get("/api/expansions").json()] + gid = client.post( + "/api/groups", json={"name": "Группа", "expansion_ids": exps}, headers=csrf_headers(client) + ).json()["id"] + p2 = add_group_member(engine, gid, "Оппонент") + fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()] + create_finished_match( + client, + gid, + [ + {"user_id": me["id"], "faction_id": fids[0], "place": 1, "was_random": True}, + {"user_id": p2, "faction_id": fids[1], "place": 2, "was_random": True}, + ], + ) + stats = client.get("/api/users/me/stats").json() + assert stats["most_played_faction"] is None