Files

206 lines
6.7 KiB
Python

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])