from django.shortcuts import get_object_or_404 from rest_framework import permissions, status from rest_framework.response import Response from rest_framework.views import APIView from .game_services import game_payload, request_sudoku_hint, start_game, submit_game from .models import Contest, ContestAttempt, MathGameAttempt, RealtimeMatch from .services import ( cancel_waiting_match, create_challenge, finalize_game_match, find_match, join_challenge, match_payload, notify_match_on_commit, refresh_match_state, start_attempt, submit_attempt, ) class ContestListView(APIView): permission_classes = [permissions.AllowAny] def get(self, request): contests = Contest.objects.filter(status=Contest.Status.PUBLISHED).order_by( "kind", "title" ) return Response( [ { "slug": contest.slug, "title": contest.title, "kind": contest.kind, "track": contest.track, "duration_seconds": contest.duration_seconds, "starts_at": contest.starts_at, "ends_at": contest.ends_at, } for contest in contests ] ) class AttemptStartView(APIView): def post(self, request, slug): contest = get_object_or_404(Contest, slug=slug) return Response(start_attempt(request.user, contest), status=status.HTTP_201_CREATED) class AttemptSubmitView(APIView): def post(self, request, attempt_id): attempt = get_object_or_404(ContestAttempt, id=attempt_id, user=request.user) payload = submit_attempt( user=request.user, attempt_id=attempt_id, raw_answers=request.data.get("answers", []), submission_key=request.headers.get("Idempotency-Key"), ) if attempt.match_id: match = refresh_match_state(attempt.match_id) return Response(match_payload(match, request.user)) return Response(payload) class MatchmakingView(APIView): def post(self, request, slug): contest = get_object_or_404(Contest, slug=slug) match = find_match( request.user, contest, request.data.get("game_kind", RealtimeMatch.GameKind.QUIZ), ) return Response(match_payload(match, request.user), status=status.HTTP_202_ACCEPTED) class MatchStateView(APIView): def get(self, request, match_id): existing = get_object_or_404( RealtimeMatch.objects.select_related("player_one", "player_two"), id=match_id, ) if request.user.id not in (existing.player_one_id, existing.player_two_id): return Response(status=status.HTTP_403_FORBIDDEN) match = refresh_match_state(match_id) return Response(match_payload(match, request.user)) class ChallengeCreateView(APIView): def post(self, request, slug): contest = get_object_or_404(Contest, slug=slug) match = create_challenge( request.user, contest, request.data.get("game_kind", RealtimeMatch.GameKind.QUIZ), ) return Response(match_payload(match, request.user), status=status.HTTP_201_CREATED) class ChallengeJoinView(APIView): def post(self, request): match = join_challenge(request.user, request.data.get("challenge_code")) return Response(match_payload(match, request.user)) class MatchCancelView(APIView): def post(self, request, match_id): get_object_or_404(RealtimeMatch, id=match_id) match = cancel_waiting_match(request.user, match_id) return Response(match_payload(match, request.user)) class LeaderboardView(APIView): permission_classes = [permissions.AllowAny] def get(self, request, slug): contest = get_object_or_404(Contest, slug=slug) attempts = ( ContestAttempt.objects.filter( contest=contest, status=ContestAttempt.Status.SUBMITTED, cheat_flags__isnull=True, ) .select_related("user") .order_by("-score", "duration_ms", "submitted_at")[:100] ) return Response( [ { "rank": index, "nickname": attempt.user.nickname, "track": attempt.user.track, "score": attempt.score, "correct_count": attempt.correct_count, "duration_ms": attempt.duration_ms, } for index, attempt in enumerate(attempts, start=1) ] ) class MathGameCatalogView(APIView): permission_classes = [permissions.AllowAny] def get(self, request): return Response( [ { "kind": MathGameAttempt.Kind.TWENTY_FOUR, "title": "24 点", "summary": "四个数字各用一次,只用四则运算得到 24。", "ability": "connection", "estimated_minutes": 3, }, { "kind": MathGameAttempt.Kind.SUDOKU, "title": "数独", "summary": "在行、列和九宫格约束中完成 9×9 数字推理。", "ability": "detection", "estimated_minutes": 8, }, ] ) class MathGameStartView(APIView): def post(self, request, kind): payload = start_game( request.user, kind, MathGameAttempt.Difficulty.STANDARD, ) return Response(payload, status=status.HTTP_201_CREATED) class MathGameSubmitView(APIView): def post(self, request, attempt_id): attempt = get_object_or_404( MathGameAttempt, id=attempt_id, user=request.user, ) payload = submit_game( request.user, attempt_id, request.data, request.headers.get("Idempotency-Key"), ) if attempt.match_id: match = finalize_game_match(attempt.match_id) if match.status != RealtimeMatch.Status.COMPLETED: notify_match_on_commit(match.id, "submitted") return Response(match_payload(match, request.user)) return Response(payload) class SudokuHintView(APIView): def post(self, request, attempt_id): return Response(request_sudoku_hint(request.user, attempt_id)) class MathGameHistoryView(APIView): def get(self, request): attempts = MathGameAttempt.objects.filter(user=request.user)[:20] return Response([game_payload(attempt) for attempt in attempts])