from __future__ import annotations from dataclasses import dataclass from statistics import mean, median from typing import Any ALLOWED_PUZZLES = [ "bridges", "flow_free", "galaxies", "loopy", "pattern", "undead", ] ALLOWED_DIFFICULTIES = ["easy", "medium", "hard"] SESSION_STATUSES = ["assigned", "ready", "attempted", "solved"] def normalize_player_name(value: str) -> str: parts = value.strip().split() return " ".join(parts).lower() def engine_for_puzzle(puzzle_type: str) -> str: return f"{puzzle_type}_ascii" @dataclass(frozen=True) class DatasetRow: filename: str puzzlename: str args: str problem: str solution: str difficulty: str image_base64: str | None = None @classmethod def from_payload(cls, payload: dict[str, Any]) -> "DatasetRow": return cls( filename=str(payload["filename"]), puzzlename=str(payload["puzzlename"]), args=str(payload.get("args") or ""), problem=str(payload["problem"]), solution=str(payload["solution"]), difficulty=str(payload["difficulty"]), image_base64=payload.get("image_base64"), ) @dataclass(frozen=True) class SolveAggregate: player_name_norm: str puzzle_type: str difficulty: str solve_count: int avg_ms: float median_ms: float min_ms: int max_ms: int @classmethod def from_rows( cls, *, player_name_norm: str, puzzle_type: str, difficulty: str, elapsed_values: list[int], ) -> "SolveAggregate": return cls( player_name_norm=player_name_norm, puzzle_type=puzzle_type, difficulty=difficulty, solve_count=len(elapsed_values), avg_ms=float(mean(elapsed_values)), median_ms=float(median(elapsed_values)), min_ms=min(elapsed_values), max_ms=max(elapsed_values), )