feat: add Prism snake and gameplay database lifecycle
Build and Push Docker Container / build-and-push (push) Successful in 8m3s
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.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""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]
|
||||
Reference in New Issue
Block a user