Статистика: любимая фракция — только по сознательному выбору игрока

Партии, где фракция выпала рандомом (was_random), в счётчик любимой
не идут: рандомная раздача о предпочтениях не говорит. Если игрок ни
разу не выбирал фракцию сам — любимой нет вовсе. Лучшая/худшая фракции
и остальная статистика считаются по всем партиям, как раньше.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 17:16:28 +03:00
co-authored by Claude Fable 5
parent b349891bdb
commit e7323cc3c4
3 changed files with 79 additions and 5 deletions
+10 -2
View File
@@ -34,6 +34,7 @@ scored AS (
m.group_id AS group_id, m.group_id AS group_id,
m.played_at AS played_at, m.played_at AS played_at,
mp.place AS place, mp.place AS place,
mp.was_random AS was_random,
m.player_count AS player_count, m.player_count AS player_count,
({MATCH_POINTS_SQL}) AS points, ({MATCH_POINTS_SQL}) AS points,
CASE WHEN mp.place = 1 THEN 1 ELSE 0 END AS is_win 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, SUM(s.is_win) AS wins,
AVG(CAST(s.is_win AS FLOAT)) AS win_rate, AVG(CAST(s.is_win AS FLOAT)) AS win_rate,
AVG(s.place) AS avg_place, AVG(s.place) AS avg_place,
AVG(s.points) * 100 AS score -- фракции: чистое среднее (служебная метрика AVG(s.points) * 100 AS score, -- фракции: чистое среднее (служебная метрика
-- «лучшая/худшая», сглаживание задавило бы её к 50) -- «лучшая/худшая», сглаживание задавило бы её к 50)
SUM(CASE WHEN s.was_random THEN 0 ELSE 1 END) AS chosen_games
FROM scored s FROM scored s
JOIN factions f ON f.id = s.faction_id JOIN factions f ON f.id = s.faction_id
JOIN expansions e ON e.id = f.expansion_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), "win_rate": _round(r["win_rate"] or 0.0, 4),
"avg_place": _round(r["avg_place"], 2), "avg_place": _round(r["avg_place"], 2),
"score": _round(r["score"], 1), "score": _round(r["score"], 1),
# Служебное (в API-схему не входит): партии, где фракция выбрана
# вручную, — для «любимой фракции» рандомные раздачи не в счёт.
"chosen_games": int(r["chosen_games"] or 0),
} }
) )
return out 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] 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 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 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 { return {
"user_id": user_id, "user_id": user_id,
"overall": overall, "overall": overall,
+9 -2
View File
@@ -140,8 +140,15 @@ def finish_match(
def create_finished_match( def create_finished_match(
client: TestClient, group_id: int, players: list[dict], win_reason: str = "objectives" client: TestClient, group_id: int, players: list[dict], win_reason: str = "objectives"
) -> dict: ) -> dict:
"""players: [{user_id, faction_id, place, comment?}] → завершённая партия (для статистики).""" """players: [{user_id, faction_id, place, comment?, was_random?}] → завершённая партия."""
roster = [{"user_id": p["user_id"], "faction_id": p["faction_id"]} for p in players] 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) started = start_match(client, group_id, roster)
assert started.status_code == 200, started.text assert started.status_code == 200, started.text
results = [ results = [
+59
View File
@@ -124,3 +124,62 @@ def test_leaderboard_includes_avatar_url(client: TestClient, engine, monkeypatch
headers=csrf_headers(client), headers=csrf_headers(client),
) )
assert "/avatar?v=" in entry_for(me["id"])["avatar_url"] # после загрузки — ссылка 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