c704fbc742
Build and Push Docker Container / build-and-push (push) Successful in 8m3s
- Add bitboard-accelerated Prism and versioned Supreme snake implementations. - Add database-backed move benchmarks and focused strategy tests. - Normalize gameplay storage while preserving replay compatibility. - Add deterministic game quality scoring and replay retention tiers. - Add backup-first SQLite cleanup, verification, and replacement tooling. - Add safe compact-plus-delta database merging with conflict detection. - Extend SQLite and PostgreSQL schemas for replay and quality metadata. - Add PostgreSQL development service and pytest import configuration. - Update gameplay documentation and the quart_common submodule revision.
101 lines
3.0 KiB
Python
101 lines
3.0 KiB
Python
"""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]
|