"""Contracts and capability helpers for the Elo/MMR monthly ranking system.""" from __future__ import annotations from dataclasses import asdict, dataclass CAPABILITY_EXACT = "exact" CAPABILITY_APPROXIMATE = "approximate" CAPABILITY_UNAVAILABLE = "not_available" ACCURACY_EXACT = "exact" ACCURACY_APPROXIMATE = "approximate" ACCURACY_PARTIAL = "partial" DEFAULT_BASE_MMR = 1000.0 ELO_K_FACTOR = 60.0 MIN_VALID_MATCH_DURATION_SECONDS = 900 MIN_VALID_MATCH_PLAYERS = 20 MIN_VALID_PLAYER_PARTICIPATION_SECONDS = 900 MIN_VALID_PLAYER_PARTICIPATION_RATIO = 0.45 FULL_QUALITY_PLAYER_COUNT = 70 FULL_QUALITY_DURATION_SECONDS = 3600 MONTHLY_MIN_VALID_MATCHES = 5 MONTHLY_MIN_TIME_SECONDS = 21600 MONTHLY_ACTIVITY_TARGET_MATCHES = 12 MONTHLY_ACTIVITY_TARGET_HOURS = 20.0 DEFAULT_MONTHLY_SCOREBOARD_MIN_MATCHES = 3 @dataclass(frozen=True, slots=True) class EloSignalAvailability: """Normalized availability state for one scoring input.""" name: str status: str detail: str def to_dict(self) -> dict[str, object]: """Return the availability entry as a serializable mapping.""" return asdict(self) def build_signal(name: str, status: str, detail: str) -> dict[str, object]: """Create a normalized availability block for one signal.""" return EloSignalAvailability(name=name, status=status, detail=detail).to_dict() def summarize_accuracy(signals: list[dict[str, object]]) -> dict[str, object]: """Summarize exact, approximate and unavailable signals for one calculation.""" exact_count = sum(1 for signal in signals if signal.get("status") == CAPABILITY_EXACT) approximate_count = sum( 1 for signal in signals if signal.get("status") == CAPABILITY_APPROXIMATE ) unavailable_count = sum( 1 for signal in signals if signal.get("status") == CAPABILITY_UNAVAILABLE ) if unavailable_count > 0: accuracy_mode = ACCURACY_PARTIAL elif approximate_count > 0: accuracy_mode = ACCURACY_APPROXIMATE else: accuracy_mode = ACCURACY_EXACT total = max(1, len(signals)) return { "accuracy_mode": accuracy_mode, "exact_count": exact_count, "approximate_count": approximate_count, "unavailable_count": unavailable_count, "exact_ratio": round(exact_count / total, 3), "approximate_ratio": round(approximate_count / total, 3), "unavailable_ratio": round(unavailable_count / total, 3), "signals": list(signals), }