201 lines
6.0 KiB
Python
201 lines
6.0 KiB
Python
import json
|
|
import random
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.db import transaction
|
|
from django.http import HttpRequest, JsonResponse
|
|
from django.views.decorators.http import require_GET, require_POST
|
|
|
|
from fupogfakta.models import Category, GameSession, Player, Question, RoundConfig
|
|
|
|
SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
SESSION_CODE_LENGTH = 6
|
|
MAX_CODE_GENERATION_ATTEMPTS = 20
|
|
JOINABLE_STATUSES = {
|
|
GameSession.Status.LOBBY,
|
|
GameSession.Status.LIE,
|
|
GameSession.Status.GUESS,
|
|
GameSession.Status.REVEAL,
|
|
}
|
|
|
|
|
|
def _json_body(request: HttpRequest) -> dict:
|
|
if not request.body:
|
|
return {}
|
|
|
|
try:
|
|
return json.loads(request.body)
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
|
|
|
|
def _generate_session_code() -> str:
|
|
return "".join(random.choices(SESSION_CODE_ALPHABET, k=SESSION_CODE_LENGTH))
|
|
|
|
|
|
def _create_unique_session_code() -> str:
|
|
for _ in range(MAX_CODE_GENERATION_ATTEMPTS):
|
|
code = _generate_session_code()
|
|
if not GameSession.objects.filter(code=code).exists():
|
|
return code
|
|
|
|
raise RuntimeError("Could not generate unique session code")
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def create_session(request: HttpRequest) -> JsonResponse:
|
|
code = _create_unique_session_code()
|
|
session = GameSession.objects.create(host=request.user, code=code)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"session": {
|
|
"code": session.code,
|
|
"status": session.status,
|
|
"host_id": session.host_id,
|
|
"current_round": session.current_round,
|
|
}
|
|
},
|
|
status=201,
|
|
)
|
|
|
|
|
|
@require_POST
|
|
def join_session(request: HttpRequest) -> JsonResponse:
|
|
payload = _json_body(request)
|
|
|
|
code = str(payload.get("code", "")).strip().upper()
|
|
nickname = str(payload.get("nickname", "")).strip()
|
|
|
|
if not code:
|
|
return JsonResponse({"error": "Session code is required"}, status=400)
|
|
|
|
if len(nickname) < 2 or len(nickname) > 40:
|
|
return JsonResponse({"error": "Nickname must be between 2 and 40 characters"}, status=400)
|
|
|
|
try:
|
|
session = GameSession.objects.get(code=code)
|
|
except GameSession.DoesNotExist:
|
|
return JsonResponse({"error": "Session not found"}, status=404)
|
|
|
|
if session.status not in JOINABLE_STATUSES:
|
|
return JsonResponse({"error": "Session is not joinable"}, status=400)
|
|
|
|
if Player.objects.filter(session=session, nickname__iexact=nickname).exists():
|
|
return JsonResponse({"error": "Nickname already taken"}, status=409)
|
|
|
|
player = Player.objects.create(session=session, nickname=nickname)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"player": {
|
|
"id": player.id,
|
|
"nickname": player.nickname,
|
|
"score": player.score,
|
|
},
|
|
"session": {
|
|
"code": session.code,
|
|
"status": session.status,
|
|
},
|
|
},
|
|
status=201,
|
|
)
|
|
|
|
|
|
@require_GET
|
|
def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
|
session_code = code.strip().upper()
|
|
|
|
try:
|
|
session = GameSession.objects.get(code=session_code)
|
|
except GameSession.DoesNotExist:
|
|
return JsonResponse({"error": "Session not found"}, status=404)
|
|
|
|
players = list(
|
|
session.players.order_by("nickname").values(
|
|
"id",
|
|
"nickname",
|
|
"score",
|
|
"is_connected",
|
|
)
|
|
)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"session": {
|
|
"code": session.code,
|
|
"status": session.status,
|
|
"host_id": session.host_id,
|
|
"current_round": session.current_round,
|
|
"players_count": len(players),
|
|
},
|
|
"players": players,
|
|
}
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|
payload = _json_body(request)
|
|
category_slug = str(payload.get("category_slug", "")).strip()
|
|
|
|
if not category_slug:
|
|
return JsonResponse({"error": "category_slug is required"}, status=400)
|
|
|
|
session_code = code.strip().upper()
|
|
|
|
try:
|
|
session = GameSession.objects.get(code=session_code)
|
|
except GameSession.DoesNotExist:
|
|
return JsonResponse({"error": "Session not found"}, status=404)
|
|
|
|
if session.host_id != request.user.id:
|
|
return JsonResponse({"error": "Only host can start round"}, status=403)
|
|
|
|
if session.status != GameSession.Status.LOBBY:
|
|
return JsonResponse({"error": "Round can only be started from lobby"}, status=400)
|
|
|
|
try:
|
|
category = Category.objects.get(slug=category_slug, is_active=True)
|
|
except Category.DoesNotExist:
|
|
return JsonResponse({"error": "Category not found"}, status=404)
|
|
|
|
if not Question.objects.filter(category=category, is_active=True).exists():
|
|
return JsonResponse({"error": "Category has no active questions"}, status=400)
|
|
|
|
with transaction.atomic():
|
|
session = GameSession.objects.select_for_update().get(pk=session.pk)
|
|
if session.status != GameSession.Status.LOBBY:
|
|
return JsonResponse({"error": "Round can only be started from lobby"}, status=400)
|
|
|
|
round_config, created = RoundConfig.objects.get_or_create(
|
|
session=session,
|
|
number=session.current_round,
|
|
defaults={"category": category},
|
|
)
|
|
if not created:
|
|
return JsonResponse({"error": "Round already configured"}, status=409)
|
|
|
|
session.status = GameSession.Status.LIE
|
|
session.save(update_fields=["status"])
|
|
|
|
return JsonResponse(
|
|
{
|
|
"session": {
|
|
"code": session.code,
|
|
"status": session.status,
|
|
"current_round": session.current_round,
|
|
},
|
|
"round": {
|
|
"number": round_config.number,
|
|
"category": {
|
|
"slug": round_config.category.slug,
|
|
"name": round_config.category.name,
|
|
},
|
|
},
|
|
},
|
|
status=201,
|
|
)
|