feat: add Prism snake and gameplay database lifecycle
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:
2026-08-01 16:21:02 +02:00
parent 9a7f4de586
commit c704fbc742
21 changed files with 3156 additions and 88 deletions
+100
View File
@@ -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]