"""Monthly MVP V1 scoring helpers.""" from __future__ import annotations import math from typing import Mapping MONTHLY_MVP_VERSION = "v1" MONTHLY_MVP_MIN_MATCHES = 6 MONTHLY_MVP_MIN_TIME_SECONDS = 21600 MONTHLY_MVP_FULL_PARTICIPATION_SECONDS = 28800 MONTHLY_MVP_TEAMKILL_PENALTY_CAP = 6.0 MONTHLY_MVP_TEAMKILL_PENALTY_PER_KILL = 0.5 def build_monthly_mvp_rankings( aggregated_rows: list[Mapping[str, object]], *, limit: int, ) -> dict[str, object]: """Transform aggregated monthly totals into ranked MVP V1 items.""" eligible_rows = [ _build_eligible_player_summary(row) for row in aggregated_rows if _is_eligible_player_row(row) ] if not eligible_rows: return { "ranking_version": MONTHLY_MVP_VERSION, "eligibility": _build_eligibility_metadata(), "items": [], "eligible_players_count": 0, } max_total_kills = max(item["totals"]["kills"] for item in eligible_rows) max_total_support = max(item["totals"]["support"] for item in eligible_rows) max_kpm = max(item["derived"]["kpm"] for item in eligible_rows) max_kda = max(item["derived"]["kda"] for item in eligible_rows) for item in eligible_rows: component_scores = { "kills_score": _log_normalized_score(item["totals"]["kills"], max_total_kills), "support_score": _log_normalized_score(item["totals"]["support"], max_total_support), "kpm_score": _log_normalized_score(item["derived"]["kpm"], max_kpm), "kda_score": _log_normalized_score(item["derived"]["kda"], max_kda), "participation_score": round( 100 * min( 1.0, item["totals"]["time_seconds"] / MONTHLY_MVP_FULL_PARTICIPATION_SECONDS, ), 3, ), } teamkill_penalty = round( min( MONTHLY_MVP_TEAMKILL_PENALTY_CAP, item["totals"]["teamkills"] * MONTHLY_MVP_TEAMKILL_PENALTY_PER_KILL, ), 3, ) item["component_scores"] = component_scores item["teamkill_penalty"] = teamkill_penalty item["mvp_score"] = round( (0.35 * component_scores["kills_score"]) + (0.20 * component_scores["support_score"]) + (0.20 * component_scores["kpm_score"]) + (0.15 * component_scores["kda_score"]) + (0.10 * component_scores["participation_score"]) - teamkill_penalty, 3, ) ranked_items = sorted( eligible_rows, key=lambda item: ( -item["mvp_score"], -item["component_scores"]["participation_score"], -item["component_scores"]["kills_score"], -item["component_scores"]["support_score"], item["totals"]["teamkills"], str(item["player"]["name"]).casefold(), str(item["player"]["stable_player_key"]), ), ) for position, item in enumerate(ranked_items[:limit], start=1): item["ranking_position"] = position return { "ranking_version": MONTHLY_MVP_VERSION, "eligibility": _build_eligibility_metadata(), "eligible_players_count": len(eligible_rows), "items": ranked_items[:limit], } def _is_eligible_player_row(row: Mapping[str, object]) -> bool: matches_count = int(row.get("matches_count") or 0) time_seconds = int(row.get("total_time_seconds") or 0) has_required_fields = all( row.get(field_name) is not None for field_name in ("total_kills", "total_deaths", "total_support", "total_time_seconds") ) return ( has_required_fields and matches_count >= MONTHLY_MVP_MIN_MATCHES and time_seconds >= MONTHLY_MVP_MIN_TIME_SECONDS ) def _build_eligible_player_summary(row: Mapping[str, object]) -> dict[str, object]: total_kills = int(row.get("total_kills") or 0) total_deaths = int(row.get("total_deaths") or 0) total_support = int(row.get("total_support") or 0) total_teamkills = int(row.get("total_teamkills") or 0) total_time_seconds = int(row.get("total_time_seconds") or 0) total_time_minutes = max(total_time_seconds / 60.0, 1.0) kpm = round(total_kills / total_time_minutes, 6) kda = round(total_kills / max(total_deaths, 1), 6) return { "server": { "slug": row.get("server_slug"), "name": row.get("server_name"), }, "player": { "stable_player_key": row.get("stable_player_key"), "name": row.get("player_name"), "steam_id": row.get("steam_id"), }, "matches_considered": int(row.get("matches_count") or 0), "totals": { "kills": total_kills, "deaths": total_deaths, "support": total_support, "teamkills": total_teamkills, "time_seconds": total_time_seconds, "time_minutes": round(total_time_seconds / 60.0, 2), }, "derived": { "kpm": kpm, "kda": kda, }, } def _log_normalized_score(value: float | int, max_value: float | int) -> float: if value <= 0 or max_value <= 0: return 0.0 return round((100 * math.log1p(value)) / math.log1p(max_value), 3) def _build_eligibility_metadata() -> dict[str, object]: return { "minimum_matches": MONTHLY_MVP_MIN_MATCHES, "minimum_time_seconds": MONTHLY_MVP_MIN_TIME_SECONDS, "minimum_time_hours": round(MONTHLY_MVP_MIN_TIME_SECONDS / 3600, 1), "full_participation_seconds": MONTHLY_MVP_FULL_PARTICIPATION_SECONDS, "full_participation_hours": round(MONTHLY_MVP_FULL_PARTICIPATION_SECONDS / 3600, 1), "teamkill_penalty_per_kill": MONTHLY_MVP_TEAMKILL_PENALTY_PER_KILL, "teamkill_penalty_cap": MONTHLY_MVP_TEAMKILL_PENALTY_CAP, }