Implement monthly MVP V2 workflow and snapshots

This commit is contained in:
devRaGonSa
2026-03-24 16:40:19 +01:00
parent c84ebad5af
commit af140d6f3e
13 changed files with 1620 additions and 1 deletions

View File

@@ -142,6 +142,7 @@ def generate_historical_snapshots(
"full_snapshot_every_runs": full_snapshot_every_runs,
"prewarm_only": not should_run_full_refresh,
"refresh_interval_seconds": get_historical_refresh_interval_seconds(),
"includes_monthly_mvp_v2": True,
}

View File

@@ -255,7 +255,12 @@ def _is_effectively_empty_snapshot_payload(
items = payload.get("items")
return not isinstance(items, list) or len(items) == 0
if snapshot_type in {"weekly-leaderboard", "monthly-leaderboard", "monthly-mvp"}:
if snapshot_type in {
"weekly-leaderboard",
"monthly-leaderboard",
"monthly-mvp",
"monthly-mvp-v2",
}:
items = payload.get("items")
return not isinstance(items, list) or len(items) == 0
@@ -287,6 +292,18 @@ def _build_snapshot_filename(*, snapshot_type: str, metric: str | None) -> str:
return "server-summary.json"
if snapshot_type == "recent-matches":
return "recent-matches.json"
if snapshot_type == "monthly-mvp-v2":
return "monthly-mvp-v2.json"
if snapshot_type == "player-event-most-killed":
return "player-events-most-killed.json"
if snapshot_type == "player-event-death-by":
return "player-events-death-by.json"
if snapshot_type == "player-event-duels":
return "player-events-duels.json"
if snapshot_type == "player-event-weapon-kills":
return "player-events-weapon-kills.json"
if snapshot_type == "player-event-teamkills":
return "player-events-teamkills.json"
if snapshot_type == "weekly-leaderboard":
metric_suffix = "matches-over-100-kills" if metric == "matches_over_100_kills" else _slugify(metric or "unknown")
return f"weekly-{metric_suffix}.json"

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
@@ -11,15 +12,30 @@ from .historical_storage import (
list_historical_servers,
list_monthly_leaderboard,
list_monthly_mvp_ranking,
list_monthly_mvp_v2_ranking,
list_recent_historical_matches,
list_weekly_leaderboard,
)
from .player_event_aggregates import (
list_death_by,
list_most_killed,
list_net_duel_summaries,
list_teamkill_summaries,
list_weapon_kills,
)
from .player_event_storage import initialize_player_event_storage
SNAPSHOT_TYPE_SERVER_SUMMARY = "server-summary"
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD = "weekly-leaderboard"
SNAPSHOT_TYPE_MONTHLY_LEADERBOARD = "monthly-leaderboard"
SNAPSHOT_TYPE_MONTHLY_MVP = "monthly-mvp"
SNAPSHOT_TYPE_MONTHLY_MVP_V2 = "monthly-mvp-v2"
SNAPSHOT_TYPE_RECENT_MATCHES = "recent-matches"
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED = "player-event-most-killed"
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY = "player-event-death-by"
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS = "player-event-duels"
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS = "player-event-weapon-kills"
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS = "player-event-teamkills"
SUPPORTED_SNAPSHOT_TYPES = frozenset(
{
@@ -27,7 +43,13 @@ SUPPORTED_SNAPSHOT_TYPES = frozenset(
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
SNAPSHOT_TYPE_MONTHLY_LEADERBOARD,
SNAPSHOT_TYPE_MONTHLY_MVP,
SNAPSHOT_TYPE_MONTHLY_MVP_V2,
SNAPSHOT_TYPE_RECENT_MATCHES,
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED,
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY,
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS,
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS,
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS,
}
)
@@ -52,6 +74,13 @@ SNAPSHOT_LEADERBOARD_METRICS = (
"matches_over_100_kills",
"support",
)
PLAYER_EVENT_SNAPSHOT_TYPES = (
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED,
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY,
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS,
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS,
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS,
)
DEFAULT_SNAPSHOT_WINDOW = "all-time"
DEFAULT_WEEKLY_SNAPSHOT_WINDOW = "7d"
@@ -134,6 +163,24 @@ def build_historical_server_snapshots(
db_path=db_path,
)
)
snapshots.append(
_build_monthly_mvp_v2_snapshot(
server_key,
generated_at_value,
limit=leaderboard_limit,
db_path=db_path,
)
)
for snapshot_type in PLAYER_EVENT_SNAPSHOT_TYPES:
snapshots.append(
_build_player_event_snapshot(
server_key,
snapshot_type,
generated_at_value,
limit=leaderboard_limit,
db_path=db_path,
)
)
snapshots.append(
_build_recent_matches_snapshot(
@@ -188,6 +235,24 @@ def build_priority_historical_snapshots(
db_path=db_path,
)
)
snapshots.append(
_build_monthly_mvp_v2_snapshot(
server_key,
generated_at_value,
limit=leaderboard_limit,
db_path=db_path,
)
)
for snapshot_type in PLAYER_EVENT_SNAPSHOT_TYPES:
snapshots.append(
_build_player_event_snapshot(
server_key,
snapshot_type,
generated_at_value,
limit=leaderboard_limit,
db_path=db_path,
)
)
snapshots.append(
_build_recent_matches_snapshot(
server_key,
@@ -420,6 +485,58 @@ def _build_recent_matches_snapshot(
}
def _build_player_event_snapshot(
server_key: str,
snapshot_type: str,
generated_at: datetime,
*,
limit: int,
db_path: Path | None = None,
) -> dict[str, object]:
month_key = _get_latest_player_event_month_key(server_key=server_key, db_path=db_path)
source_range_start = None
source_range_end = None
items: list[dict[str, object]] = []
found = False
if month_key:
source_range_start, source_range_end = _get_player_event_source_range(
server_key=server_key,
month_key=month_key,
db_path=db_path,
)
items = _list_player_event_snapshot_items(
snapshot_type=snapshot_type,
server_key=server_key,
month_key=month_key,
limit=limit,
db_path=db_path,
)
found = bool(items or source_range_start or source_range_end)
return {
"server_key": server_key,
"snapshot_type": snapshot_type,
"metric": None,
"window": DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
"generated_at": generated_at,
"source_range_start": source_range_start,
"source_range_end": source_range_end,
"is_stale": False,
"payload": {
"server_key": server_key,
"period": "monthly",
"month_key": month_key,
"limit": limit,
"found": found,
"generated_at": _to_iso(generated_at),
"source_range_start": _to_iso(source_range_start) if source_range_start else None,
"source_range_end": _to_iso(source_range_end) if source_range_end else None,
"items": items,
},
}
def _build_monthly_mvp_snapshot(
server_key: str,
generated_at: datetime,
@@ -452,6 +569,45 @@ def _build_monthly_mvp_snapshot(
}
def _build_monthly_mvp_v2_snapshot(
server_key: str,
generated_at: datetime,
*,
limit: int,
db_path: Path | None = None,
) -> dict[str, object]:
ranking_result = list_monthly_mvp_v2_ranking(
limit=limit,
server_id=server_key,
db_path=db_path,
)
month_key = str(ranking_result.get("window_start") or "")[:7] or None
event_coverage = ranking_result.get("event_coverage")
source_range_start = None
source_range_end = None
if isinstance(event_coverage, dict):
source_range_start = _parse_optional_timestamp(event_coverage.get("source_range_start"))
source_range_end = _parse_optional_timestamp(event_coverage.get("source_range_end"))
return {
"server_key": server_key,
"snapshot_type": SNAPSHOT_TYPE_MONTHLY_MVP_V2,
"metric": None,
"window": DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
"generated_at": generated_at,
"source_range_start": source_range_start,
"source_range_end": source_range_end,
"is_stale": False,
"payload": {
"server_key": server_key,
"limit": limit,
"month_key": month_key,
"found": bool(event_coverage.get("ready")) if isinstance(event_coverage, dict) else False,
"generated_at": _to_iso(generated_at),
**ranking_result,
},
}
def _resolve_snapshot_target_keys(
*,
server_key: str | None,
@@ -470,6 +626,93 @@ def _resolve_snapshot_target_keys(
return [normalized_server_key, ALL_SERVERS_SLUG]
def _list_player_event_snapshot_items(
*,
snapshot_type: str,
server_key: str,
month_key: str,
limit: int,
db_path: Path | None,
) -> list[dict[str, object]]:
aggregator_by_snapshot_type = {
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED: list_most_killed,
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY: list_death_by,
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS: list_net_duel_summaries,
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS: list_weapon_kills,
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS: list_teamkill_summaries,
}
aggregator = aggregator_by_snapshot_type[snapshot_type]
return aggregator(
server_slug=server_key,
month=month_key,
limit=limit,
db_path=db_path,
)
def _get_latest_player_event_month_key(
*,
server_key: str,
db_path: Path | None = None,
) -> str | None:
resolved_path = initialize_player_event_storage(db_path=db_path)
where_sql, params = _build_player_event_scope_where(server_key=server_key)
with _connect(resolved_path) as connection:
row = connection.execute(
f"""
SELECT MAX(substr(occurred_at, 1, 7)) AS latest_month
FROM player_event_raw_ledger
WHERE occurred_at IS NOT NULL
AND {where_sql}
""",
params,
).fetchone()
if not row or not row["latest_month"]:
return None
return str(row["latest_month"])
def _get_player_event_source_range(
*,
server_key: str,
month_key: str,
db_path: Path | None = None,
) -> tuple[datetime | None, datetime | None]:
resolved_path = initialize_player_event_storage(db_path=db_path)
where_sql, params = _build_player_event_scope_where(server_key=server_key)
with _connect(resolved_path) as connection:
row = connection.execute(
f"""
SELECT
MIN(occurred_at) AS source_range_start,
MAX(occurred_at) AS source_range_end
FROM player_event_raw_ledger
WHERE occurred_at IS NOT NULL
AND substr(occurred_at, 1, 7) = ?
AND {where_sql}
""",
[month_key, *params],
).fetchone()
if not row:
return None, None
return (
_parse_optional_timestamp(row["source_range_start"]),
_parse_optional_timestamp(row["source_range_end"]),
)
def _build_player_event_scope_where(*, server_key: str) -> tuple[str, list[object]]:
if server_key == ALL_SERVERS_SLUG:
return "1 = 1", []
return "server_slug = ?", [server_key]
def _connect(db_path: Path) -> sqlite3.Connection:
connection = sqlite3.connect(db_path)
connection.row_factory = sqlite3.Row
return connection
def _parse_optional_timestamp(value: object) -> datetime | None:
if not isinstance(value, str) or not value.strip():
return None

View File

@@ -14,6 +14,7 @@ from .config import (
)
from .historical_models import HistoricalServerDefinition
from .monthly_mvp import build_monthly_mvp_rankings
from .monthly_mvp_v2 import build_monthly_mvp_v2_rankings
DEFAULT_HISTORICAL_SERVERS = (
@@ -1569,6 +1570,331 @@ def list_monthly_mvp_ranking(
}
def list_monthly_mvp_v2_ranking(
*,
limit: int = 10,
server_id: str | None = None,
db_path: Path | None = None,
) -> dict[str, object]:
"""Return the monthly MVP V2 ranking built from monthly totals plus V2 signals."""
resolved_path = initialize_historical_storage(db_path=db_path)
aggregate_all_servers = _is_all_servers_selector(server_id)
current_time = datetime.now(timezone.utc)
current_month_start = _start_of_month(current_time)
previous_month_start = _start_of_previous_month(current_month_start)
monthly_window = _select_monthly_window(
server_id=server_id,
current_time=current_time,
current_month_start=current_month_start,
previous_month_start=previous_month_start,
db_path=resolved_path,
)
window_start = monthly_window["window_start"]
window_end = monthly_window["window_end"]
month_key = window_start.strftime("%Y-%m")
event_coverage = _get_monthly_player_event_coverage(
server_id=server_id,
month_key=month_key,
db_path=resolved_path,
)
window_days = _calculate_window_days(window_start=window_start, window_end=window_end)
empty_result = {
"timeframe": "monthly",
"metric": "mvp-v2",
"ranking_version": "v2",
"window_start": window_start.isoformat().replace("+00:00", "Z"),
"window_end": window_end.isoformat().replace("+00:00", "Z"),
"window_days": window_days,
"window_kind": monthly_window["window_kind"],
"window_label": monthly_window["window_label"],
"uses_fallback": monthly_window["uses_fallback"],
"selection_reason": monthly_window["selection_reason"],
"current_month_start": current_month_start.isoformat().replace("+00:00", "Z"),
"current_month_closed_matches": monthly_window["current_month_closed_matches"],
"previous_month_closed_matches": monthly_window["previous_month_closed_matches"],
"sufficient_sample": {
"minimum_closed_matches": monthly_window["minimum_closed_matches"],
"current_month_closed_matches": monthly_window["current_month_closed_matches"],
"current_month_has_sufficient_sample": monthly_window["current_month_has_sufficient_sample"],
"is_early_month": monthly_window["is_early_month"],
},
"event_coverage": event_coverage,
}
if not bool(event_coverage["ready"]):
return {
**empty_result,
"eligibility": None,
"eligible_players_count": 0,
"items": [],
}
where_clauses = [
"historical_matches.ended_at IS NOT NULL",
"historical_matches.ended_at >= ?",
"historical_matches.ended_at < ?",
]
params: list[object] = [
window_start.isoformat().replace("+00:00", "Z"),
window_end.isoformat().replace("+00:00", "Z"),
]
if server_id and not aggregate_all_servers:
normalized_server_id = server_id.strip()
where_clauses.append(
"(historical_servers.slug = ? OR CAST(historical_servers.server_number AS TEXT) = ?)"
)
params.extend([normalized_server_id, normalized_server_id])
event_scope_sql, event_scope_params = _build_player_event_scope_sql(server_id)
server_slug_expression = (
f"'{ALL_SERVERS_SLUG}'"
if aggregate_all_servers
else "historical_servers.slug"
)
server_name_expression = (
f"'{ALL_SERVERS_DISPLAY_NAME}'"
if aggregate_all_servers
else "historical_servers.display_name"
)
group_by_expression = (
"historical_players.id"
if aggregate_all_servers
else "historical_servers.slug, historical_players.id"
)
with _connect(resolved_path) as connection:
rows = connection.execute(
f"""
WITH most_killed_pairs AS (
SELECT
killer_player_key AS stable_player_key,
victim_player_key,
COALESCE(SUM(event_value), 0) AS total_kills
FROM player_event_raw_ledger
WHERE event_type = 'player_kill_summary'
AND occurred_at IS NOT NULL
AND substr(occurred_at, 1, 7) = ?
AND {event_scope_sql}
AND killer_player_key IS NOT NULL
AND victim_player_key IS NOT NULL
GROUP BY killer_player_key, victim_player_key
),
most_killed_by_player AS (
SELECT
stable_player_key,
MAX(total_kills) AS most_killed_count
FROM most_killed_pairs
GROUP BY stable_player_key
),
death_by_pairs AS (
SELECT
victim_player_key AS stable_player_key,
killer_player_key,
COALESCE(SUM(event_value), 0) AS total_kills
FROM player_event_raw_ledger
WHERE event_type = 'player_death_summary'
AND occurred_at IS NOT NULL
AND substr(occurred_at, 1, 7) = ?
AND {event_scope_sql}
AND killer_player_key IS NOT NULL
AND victim_player_key IS NOT NULL
GROUP BY victim_player_key, killer_player_key
),
death_by_player AS (
SELECT
stable_player_key,
MAX(total_kills) AS death_by_count
FROM death_by_pairs
GROUP BY stable_player_key
),
duel_pairs AS (
SELECT
CASE
WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '')
THEN killer_player_key
ELSE victim_player_key
END AS player_a_key,
CASE
WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '')
THEN victim_player_key
ELSE killer_player_key
END AS player_b_key,
COALESCE(
SUM(
CASE
WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '')
THEN event_value
ELSE -event_value
END
),
0
) AS net_duel_value
FROM player_event_raw_ledger
WHERE event_type = 'player_kill_summary'
AND occurred_at IS NOT NULL
AND substr(occurred_at, 1, 7) = ?
AND {event_scope_sql}
AND killer_player_key IS NOT NULL
AND victim_player_key IS NOT NULL
GROUP BY
CASE
WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '')
THEN killer_player_key
ELSE victim_player_key
END,
CASE
WHEN COALESCE(killer_player_key, '') <= COALESCE(victim_player_key, '')
THEN victim_player_key
ELSE killer_player_key
END
),
duel_player_values AS (
SELECT
player_a_key AS stable_player_key,
CASE WHEN net_duel_value > 0 THEN net_duel_value ELSE 0 END AS positive_duel_value
FROM duel_pairs
UNION ALL
SELECT
player_b_key AS stable_player_key,
CASE WHEN net_duel_value < 0 THEN -net_duel_value ELSE 0 END AS positive_duel_value
FROM duel_pairs
),
ranked_duel_values AS (
SELECT
stable_player_key,
positive_duel_value,
ROW_NUMBER() OVER (
PARTITION BY stable_player_key
ORDER BY positive_duel_value DESC
) AS duel_rank
FROM duel_player_values
WHERE stable_player_key IS NOT NULL
AND positive_duel_value > 0
),
duel_control_by_player AS (
SELECT
stable_player_key,
COALESCE(SUM(positive_duel_value), 0) AS duel_control_raw
FROM ranked_duel_values
WHERE duel_rank <= 3
GROUP BY stable_player_key
)
SELECT
{server_slug_expression} AS server_slug,
{server_name_expression} AS server_name,
historical_players.stable_player_key,
historical_players.display_name AS player_name,
historical_players.steam_id,
COUNT(DISTINCT historical_matches.id) AS matches_count,
COALESCE(SUM(historical_player_match_stats.kills), 0) AS total_kills,
COALESCE(SUM(historical_player_match_stats.deaths), 0) AS total_deaths,
COALESCE(SUM(historical_player_match_stats.support), 0) AS total_support,
COALESCE(SUM(historical_player_match_stats.teamkills), 0) AS total_teamkills,
COALESCE(SUM(historical_player_match_stats.time_seconds), 0) AS total_time_seconds,
COALESCE(MAX(most_killed_by_player.most_killed_count), 0) AS most_killed_count,
COALESCE(MAX(death_by_player.death_by_count), 0) AS death_by_count,
COALESCE(MAX(duel_control_by_player.duel_control_raw), 0) AS duel_control_raw
FROM historical_player_match_stats
INNER JOIN historical_matches
ON historical_matches.id = historical_player_match_stats.historical_match_id
INNER JOIN historical_servers
ON historical_servers.id = historical_matches.historical_server_id
INNER JOIN historical_players
ON historical_players.id = historical_player_match_stats.historical_player_id
LEFT JOIN most_killed_by_player
ON most_killed_by_player.stable_player_key = historical_players.stable_player_key
LEFT JOIN death_by_player
ON death_by_player.stable_player_key = historical_players.stable_player_key
LEFT JOIN duel_control_by_player
ON duel_control_by_player.stable_player_key = historical_players.stable_player_key
WHERE {" AND ".join(where_clauses)}
GROUP BY {group_by_expression}
""",
[
month_key,
*event_scope_params,
month_key,
*event_scope_params,
month_key,
*event_scope_params,
*params,
],
).fetchall()
ranking_result = build_monthly_mvp_v2_rankings(
[dict(row) for row in rows],
limit=limit,
)
for item in ranking_result["items"]:
item["time_range"] = {
"start": window_start.isoformat().replace("+00:00", "Z"),
"end": window_end.isoformat().replace("+00:00", "Z"),
"window_days": window_days,
}
return {
**empty_result,
"ranking_version": ranking_result["ranking_version"],
"eligibility": ranking_result["eligibility"],
"eligible_players_count": ranking_result["eligible_players_count"],
"items": ranking_result["items"],
}
def _get_monthly_player_event_coverage(
*,
server_id: str | None,
month_key: str,
db_path: Path,
) -> dict[str, object]:
scope_sql, scope_params = _build_player_event_scope_sql(server_id)
with _connect(db_path) as connection:
latest_row = connection.execute(
f"""
SELECT MAX(substr(occurred_at, 1, 7)) AS latest_month_key
FROM player_event_raw_ledger
WHERE occurred_at IS NOT NULL
AND {scope_sql}
""",
scope_params,
).fetchone()
month_row = connection.execute(
f"""
SELECT
COUNT(*) AS event_count,
MIN(occurred_at) AS source_range_start,
MAX(occurred_at) AS source_range_end
FROM player_event_raw_ledger
WHERE occurred_at IS NOT NULL
AND substr(occurred_at, 1, 7) = ?
AND {scope_sql}
""",
[month_key, *scope_params],
).fetchone()
latest_month_key = str(latest_row["latest_month_key"]) if latest_row and latest_row["latest_month_key"] else None
event_count = int(month_row["event_count"] or 0) if month_row else 0
return {
"month_key": month_key,
"latest_month_key": latest_month_key,
"ready": bool(event_count > 0 and latest_month_key == month_key),
"event_count": event_count,
"source_range_start": month_row["source_range_start"] if month_row else None,
"source_range_end": month_row["source_range_end"] if month_row else None,
"selection_reason": (
"month-key-aligned"
if event_count > 0 and latest_month_key == month_key
else "player-event-month-mismatch-or-missing"
),
}
def _build_player_event_scope_sql(server_id: str | None) -> tuple[str, list[object]]:
if not server_id or _is_all_servers_selector(server_id):
return "1 = 1", []
normalized_server_id = server_id.strip()
return "server_slug = ?", [normalized_server_id]
def _connect(db_path: Path) -> sqlite3.Connection:
connection = sqlite3.connect(db_path, timeout=30.0)
connection.row_factory = sqlite3.Row

View File

@@ -0,0 +1,201 @@
"""Monthly MVP V2 scoring helpers."""
from __future__ import annotations
import math
from typing import Mapping
MONTHLY_MVP_V2_VERSION = "v2"
MONTHLY_MVP_V2_MIN_MATCHES = 6
MONTHLY_MVP_V2_MIN_TIME_SECONDS = 21600
MONTHLY_MVP_V2_FULL_PARTICIPATION_SECONDS = 28800
MONTHLY_MVP_V2_ADVANCED_CONFIDENCE_KILLS = 35
MONTHLY_MVP_V2_TEAMKILL_PENALTY_CAP = 8.0
MONTHLY_MVP_V2_TEAMKILL_PENALTY_PER_KILL = 0.75
def build_monthly_mvp_v2_rankings(
aggregated_rows: list[Mapping[str, object]],
*,
limit: int,
) -> dict[str, object]:
"""Transform aggregated monthly totals plus V2 event signals into rankings."""
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_V2_VERSION,
"eligibility": _build_eligibility_metadata(),
"eligible_players_count": 0,
"items": [],
}
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)
max_rivalry_edge = max(item["advanced"]["rivalry_edge_raw"] for item in eligible_rows)
max_duel_control = max(item["advanced"]["duel_control_raw"] 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_V2_FULL_PARTICIPATION_SECONDS,
),
3,
),
"rivalry_edge_score": _log_normalized_score(
item["advanced"]["rivalry_edge_raw"],
max_rivalry_edge,
),
"duel_control_score": _log_normalized_score(
item["advanced"]["duel_control_raw"],
max_duel_control,
),
}
advanced_confidence = round(
min(
1.0,
item["totals"]["kills"] / MONTHLY_MVP_V2_ADVANCED_CONFIDENCE_KILLS,
),
3,
)
teamkill_penalty_v2 = round(
min(
MONTHLY_MVP_V2_TEAMKILL_PENALTY_CAP,
item["totals"]["teamkills"] * MONTHLY_MVP_V2_TEAMKILL_PENALTY_PER_KILL,
),
3,
)
item["component_scores"] = component_scores
item["advanced_confidence"] = advanced_confidence
item["teamkill_penalty_v2"] = teamkill_penalty_v2
item["mvp_v2_score"] = round(
(0.30 * component_scores["kills_score"])
+ (0.18 * component_scores["support_score"])
+ (0.18 * component_scores["kpm_score"])
+ (0.12 * component_scores["kda_score"])
+ (0.10 * component_scores["participation_score"])
+ advanced_confidence
* (
(0.07 * component_scores["rivalry_edge_score"])
+ (0.05 * component_scores["duel_control_score"])
)
- teamkill_penalty_v2,
3,
)
ranked_items = sorted(
eligible_rows,
key=lambda item: (
-item["mvp_v2_score"],
-item["advanced_confidence"],
-item["component_scores"]["participation_score"],
-item["component_scores"]["kills_score"],
-item["component_scores"]["rivalry_edge_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_V2_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_V2_MIN_MATCHES
and time_seconds >= MONTHLY_MVP_V2_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)
most_killed_count = int(row.get("most_killed_count") or 0)
death_by_count = int(row.get("death_by_count") or 0)
duel_control_raw = int(row.get("duel_control_raw") or 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,
},
"advanced": {
"most_killed_count": most_killed_count,
"death_by_count": death_by_count,
"rivalry_edge_raw": max(0, most_killed_count - death_by_count),
"duel_control_raw": duel_control_raw,
},
}
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_V2_MIN_MATCHES,
"minimum_time_seconds": MONTHLY_MVP_V2_MIN_TIME_SECONDS,
"minimum_time_hours": round(MONTHLY_MVP_V2_MIN_TIME_SECONDS / 3600, 1),
"full_participation_seconds": MONTHLY_MVP_V2_FULL_PARTICIPATION_SECONDS,
"full_participation_hours": round(
MONTHLY_MVP_V2_FULL_PARTICIPATION_SECONDS / 3600,
1,
),
"advanced_confidence_kills": MONTHLY_MVP_V2_ADVANCED_CONFIDENCE_KILLS,
"teamkill_penalty_per_kill": MONTHLY_MVP_V2_TEAMKILL_PENALTY_PER_KILL,
"teamkill_penalty_cap": MONTHLY_MVP_V2_TEAMKILL_PENALTY_CAP,
}

View File

@@ -17,6 +17,12 @@ from .historical_snapshots import (
DEFAULT_WEEKLY_SNAPSHOT_WINDOW,
SNAPSHOT_TYPE_MONTHLY_LEADERBOARD,
SNAPSHOT_TYPE_MONTHLY_MVP,
SNAPSHOT_TYPE_MONTHLY_MVP_V2,
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY,
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS,
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED,
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS,
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS,
SNAPSHOT_TYPE_RECENT_MATCHES,
SNAPSHOT_TYPE_SERVER_SUMMARY,
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
@@ -357,6 +363,59 @@ def build_monthly_mvp_payload(
}
def build_player_event_payload(
*,
limit: int = 10,
server_id: str | None = None,
view: str = "most-killed",
) -> dict[str, object]:
"""Return one V2 player-event payload through the stable API surface."""
snapshot_payload = build_player_event_snapshot_payload(
limit=limit,
server_id=server_id,
view=view,
)
data = snapshot_payload["data"]
return {
"status": "ok",
"data": {
**data,
"title": _build_player_event_title(
view=view,
is_all_servers=server_id == ALL_SERVERS_SLUG,
snapshot=False,
),
"context": "historical-player-events",
"source": "historical-precomputed-player-event-snapshots",
},
}
def build_monthly_mvp_v2_payload(
*,
limit: int = 10,
server_id: str | None = None,
) -> dict[str, object]:
"""Return the precomputed monthly MVP V2 payload through the stable API surface."""
snapshot_payload = build_monthly_mvp_v2_snapshot_payload(
limit=limit,
server_id=server_id,
)
data = snapshot_payload["data"]
return {
"status": "ok",
"data": {
**data,
"title": _build_monthly_mvp_v2_title(
is_all_servers=server_id == ALL_SERVERS_SLUG,
snapshot=False,
),
"context": "historical-monthly-mvp-v2",
"source": "historical-precomputed-snapshots",
},
}
def build_historical_server_summary_snapshot_payload(
*,
server_slug: str | None = None,
@@ -573,6 +632,105 @@ def build_monthly_mvp_snapshot_payload(
}
def build_monthly_mvp_v2_snapshot_payload(
*,
limit: int = 10,
server_id: str | None = None,
) -> dict[str, object]:
"""Return one precomputed monthly MVP V2 snapshot."""
snapshot = _get_historical_snapshot_record(
server_key=server_id,
snapshot_type=SNAPSHOT_TYPE_MONTHLY_MVP_V2,
window=DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
)
payload = snapshot.get("payload") if snapshot else {}
items = payload.get("items") if isinstance(payload, dict) else None
sliced_items = list(items[:limit]) if isinstance(items, list) else []
found = bool(payload.get("found")) if isinstance(payload, dict) else False
return {
"status": "ok",
"data": {
"title": _build_monthly_mvp_v2_title(
is_all_servers=server_id == ALL_SERVERS_SLUG,
snapshot=True,
),
"context": "historical-monthly-mvp-v2-snapshot",
"source": "historical-precomputed-snapshots",
"server_slug": server_id,
"timeframe": "monthly",
"metric": "mvp-v2",
"found": snapshot is not None and found,
**_build_historical_snapshot_metadata(snapshot),
"month_key": payload.get("month_key") if isinstance(payload, dict) else None,
"window_days": payload.get("window_days") if isinstance(payload, dict) else None,
"window_start": payload.get("window_start") if isinstance(payload, dict) else None,
"window_end": payload.get("window_end") if isinstance(payload, dict) else None,
"window_kind": payload.get("window_kind") if isinstance(payload, dict) else None,
"window_label": payload.get("window_label") if isinstance(payload, dict) else None,
"uses_fallback": bool(payload.get("uses_fallback")) if isinstance(payload, dict) else False,
"selection_reason": payload.get("selection_reason") if isinstance(payload, dict) else None,
"current_month_start": payload.get("current_month_start") if isinstance(payload, dict) else None,
"current_month_closed_matches": (
payload.get("current_month_closed_matches") if isinstance(payload, dict) else None
),
"previous_month_closed_matches": (
payload.get("previous_month_closed_matches") if isinstance(payload, dict) else None
),
"sufficient_sample": payload.get("sufficient_sample") if isinstance(payload, dict) else None,
"eligibility": payload.get("eligibility") if isinstance(payload, dict) else None,
"ranking_version": payload.get("ranking_version") if isinstance(payload, dict) else None,
"event_coverage": payload.get("event_coverage") if isinstance(payload, dict) else None,
"eligible_players_count": (
payload.get("eligible_players_count") if isinstance(payload, dict) else 0
),
"snapshot_limit": payload.get("limit") if isinstance(payload, dict) else None,
"limit": limit,
"items": sliced_items,
},
}
def build_player_event_snapshot_payload(
*,
limit: int = 10,
server_id: str | None = None,
view: str = "most-killed",
) -> dict[str, object]:
"""Return one precomputed V2 player-event snapshot."""
snapshot_type = _resolve_player_event_snapshot_type(view)
snapshot = _get_historical_snapshot_record(
server_key=server_id,
snapshot_type=snapshot_type,
window=DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
)
payload = snapshot.get("payload") if snapshot else {}
items = payload.get("items") if isinstance(payload, dict) else None
sliced_items = list(items[:limit]) if isinstance(items, list) else []
found = bool(payload.get("found")) if isinstance(payload, dict) else False
return {
"status": "ok",
"data": {
"title": _build_player_event_title(
view=view,
is_all_servers=server_id == ALL_SERVERS_SLUG,
snapshot=True,
),
"context": "historical-player-events-snapshot",
"source": "historical-precomputed-player-event-snapshots",
"server_slug": server_id,
"timeframe": "monthly",
"metric": view,
"found": snapshot is not None and found,
**_build_historical_snapshot_metadata(snapshot),
"period": payload.get("period") if isinstance(payload, dict) else "monthly",
"month_key": payload.get("month_key") if isinstance(payload, dict) else None,
"snapshot_limit": payload.get("limit") if isinstance(payload, dict) else None,
"limit": limit,
"items": sliced_items,
},
}
def build_historical_server_summary_payload(
*,
server_slug: str | None = None,
@@ -683,6 +841,42 @@ def _build_monthly_mvp_title(*, is_all_servers: bool, snapshot: bool = False) ->
return f"{prefix}Top MVP mensual {scope_label}"
def _build_monthly_mvp_v2_title(*, is_all_servers: bool, snapshot: bool = False) -> str:
prefix = "Snapshot " if snapshot else ""
scope_label = "global" if is_all_servers else "por servidor"
return f"{prefix}Top MVP mensual V2 {scope_label}"
def _build_player_event_title(
*,
view: str,
is_all_servers: bool,
snapshot: bool = False,
) -> str:
prefix = "Snapshot " if snapshot else ""
scope_label = "global" if is_all_servers else "por servidor"
title_by_view = {
"most-killed": f"{prefix}Most killed mensual {scope_label}",
"death-by": f"{prefix}Death by mensual {scope_label}",
"duels": f"{prefix}Duelos netos mensuales {scope_label}",
"weapon-kills": f"{prefix}Kills por arma mensuales {scope_label}",
"teamkills": f"{prefix}Teamkills mensuales {scope_label}",
}
return title_by_view.get(view, f"{prefix}Metricas V2 mensuales {scope_label}")
def _resolve_player_event_snapshot_type(view: str) -> str:
normalized_view = view.strip().lower() if isinstance(view, str) else "most-killed"
snapshot_type_by_view = {
"most-killed": SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED,
"death-by": SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY,
"duels": SNAPSHOT_TYPE_PLAYER_EVENT_DUELS,
"weapon-kills": SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS,
"teamkills": SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS,
}
return snapshot_type_by_view.get(normalized_view, SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED)
def _enrich_server_items(items: list[dict[str, object]]) -> list[dict[str, object]]:
target_index = get_live_data_source().build_target_index()
enriched_items: list[dict[str, object]] = []

View File

@@ -12,9 +12,13 @@ from .payloads import (
build_health_payload,
build_historical_leaderboard_payload,
build_monthly_mvp_payload,
build_monthly_mvp_v2_payload,
build_monthly_leaderboard_payload,
build_monthly_leaderboard_snapshot_payload,
build_monthly_mvp_snapshot_payload,
build_monthly_mvp_v2_snapshot_payload,
build_player_event_payload,
build_player_event_snapshot_payload,
build_historical_server_summary_snapshot_payload,
build_historical_player_profile_payload,
build_historical_server_summary_payload,
@@ -119,6 +123,31 @@ def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object]
server_id=server_id,
)
if parsed.path == "/api/historical/monthly-mvp-v2":
limit = _parse_limit(parsed.query)
if limit is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
server_id = parse_qs(parsed.query).get("server", [None])[0]
return HTTPStatus.OK, build_monthly_mvp_v2_payload(
limit=limit,
server_id=server_id,
)
if parsed.path == "/api/historical/player-events":
limit = _parse_limit(parsed.query)
if limit is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
params = parse_qs(parsed.query)
server_id = params.get("server", [None])[0]
view = params.get("view", ["most-killed"])[0]
if view not in {"most-killed", "death-by", "duels", "weapon-kills", "teamkills"}:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid view parameter")
return HTTPStatus.OK, build_player_event_payload(
limit=limit,
server_id=server_id,
view=view,
)
if parsed.path == "/api/historical/snapshots/leaderboard":
limit = _parse_limit(parsed.query)
if limit is None:
@@ -163,6 +192,31 @@ def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object]
server_id=server_id,
)
if parsed.path == "/api/historical/snapshots/monthly-mvp-v2":
limit = _parse_limit(parsed.query)
if limit is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
server_id = parse_qs(parsed.query).get("server", [None])[0]
return HTTPStatus.OK, build_monthly_mvp_v2_snapshot_payload(
limit=limit,
server_id=server_id,
)
if parsed.path == "/api/historical/snapshots/player-events":
limit = _parse_limit(parsed.query)
if limit is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
params = parse_qs(parsed.query)
server_id = params.get("server", [None])[0]
view = params.get("view", ["most-killed"])[0]
if view not in {"most-killed", "death-by", "duels", "weapon-kills", "teamkills"}:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid view parameter")
return HTTPStatus.OK, build_player_event_snapshot_payload(
limit=limit,
server_id=server_id,
view=view,
)
if parsed.path == "/api/historical/snapshots/weekly-leaderboard":
limit = _parse_limit(parsed.query)
if limit is None: