"""Deterministic gameplay quality scoring. Quality controls replay retention, never whether a game's result contributes to historical rates. Structural failures produce ``invalid``; otherwise strategic signals produce a 0-100 score and high/medium/low tier. """ from dataclasses import dataclass QUALITY_ORDER = {"invalid": 0, "low": 1, "medium": 2, "high": 3} @dataclass(frozen=True) class GameQualityInput: status:str final_turn:int turn_rows:int min_turn:int|None max_turn:int|None valid_moves:int thinking_rows:int distinct_moves:int snake_turn_rows:int winner_name:str|None @dataclass(frozen=True) class GameQuality: score:int tier:str reasons:tuple[str, ...] def rate_game_quality(data:GameQualityInput) -> GameQuality: reasons:list[str] = [] expected_turns = max(1, data.final_turn) coverage = min(1.0, data.turn_rows / expected_turns) valid_ratio = data.valid_moves / data.turn_rows if data.turn_rows else 0.0 thinking_ratio = data.thinking_rows / data.turn_rows if data.turn_rows else 0.0 average_snakes = data.snake_turn_rows / data.turn_rows if data.turn_rows else 0.0 if data.status != "finished": reasons.append("unfinished_game") if data.turn_rows == 0: reasons.append("missing_turns") observed_span = ( data.max_turn - data.min_turn + 1 if data.min_turn is not None and data.max_turn is not None else 0 ) if coverage < 0.8 or observed_span != data.turn_rows: reasons.append("incomplete_turn_sequence") if valid_ratio < 0.95: reasons.append("invalid_or_missing_moves") if reasons: return GameQuality(score=0, tier="invalid", reasons=tuple(reasons)) score = 25.0 * coverage score += 10.0 * valid_ratio score += 20.0 * min(1.0, data.final_turn / 40.0) score += 15.0 * thinking_ratio score += 10.0 * min(1.0, data.distinct_moves / 3.0) if average_snakes >= 3.0: score += 15.0 elif average_snakes >= 1.8: score += 10.0 elif average_snakes >= 1.0: score += 3.0 if data.winner_name: score += 5.0 if coverage >= 0.98: reasons.append("complete_turn_sequence") if valid_ratio == 1.0: reasons.append("valid_moves") if thinking_ratio >= 0.9: reasons.append("complete_thinking_data") elif thinking_ratio < 0.25: reasons.append("sparse_thinking_data") if average_snakes >= 1.8: reasons.append("competitive_game") else: reasons.append("limited_opposition_data") if data.final_turn < 3: reasons.append("very_short_game") elif data.final_turn < 10: reasons.append("short_game") else: reasons.append("substantial_game_length") if data.distinct_moves <= 1: reasons.append("low_move_diversity") rounded_score = max(0, min(100, round(score))) if rounded_score >= 80 and data.final_turn >= 10: tier = "high" elif rounded_score >= 55: tier = "medium" else: tier = "low" return GameQuality(score=rounded_score, tier=tier, reasons=tuple(reasons)) def quality_meets_minimum(tier:str, minimum_tier:str) -> bool: return QUALITY_ORDER.get(tier, 0) >= QUALITY_ORDER[minimum_tier]