"""Core Elo/MMR rebuild engine backed by real historical signals.""" from __future__ import annotations import argparse import json from collections import defaultdict from datetime import datetime, timezone from statistics import pstdev from typing import Iterable from .config import get_historical_data_source_kind from .data_sources import ( SOURCE_KIND_PUBLIC_SCOREBOARD, SOURCE_KIND_RCON, build_source_attempt, build_source_policy, get_rcon_historical_read_model, ) from .elo_mmr_models import ( CAPABILITY_APPROXIMATE, CAPABILITY_EXACT, CAPABILITY_UNAVAILABLE, DEFAULT_BASE_MMR, ELO_K_FACTOR, FULL_QUALITY_DURATION_SECONDS, FULL_QUALITY_PLAYER_COUNT, MIN_VALID_MATCH_DURATION_SECONDS, MIN_VALID_PLAYER_PARTICIPATION_RATIO, MIN_VALID_PLAYER_PARTICIPATION_SECONDS, MIN_VALID_MATCH_PLAYERS, MONTHLY_ACTIVITY_TARGET_HOURS, MONTHLY_ACTIVITY_TARGET_MATCHES, MONTHLY_MIN_TIME_SECONDS, MONTHLY_MIN_VALID_MATCHES, build_signal, summarize_accuracy, ) from .elo_mmr_storage import ( get_elo_mmr_player_profile, initialize_elo_mmr_storage, list_elo_mmr_monthly_rankings, replace_elo_mmr_state, ) from .historical_storage import ALL_SERVERS_SLUG, initialize_historical_storage from .rcon_historical_read_model import get_rcon_historical_competitive_match_context from .sqlite_utils import connect_sqlite_readonly from .writer_lock import backend_writer_lock, build_writer_lock_holder SCOPE_ALL_SERVERS = ALL_SERVERS_SLUG QUALITY_BUCKET_HIGH = "high" QUALITY_BUCKET_MEDIUM = "medium" QUALITY_BUCKET_LOW = "low" ROLE_BUCKET_SUPPORT = "support" ROLE_BUCKET_OFFENSE = "offense" ROLE_BUCKET_DEFENSE = "defense" ROLE_BUCKET_COMBAT = "combat" ROLE_BUCKET_GENERALIST = "generalist" MONTHLY_MIN_AVG_PARTICIPATION_RATIO = 0.45 MONTHLY_RANK_WEIGHT_COMPETITIVE_GAIN = 0.70 MONTHLY_RANK_WEIGHT_MATCH_SCORE = 0.14 MONTHLY_RANK_WEIGHT_STRENGTH_OF_SCHEDULE = 0.05 MONTHLY_RANK_WEIGHT_CONSISTENCY = 0.04 MONTHLY_RANK_WEIGHT_CONFIDENCE = 0.04 MONTHLY_RANK_WEIGHT_ACTIVITY = 0.03 EXACT_MODIFIER_K_SHARE = 0.06 PROXY_MODIFIER_K_SHARE = 0.02 ROLE_WEIGHTS = { ROLE_BUCKET_SUPPORT: {"combat": 0.18, "objective": 0.18, "utility": 0.42, "discipline": 0.22}, ROLE_BUCKET_OFFENSE: {"combat": 0.38, "objective": 0.30, "utility": 0.10, "discipline": 0.22}, ROLE_BUCKET_DEFENSE: {"combat": 0.26, "objective": 0.34, "utility": 0.16, "discipline": 0.24}, ROLE_BUCKET_COMBAT: {"combat": 0.48, "objective": 0.14, "utility": 0.14, "discipline": 0.24}, ROLE_BUCKET_GENERALIST: {"combat": 0.34, "objective": 0.22, "utility": 0.20, "discipline": 0.24}, } def rebuild_elo_mmr_models(*, db_path=None) -> dict[str, object]: """Rebuild persistent player ratings and monthly rankings from scratch.""" with backend_writer_lock(holder=build_writer_lock_holder("app.elo_mmr_engine rebuild")): resolved_path = initialize_historical_storage(db_path=db_path) initialize_elo_mmr_storage(db_path=resolved_path) historical_source_policy = _build_historical_source_policy_for_elo() rcon_read_model = get_rcon_historical_read_model() match_rows = _load_closed_match_rows(db_path=resolved_path) grouped_matches = _group_match_rows(match_rows) rcon_match_context_cache: dict[tuple[str, str | None, str | None], dict[str, object] | None] = {} ratings_by_scope: dict[str, dict[str, dict[str, object]]] = {SCOPE_ALL_SERVERS: {}} player_ratings: list[dict[str, object]] = [] match_results: list[dict[str, object]] = [] monthly_checkpoints: list[dict[str, object]] = [] for match_group in grouped_matches: server_scope = match_group["server_slug"] ratings_by_scope.setdefault(server_scope, {}) rcon_match_context = None if rcon_read_model is not None: cache_key = ( str(match_group["server_slug"]), str(match_group.get("ended_at")) if match_group.get("ended_at") is not None else None, str(match_group.get("map_pretty_name") or match_group.get("map_name") or "") or None, ) if cache_key not in rcon_match_context_cache: rcon_match_context_cache[cache_key] = get_rcon_historical_competitive_match_context( server_key=str(match_group["server_slug"]), ended_at=match_group.get("ended_at"), map_name=match_group.get("map_pretty_name") or match_group.get("map_name"), ) rcon_match_context = rcon_match_context_cache[cache_key] for scope_key in (server_scope, SCOPE_ALL_SERVERS): match_results.extend( _score_match_for_scope( match_group=match_group, scope_key=scope_key, ratings_by_scope=ratings_by_scope[scope_key], rcon_match_context=rcon_match_context, ) ) for scope_ratings in ratings_by_scope.values(): player_ratings.extend(scope_ratings.values()) monthly_rankings = _build_monthly_rankings(match_results) checkpoint_groups: dict[tuple[str, str], list[dict[str, object]]] = defaultdict(list) for row in monthly_rankings: checkpoint_groups[(row["scope_key"], row["month_key"])].append(row) generated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") for (scope_key, month_key), rows in checkpoint_groups.items(): eligible_count = sum(1 for row in rows if row["eligible"]) exact_ratio = round( sum(float(row["capabilities"]["exact_ratio"]) for row in rows) / max(1, len(rows)), 3, ) approximate_ratio = round( sum(float(row["capabilities"]["approximate_ratio"]) for row in rows) / max(1, len(rows)), 3, ) unavailable_ratio = round( sum(float(row["capabilities"]["unavailable_ratio"]) for row in rows) / max(1, len(rows)), 3, ) partial_count = sum(1 for row in rows if row["accuracy_mode"] == "partial") monthly_checkpoints.append( { "scope_key": scope_key, "month_key": month_key, "generated_at": generated_at, "player_count": len(rows), "eligible_player_count": eligible_count, "source_policy": historical_source_policy, "capabilities_summary": { "accuracy_mode": "partial" if partial_count > 0 else "approximate" if approximate_ratio > 0 else "exact", "exact_ratio": exact_ratio, "approximate_ratio": approximate_ratio, "unavailable_ratio": unavailable_ratio, "partial_count": partial_count, "notes": [ "Outcome, combat, utility, match validity and player participation use real stored signals.", "ObjectiveIndex, role bucket, discipline and strength of schedule rely partly on honest proxies.", "LeadershipIndex is not available with the current repository telemetry.", ], }, } ) replace_elo_mmr_state( player_ratings=player_ratings, match_results=match_results, monthly_rankings=monthly_rankings, monthly_checkpoints=monthly_checkpoints, db_path=resolved_path, ) latest_month_by_scope = { checkpoint["scope_key"]: checkpoint["month_key"] for checkpoint in monthly_checkpoints } return { "status": "ok", "historical_source_policy": historical_source_policy, "totals": { "matches_scored": len({(row["scope_key"], row["external_match_id"]) for row in match_results}), "player_ratings": len(player_ratings), "match_results": len(match_results), "monthly_rankings": len(monthly_rankings), "monthly_checkpoints": len(monthly_checkpoints), }, "latest_month_by_scope": latest_month_by_scope, } def list_elo_mmr_leaderboard_payload(*, server_id: str | None, limit: int) -> dict[str, object]: """Return the current monthly Elo/MMR leaderboard for one scope.""" scope_key = _normalize_scope_key(server_id) result = list_elo_mmr_monthly_rankings(scope_key=scope_key, limit=limit) return { "scope_key": scope_key, "month_key": result["month_key"], "found": result["found"], "generated_at": result["generated_at"], "items": result["items"], "source_policy": result["source_policy"] or _build_historical_source_policy_for_elo(), "capabilities_summary": result["capabilities_summary"], } def get_elo_mmr_player_payload(*, player_id: str, server_id: str | None) -> dict[str, object] | None: """Return one Elo/MMR player profile.""" return get_elo_mmr_player_profile( player_id=player_id, scope_key=_normalize_scope_key(server_id), ) def build_arg_parser() -> argparse.ArgumentParser: """Build the CLI parser for Elo/MMR maintenance.""" parser = argparse.ArgumentParser( description="Rebuild or inspect the Elo/MMR monthly ranking system.", ) parser.add_argument( "mode", choices=("rebuild", "leaderboard", "player"), help="rebuild recomputes all persisted Elo/MMR state; leaderboard and player inspect the read model", ) parser.add_argument("--server", dest="server_id", help="optional server scope") parser.add_argument("--limit", type=int, default=10, help="max rows for leaderboard mode") parser.add_argument("--player", dest="player_id", help="player id or steam id for player mode") return parser def main(argv: Iterable[str] | None = None) -> int: """Run the Elo/MMR CLI.""" parser = build_arg_parser() args = parser.parse_args(list(argv) if argv is not None else None) if args.mode == "rebuild": print(json.dumps(rebuild_elo_mmr_models(), indent=2)) return 0 if args.mode == "leaderboard": print(json.dumps(list_elo_mmr_leaderboard_payload(server_id=args.server_id, limit=args.limit), indent=2)) return 0 if not args.player_id: parser.error("--player is required in player mode") print(json.dumps(get_elo_mmr_player_payload(player_id=args.player_id, server_id=args.server_id), indent=2)) return 0 def _load_closed_match_rows(*, db_path) -> list[dict[str, object]]: with connect_sqlite_readonly(db_path) as connection: rows = connection.execute( """ SELECT historical_servers.slug AS server_slug, historical_servers.display_name AS server_name, historical_matches.external_match_id, historical_matches.started_at, historical_matches.ended_at, historical_matches.game_mode, historical_matches.allied_score, historical_matches.axis_score, historical_players.stable_player_key, historical_players.display_name AS player_name, historical_players.steam_id, historical_player_match_stats.team_side, historical_player_match_stats.kills, historical_player_match_stats.deaths, historical_player_match_stats.teamkills, historical_player_match_stats.time_seconds, historical_player_match_stats.combat, historical_player_match_stats.offense, historical_player_match_stats.defense, historical_player_match_stats.support 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 WHERE historical_matches.ended_at IS NOT NULL ORDER BY historical_matches.ended_at ASC, historical_matches.id ASC, historical_players.id ASC """ ).fetchall() return [dict(row) for row in rows] def _group_match_rows(rows: list[dict[str, object]]) -> list[dict[str, object]]: grouped: dict[tuple[str, str], list[dict[str, object]]] = defaultdict(list) for row in rows: grouped[(str(row["server_slug"]), str(row["external_match_id"]))].append(row) items: list[dict[str, object]] = [] for (server_slug, match_id), players in grouped.items(): first = players[0] items.append( { "server_slug": server_slug, "server_name": first["server_name"], "external_match_id": match_id, "started_at": first["started_at"], "ended_at": first["ended_at"], "game_mode": first["game_mode"], "allied_score": _safe_int(first["allied_score"]), "axis_score": _safe_int(first["axis_score"]), "players": players, } ) return items def _score_match_for_scope( *, match_group: dict[str, object], scope_key: str, ratings_by_scope: dict[str, dict[str, object]], rcon_match_context: dict[str, object] | None = None, ) -> list[dict[str, object]]: players = list(match_group["players"]) duration_seconds, duration_mode = _resolve_match_duration( match_group, players, rcon_match_context=rcon_match_context, ) quality_factor = _build_quality_factor( player_count=max(len(players), int(rcon_match_context.get("peak_players") or 0)) if rcon_match_context is not None else len(players), duration_seconds=duration_seconds, has_score=match_group.get("allied_score") is not None and match_group.get("axis_score") is not None, ) quality_bucket = _classify_quality_bucket(quality_factor) match_valid = duration_seconds >= MIN_VALID_MATCH_DURATION_SECONDS and len(players) >= MIN_VALID_MATCH_PLAYERS month_key = str(match_group["ended_at"])[:7] max_kills = max(max(_safe_int(player.get("kills")), 0) for player in players) or 1 max_support = max(max(_safe_int(player.get("support")), 0) for player in players) or 1 max_combat = max(max(_safe_int(player.get("combat")), 0) for player in players) or 1 max_objective = max( max(_safe_int(player.get("offense")) + _safe_int(player.get("defense")), 0) for player in players ) or 1 results: list[dict[str, object]] = [] rating_before_by_player = { str(player["stable_player_key"]): float( ratings_by_scope.get(str(player["stable_player_key"]), {}).get("current_mmr", DEFAULT_BASE_MMR) ) for player in players } for player in players: stable_player_key = str(player["stable_player_key"]) rating_row = ratings_by_scope.setdefault( stable_player_key, { "scope_key": scope_key, "stable_player_key": stable_player_key, "player_name": player["player_name"], "steam_id": player.get("steam_id"), "current_mmr": DEFAULT_BASE_MMR, "matches_processed": 0, "wins": 0, "draws": 0, "losses": 0, "last_match_id": None, "last_match_ended_at": None, "accuracy_mode": "partial", "capabilities": summarize_accuracy([]), }, ) signals: list[dict[str, object]] = [] time_seconds = _safe_int(player.get("time_seconds")) participation_ratio = _build_participation_ratio( time_seconds=time_seconds, duration_seconds=duration_seconds, ) player_match_valid = match_valid and _is_player_match_eligible( time_seconds=time_seconds, participation_ratio=participation_ratio, ) team_outcome = _resolve_team_outcome( team_side=str(player.get("team_side") or ""), allied_score=_safe_int(match_group.get("allied_score")), axis_score=_safe_int(match_group.get("axis_score")), ) outcome_score = _build_outcome_score( team_outcome=team_outcome, allied_score=_safe_int(match_group.get("allied_score")), axis_score=_safe_int(match_group.get("axis_score")), ) signals.append(build_signal("OutcomeScore", CAPABILITY_EXACT, "Derived from team side and final match score.")) signals.append(build_signal("MatchValidity", CAPABILITY_EXACT, "Uses closed match state, duration and lobby size thresholds.")) if duration_seconds > 0: signals.append(build_signal("PlayerParticipation", CAPABILITY_EXACT, "Uses persisted player time_seconds relative to match duration.")) kills = _safe_int(player.get("kills")) deaths = max(1, _safe_int(player.get("deaths"))) combat_raw = _safe_int(player.get("combat")) combat_index = round( (40.0 * (kills / max_kills)) + (35.0 * min(1.0, (kills / deaths) / 3.0)) + (25.0 * (combat_raw / max_combat)), 3, ) signals.append(build_signal("CombatIndex", CAPABILITY_EXACT, "Uses kills, KDA proxy and persisted combat score.")) support = _safe_int(player.get("support")) utility_index = round(100.0 * (support / max_support), 3) if max_support > 0 else 0.0 signals.append(build_signal("UtilityIndex", CAPABILITY_EXACT, "Uses persisted support points.")) objective_proxy = _safe_int(player.get("offense")) + _safe_int(player.get("defense")) objective_index = round(100.0 * (objective_proxy / max_objective), 3) if max_objective > 0 else 0.0 signals.append(build_signal("ObjectiveIndex", CAPABILITY_APPROXIMATE, "Approximated from offense and defense scoreboard points because no tactical event feed exists yet.")) teamkills = _safe_int(player.get("teamkills")) completion_component = round(participation_ratio * 100.0, 3) discipline_index = round( max( 0.0, (88.0 - (teamkills * 18.0)) + (0.12 * completion_component), ), 3, ) signals.append(build_signal("DisciplineIndex", CAPABILITY_APPROXIMATE, "Uses exact teamkills plus participation as an honest proxy for leave or AFK risk because direct discipline telemetry is unavailable.")) leadership_index = None signals.append(build_signal("LeadershipIndex", CAPABILITY_UNAVAILABLE, "No leadership-specific telemetry is stored in the repository yet.")) role_bucket = _resolve_role_bucket(player) signals.append(build_signal("role_bucket", CAPABILITY_APPROXIMATE, "Inferred from the dominant combat/offense/defense/support axis because literal player role is unavailable.")) if duration_mode == CAPABILITY_EXACT: signals.append(build_signal("quality_duration", CAPABILITY_EXACT, "Duration computed from match timestamps.")) else: signals.append(build_signal("quality_duration", CAPABILITY_APPROXIMATE, "Duration approximated from the maximum persisted player time.")) if rcon_match_context is not None: signals.append( build_signal( "RconCompetitiveWindow", CAPABILITY_APPROXIMATE, "Uses the closest RCON-backed competitive window for match duration and lobby density when coverage exists.", ) ) weights = ROLE_WEIGHTS.get(role_bucket, ROLE_WEIGHTS[ROLE_BUCKET_GENERALIST]) impact_score = round( sum( { "combat": combat_index, "objective": objective_index, "utility": utility_index, "discipline": discipline_index, }[key] * weight for key, weight in weights.items() ), 3, ) team_side = str(player.get("team_side") or "") strength_of_schedule_match = _build_strength_of_schedule_match( stable_player_key=stable_player_key, team_side=team_side, players=players, rating_before_by_player=rating_before_by_player, quality_factor=quality_factor, ) signals.append(build_signal("StrengthOfScheduleMatch", CAPABILITY_APPROXIMATE, "Approximated from opponent average MMR pressure plus match quality because no full roster graph is stored.")) exact_modifier_index = _build_weighted_modifier_index( left_value=combat_index, right_value=utility_index, left_weight=weights["combat"], right_weight=weights["utility"], ) proxy_modifier_index = _build_weighted_modifier_index( left_value=objective_index, right_value=discipline_index, left_weight=weights["objective"], right_weight=weights["discipline"], ) effective_score = round( ( (0.60 * outcome_score) + (0.25 * impact_score) + (0.10 * strength_of_schedule_match) + (0.05 * discipline_index) ) * participation_ratio, 3, ) if not player_match_valid: delta_mmr = 0.0 match_score = 0.0 expected_result = 0.0 actual_result = 0.0 elo_core_delta = 0.0 performance_modifier_delta = 0.0 proxy_modifier_delta = 0.0 else: expected_result = _build_expected_result( player_rating=rating_before_by_player.get(stable_player_key, DEFAULT_BASE_MMR), opponent_average_rating=_resolve_opponent_average_rating( stable_player_key=stable_player_key, team_side=team_side, players=players, rating_before_by_player=rating_before_by_player, ), ) actual_result = _build_actual_result( team_outcome=team_outcome, allied_score=_safe_int(match_group.get("allied_score")), axis_score=_safe_int(match_group.get("axis_score")), participation_ratio=participation_ratio, ) exact_modifier_edge = _build_centered_modifier_edge( exact_modifier_index, participation_ratio=participation_ratio, ) proxy_modifier_edge = _build_centered_modifier_edge( proxy_modifier_index, participation_ratio=participation_ratio, ) elo_core_delta = round( ELO_K_FACTOR * quality_factor * (actual_result - expected_result), 3, ) exact_modifier_delta = round( ELO_K_FACTOR * quality_factor * EXACT_MODIFIER_K_SHARE * exact_modifier_edge, 3, ) proxy_modifier_delta = round( ELO_K_FACTOR * quality_factor * PROXY_MODIFIER_K_SHARE * proxy_modifier_edge, 3, ) performance_modifier_delta = round( exact_modifier_delta + proxy_modifier_delta, 3, ) delta_mmr = round(elo_core_delta + performance_modifier_delta, 3) match_score = round(effective_score * quality_factor, 3) signals.append(build_signal("DeltaMMR", CAPABILITY_APPROXIMATE, "Uses Elo-like expected-vs-actual movement plus bounded HLL performance modifiers and honest proxy boundaries.")) signals.append(build_signal("MatchScore", CAPABILITY_APPROXIMATE, "Uses outcome-first competitive scoring with bounded HLL impact and schedule context, then scales by match quality.")) capability_summary = summarize_accuracy(signals) rating_before = float(rating_row["current_mmr"]) rating_after = round(rating_before + delta_mmr, 3) results.append( { "scope_key": scope_key, "month_key": month_key, "external_match_id": match_group["external_match_id"], "stable_player_key": stable_player_key, "player_name": player["player_name"], "steam_id": player.get("steam_id"), "server_slug": match_group["server_slug"], "server_name": match_group["server_name"], "match_ended_at": match_group["ended_at"], "match_valid": player_match_valid, "quality_factor": quality_factor, "quality_bucket": quality_bucket, "role_bucket": role_bucket, "role_bucket_mode": CAPABILITY_APPROXIMATE, "outcome_score": outcome_score, "combat_index": combat_index, "objective_index": objective_index, "objective_index_mode": CAPABILITY_APPROXIMATE, "utility_index": utility_index, "utility_index_mode": CAPABILITY_EXACT, "leadership_index": leadership_index, "leadership_index_mode": CAPABILITY_UNAVAILABLE, "discipline_index": discipline_index, "discipline_index_mode": CAPABILITY_APPROXIMATE, "impact_score": impact_score, "delta_mmr": delta_mmr, "mmr_before": rating_before, "mmr_after": rating_after, "match_score": match_score, "penalty_points": round((teamkills * 2.0) + max(0.0, (0.5 - participation_ratio) * 8.0), 3), "capabilities": capability_summary, "time_seconds": time_seconds, "participation_ratio": participation_ratio, "strength_of_schedule_match": strength_of_schedule_match, "team_outcome": team_outcome, "expected_result": expected_result, "actual_result": actual_result, "elo_core_delta": elo_core_delta, "performance_modifier_delta": performance_modifier_delta, "proxy_modifier_delta": proxy_modifier_delta, } ) rating_row["current_mmr"] = rating_after rating_row["matches_processed"] = int(rating_row["matches_processed"]) + 1 rating_row["last_match_id"] = match_group["external_match_id"] rating_row["last_match_ended_at"] = match_group["ended_at"] rating_row["accuracy_mode"] = capability_summary["accuracy_mode"] rating_row["capabilities"] = capability_summary if team_outcome == "win": rating_row["wins"] = int(rating_row["wins"]) + 1 elif team_outcome == "draw": rating_row["draws"] = int(rating_row["draws"]) + 1 else: rating_row["losses"] = int(rating_row["losses"]) + 1 return results def _build_monthly_rankings(match_results: list[dict[str, object]]) -> list[dict[str, object]]: grouped: dict[tuple[str, str, str], list[dict[str, object]]] = defaultdict(list) for row in match_results: grouped[(row["scope_key"], row["month_key"], row["stable_player_key"])].append(row) rankings: list[dict[str, object]] = [] grouped_by_scope_month: dict[tuple[str, str], list[dict[str, object]]] = defaultdict(list) for (scope_key, month_key, stable_player_key), rows in grouped.items(): rows.sort(key=lambda item: (item["match_ended_at"], item["external_match_id"])) valid_rows = [row for row in rows if row["match_valid"]] total_time_seconds = sum(int(row["time_seconds"] or 0) for row in rows) penalty_points = round(sum(float(row["penalty_points"]) for row in rows), 3) capability_rows = [row["capabilities"] for row in rows] exact_ratio = round(sum(float(item["exact_ratio"]) for item in capability_rows) / max(1, len(capability_rows)), 3) approximate_ratio = round(sum(float(item["approximate_ratio"]) for item in capability_rows) / max(1, len(capability_rows)), 3) unavailable_ratio = round(sum(float(item["unavailable_ratio"]) for item in capability_rows) / max(1, len(capability_rows)), 3) accuracy_mode = "partial" if unavailable_ratio > 0 else "approximate" if approximate_ratio > 0 else "exact" avg_match_score = round(sum(float(row["match_score"]) for row in valid_rows) / max(1, len(valid_rows)), 3) baseline_mmr = round(float(rows[0]["mmr_before"]), 3) current_mmr = round(float(rows[-1]["mmr_after"]), 3) mmr_gain = round(current_mmr - baseline_mmr, 3) elo_core_gain = round(sum(float(row.get("elo_core_delta") or 0.0) for row in rows), 3) performance_modifier_gain = round( sum(float(row.get("performance_modifier_delta") or 0.0) for row in rows), 3, ) proxy_modifier_gain = round( sum(float(row.get("proxy_modifier_delta") or 0.0) for row in rows), 3, ) avg_participation_ratio = round( sum(float(row.get("participation_ratio") or 0.0) for row in rows) / max(1, len(rows)), 3, ) strength_of_schedule = round( sum(float(row.get("strength_of_schedule_match") or 0.0) for row in valid_rows) / max(1, len(valid_rows)), 3, ) consistency = _build_consistency_score(valid_rows) activity = _build_activity_score(valid_rows, total_time_seconds) confidence = round( min( 100.0, (len(valid_rows) / MONTHLY_MIN_VALID_MATCHES) * 35.0 + (total_time_seconds / MONTHLY_MIN_TIME_SECONDS) * 30.0 + (avg_participation_ratio * 20.0) + (exact_ratio * 15.0), ), 3, ) eligible = ( len(valid_rows) >= MONTHLY_MIN_VALID_MATCHES and total_time_seconds >= MONTHLY_MIN_TIME_SECONDS and avg_participation_ratio >= MONTHLY_MIN_AVG_PARTICIPATION_RATIO ) eligibility_reason = _build_monthly_eligibility_reason( valid_match_count=len(valid_rows), total_time_seconds=total_time_seconds, avg_participation_ratio=avg_participation_ratio, ) grouped_by_scope_month[(scope_key, month_key)].append( { "scope_key": scope_key, "month_key": month_key, "stable_player_key": stable_player_key, "player_name": rows[-1]["player_name"], "steam_id": rows[-1].get("steam_id"), "current_mmr": current_mmr, "baseline_mmr": baseline_mmr, "mmr_gain": mmr_gain, "avg_match_score": avg_match_score, "strength_of_schedule": strength_of_schedule, "consistency": consistency, "activity": activity, "confidence": confidence, "penalty_points": penalty_points, "monthly_rank_score": 0.0, "valid_matches": len(valid_rows), "total_matches": len(rows), "total_time_seconds": total_time_seconds, "avg_participation_ratio": avg_participation_ratio, "eligible": eligible, "eligibility_reason": eligibility_reason, "accuracy_mode": accuracy_mode, "capabilities": { "accuracy_mode": accuracy_mode, "exact_ratio": exact_ratio, "approximate_ratio": approximate_ratio, "unavailable_ratio": unavailable_ratio, "signals": [ build_signal("OutcomeScore", CAPABILITY_EXACT, "Uses final scores and team side."), build_signal("CombatIndex", CAPABILITY_EXACT, "Uses historical player stats."), build_signal("ObjectiveIndex", CAPABILITY_APPROXIMATE, "Uses offense and defense scores as a tactical proxy."), build_signal("UtilityIndex", CAPABILITY_EXACT, "Uses support points."), build_signal("LeadershipIndex", CAPABILITY_UNAVAILABLE, "No leadership telemetry exists yet."), build_signal("DisciplineIndex", CAPABILITY_APPROXIMATE, "Uses teamkills exactly plus participation as a leave-risk proxy."), build_signal("StrengthOfSchedule", CAPABILITY_APPROXIMATE, "Uses opponent average MMR pressure plus match quality, not a full roster graph."), build_signal("MonthlyEligibility", CAPABILITY_EXACT, "Uses persisted valid-match count, playtime and participation thresholds."), ], }, "component_scores": { "model_version": "elo-v3-competitive", "ranking_formula_version": "elo-v3-competitive-balanced-v1", "avg_match_score": avg_match_score, "mmr_gain_raw": mmr_gain, "elo_core_gain": elo_core_gain, "performance_modifier_gain": performance_modifier_gain, "proxy_modifier_gain": proxy_modifier_gain, "competitive_gain": round( elo_core_gain + (0.25 * performance_modifier_gain) + (0.10 * proxy_modifier_gain), 3, ), "strength_of_schedule": strength_of_schedule, "consistency": consistency, "activity": activity, "confidence": confidence, "avg_participation_ratio": avg_participation_ratio, "penalty_points": penalty_points, }, } ) for rows in grouped_by_scope_month.values(): max_avg = max((row["avg_match_score"] for row in rows), default=1.0) or 1.0 max_competitive_gain = max( (max(0.0, float(row["component_scores"].get("competitive_gain") or 0.0)) for row in rows), default=1.0, ) or 1.0 max_sos = max((row["strength_of_schedule"] for row in rows), default=1.0) or 1.0 max_consistency = max((row["consistency"] for row in rows), default=1.0) or 1.0 max_activity = max((row["activity"] for row in rows), default=1.0) or 1.0 max_confidence = max((row["confidence"] for row in rows), default=1.0) or 1.0 for row in rows: competitive_gain = max(0.0, float(row["component_scores"].get("competitive_gain") or 0.0)) normalized_gain = competitive_gain / max_competitive_gain if max_competitive_gain > 0 else 0.0 row["component_scores"]["normalized_mmr_gain"] = round(normalized_gain * 100.0, 3) row["monthly_rank_score"] = round( (MONTHLY_RANK_WEIGHT_COMPETITIVE_GAIN * normalized_gain * 100.0) + (MONTHLY_RANK_WEIGHT_MATCH_SCORE * (row["avg_match_score"] / max_avg) * 100.0) + (MONTHLY_RANK_WEIGHT_STRENGTH_OF_SCHEDULE * (row["strength_of_schedule"] / max_sos) * 100.0) + (MONTHLY_RANK_WEIGHT_CONSISTENCY * (row["consistency"] / max_consistency) * 100.0) + (MONTHLY_RANK_WEIGHT_ACTIVITY * (row["activity"] / max_activity) * 100.0) + (MONTHLY_RANK_WEIGHT_CONFIDENCE * (row["confidence"] / max_confidence) * 100.0) - row["penalty_points"], 3, ) rankings.append(row) return rankings def _build_historical_source_policy_for_elo() -> dict[str, object]: if get_historical_data_source_kind() != SOURCE_KIND_RCON: return build_source_policy( primary_source=SOURCE_KIND_PUBLIC_SCOREBOARD, selected_source=SOURCE_KIND_PUBLIC_SCOREBOARD, source_attempts=[build_source_attempt(source=SOURCE_KIND_PUBLIC_SCOREBOARD, role="primary", status="success")], ) return build_source_policy( primary_source=SOURCE_KIND_RCON, selected_source="hybrid-rcon-competitive-plus-public-scoreboard", fallback_used=True, fallback_reason="rcon-competitive-context-primary-but-player-stats-still-require-public-scoreboard-supplement", source_attempts=[ build_source_attempt( source=SOURCE_KIND_RCON, role="primary", status="partial", reason="rcon-competitive-context-used-for-match-coverage-and-quality", ), build_source_attempt( source=SOURCE_KIND_PUBLIC_SCOREBOARD, role="supplemental-fallback", status="success", reason="public-scoreboard-still-provides-player-level-competitive-stats", ), ], ) def _resolve_match_duration( match_group: dict[str, object], players: list[dict[str, object]], *, rcon_match_context: dict[str, object] | None = None, ) -> tuple[int, str]: if rcon_match_context and int(rcon_match_context.get("duration_seconds") or 0) > 0: return int(rcon_match_context["duration_seconds"]), CAPABILITY_APPROXIMATE started_at = _parse_optional_timestamp(match_group.get("started_at")) ended_at = _parse_optional_timestamp(match_group.get("ended_at")) if started_at and ended_at and ended_at >= started_at: return int((ended_at - started_at).total_seconds()), CAPABILITY_EXACT return max((_safe_int(player.get("time_seconds")) for player in players), default=0), CAPABILITY_APPROXIMATE def _build_quality_factor(*, player_count: int, duration_seconds: int, has_score: bool) -> float: player_component = min(1.0, player_count / FULL_QUALITY_PLAYER_COUNT) duration_component = min(1.0, duration_seconds / FULL_QUALITY_DURATION_SECONDS) score_component = 1.0 if has_score else 0.7 return round((0.4 * player_component) + (0.4 * duration_component) + (0.2 * score_component), 3) def _build_actual_result( *, team_outcome: str, allied_score: int | None, axis_score: int | None, participation_ratio: float, ) -> float: if team_outcome == "draw": base_result = 0.5 elif team_outcome == "win": base_result = 1.0 else: base_result = 0.0 if allied_score is None or axis_score is None: margin_adjustment = 0.0 else: total_score = max(1, allied_score + axis_score) margin_ratio = abs(allied_score - axis_score) / total_score margin_adjustment = min(0.08, margin_ratio * 0.12) if team_outcome == "win": adjusted = min(1.0, base_result + margin_adjustment) elif team_outcome == "loss": adjusted = max(0.0, base_result - margin_adjustment) else: adjusted = base_result return round(0.5 + ((adjusted - 0.5) * participation_ratio), 4) def _build_weighted_modifier_index( *, left_value: float, right_value: float, left_weight: float, right_weight: float, ) -> float: total_weight = max(0.001, left_weight + right_weight) return round(((left_value * left_weight) + (right_value * right_weight)) / total_weight, 3) def _build_centered_modifier_edge(index_value: float, *, participation_ratio: float) -> float: centered = (index_value - 50.0) / 50.0 return round(max(-1.0, min(1.0, centered * participation_ratio)), 4) def _build_participation_ratio(*, time_seconds: int, duration_seconds: int) -> float: if duration_seconds <= 0: return 0.0 return round(min(1.0, max(0.0, time_seconds / duration_seconds)), 3) def _is_player_match_eligible(*, time_seconds: int, participation_ratio: float) -> bool: return ( time_seconds >= MIN_VALID_PLAYER_PARTICIPATION_SECONDS and participation_ratio >= MIN_VALID_PLAYER_PARTICIPATION_RATIO ) def _build_outcome_score(*, team_outcome: str, allied_score: int | None, axis_score: int | None) -> float: if allied_score is None or axis_score is None: return 50.0 if team_outcome == "draw" else 65.0 if team_outcome == "win" else 35.0 total_score = max(1, allied_score + axis_score) margin_ratio = abs(allied_score - axis_score) / total_score if team_outcome == "draw": return 50.0 if team_outcome == "win": return round(min(100.0, 68.0 + (margin_ratio * 32.0)), 3) return round(max(0.0, 32.0 - (margin_ratio * 32.0)), 3) def _resolve_opponent_average_rating( *, stable_player_key: str, team_side: str, players: list[dict[str, object]], rating_before_by_player: dict[str, float], ) -> float: normalized_team_side = str(team_side or "").strip().lower() opponent_ratings = [ rating_before_by_player.get(str(player["stable_player_key"]), DEFAULT_BASE_MMR) for player in players if str(player["stable_player_key"]) != stable_player_key and _is_same_team(str(player.get("team_side") or ""), normalized_team_side) is False ] if not opponent_ratings: return DEFAULT_BASE_MMR return round(sum(opponent_ratings) / len(opponent_ratings), 3) def _build_strength_of_schedule_match( *, stable_player_key: str, team_side: str, players: list[dict[str, object]], rating_before_by_player: dict[str, float], quality_factor: float, ) -> float: opponent_average = _resolve_opponent_average_rating( stable_player_key=stable_player_key, team_side=team_side, players=players, rating_before_by_player=rating_before_by_player, ) mmr_pressure = 50.0 + ((opponent_average - DEFAULT_BASE_MMR) / 8.0) quality_pressure = quality_factor * 35.0 return round(min(100.0, max(0.0, mmr_pressure + quality_pressure)), 3) def _build_expected_result(*, player_rating: float, opponent_average_rating: float) -> float: exponent = (opponent_average_rating - player_rating) / 400.0 return round(1.0 / (1.0 + (10.0**exponent)), 4) def _build_monthly_eligibility_reason( *, valid_match_count: int, total_time_seconds: int, avg_participation_ratio: float, ) -> str | None: if valid_match_count < MONTHLY_MIN_VALID_MATCHES: return "minimum-valid-matches-not-met" if total_time_seconds < MONTHLY_MIN_TIME_SECONDS: return "minimum-playtime-not-met" if avg_participation_ratio < MONTHLY_MIN_AVG_PARTICIPATION_RATIO: return "minimum-participation-ratio-not-met" return None def _classify_quality_bucket(quality_factor: float) -> str: if quality_factor >= 0.8: return QUALITY_BUCKET_HIGH if quality_factor >= 0.55: return QUALITY_BUCKET_MEDIUM return QUALITY_BUCKET_LOW def _resolve_team_outcome(*, team_side: str, allied_score: int | None, axis_score: int | None) -> str: if allied_score is None or axis_score is None or allied_score == axis_score: return "draw" normalized = team_side.strip().lower() allied_won = allied_score > axis_score if normalized.startswith("all"): return "win" if allied_won else "loss" if normalized.startswith("ax"): return "win" if not allied_won else "loss" return "draw" def _is_same_team(team_side: str, normalized_team_side: str) -> bool: candidate = team_side.strip().lower() if normalized_team_side.startswith("all"): return candidate.startswith("all") if normalized_team_side.startswith("ax"): return candidate.startswith("ax") return candidate == normalized_team_side def _resolve_role_bucket(player: dict[str, object]) -> str: axes = { ROLE_BUCKET_SUPPORT: _safe_int(player.get("support")), ROLE_BUCKET_OFFENSE: _safe_int(player.get("offense")), ROLE_BUCKET_DEFENSE: _safe_int(player.get("defense")), ROLE_BUCKET_COMBAT: _safe_int(player.get("combat")), } top_bucket, top_value = max(axes.items(), key=lambda item: item[1]) sorted_values = sorted(axes.values(), reverse=True) if top_value <= 0 or (len(sorted_values) >= 2 and sorted_values[0] == sorted_values[1]): return ROLE_BUCKET_GENERALIST return top_bucket def _build_consistency_score(rows: list[dict[str, object]]) -> float: if len(rows) <= 1: return 100.0 if rows else 0.0 values = [float(row["match_score"]) for row in rows] average = sum(values) / len(values) if average <= 0: return 0.0 return round(100.0 * (1.0 - min(1.0, pstdev(values) / max(average, 1.0))), 3) def _build_activity_score(rows: list[dict[str, object]], total_time_seconds: int) -> float: match_component = min(1.0, len(rows) / MONTHLY_ACTIVITY_TARGET_MATCHES) hour_component = min(1.0, (total_time_seconds / 3600.0) / MONTHLY_ACTIVITY_TARGET_HOURS) return round(((0.6 * match_component) + (0.4 * hour_component)) * 100.0, 3) def _normalize_scope_key(server_id: str | None) -> str: normalized = str(server_id or SCOPE_ALL_SERVERS).strip() return normalized or SCOPE_ALL_SERVERS def _parse_optional_timestamp(value: object) -> datetime | None: if not value: return None try: parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00")) except ValueError: return None if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed.astimezone(timezone.utc) def _safe_int(value: object) -> int: try: return int(value or 0) except (TypeError, ValueError): return 0 if __name__ == "__main__": raise SystemExit(main())