186 lines
6.1 KiB
Python
186 lines
6.1 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,
|
|
find_match,
|
|
join_challenge,
|
|
match_payload,
|
|
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)
|
|
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)
|
|
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,
|
|
request.data.get("difficulty", MathGameAttempt.Difficulty.STANDARD),
|
|
)
|
|
return Response(payload, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class MathGameSubmitView(APIView):
|
|
def post(self, request, attempt_id):
|
|
payload = submit_game(
|
|
request.user,
|
|
attempt_id,
|
|
request.data,
|
|
request.headers.get("Idempotency-Key"),
|
|
)
|
|
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])
|