177 lines
6.0 KiB
Python
177 lines
6.0 KiB
Python
import json
|
|
import random
|
|
from datetime import timedelta
|
|
|
|
from asgiref.sync import async_to_sync
|
|
from channels.layers import get_channel_layer
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.db import transaction
|
|
from django.http import HttpRequest, JsonResponse
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from .models import GameSession, LieAnswer, Player, Question, RoundConfig, RoundQuestion
|
|
|
|
|
|
MAX_LIE_LENGTH = 255
|
|
|
|
|
|
def _json_body(request: HttpRequest) -> dict:
|
|
if not request.body:
|
|
return {}
|
|
|
|
try:
|
|
return json.loads(request.body)
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
|
|
|
|
def _emit_realtime_session_event(session_code: str, event: str, payload: dict) -> None:
|
|
channel_layer = get_channel_layer()
|
|
if not channel_layer:
|
|
return
|
|
|
|
try:
|
|
async_to_sync(channel_layer.group_send)(
|
|
f"session_{session_code}",
|
|
{
|
|
"type": "session.event",
|
|
"event": event,
|
|
"payload": payload,
|
|
},
|
|
)
|
|
except Exception:
|
|
# Realtime broadcasting must not fail game flow.
|
|
return
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
def start_lie_question(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)
|
|
|
|
if session.host_id != request.user.id:
|
|
return JsonResponse({"error": "Only host can start lie question"}, status=403)
|
|
|
|
if session.status != GameSession.Status.LIE:
|
|
return JsonResponse({"error": "Session is not in lie phase"}, status=400)
|
|
|
|
with transaction.atomic():
|
|
session = GameSession.objects.select_for_update().get(pk=session.pk)
|
|
if session.status != GameSession.Status.LIE:
|
|
return JsonResponse({"error": "Session is not in lie phase"}, status=400)
|
|
|
|
if RoundQuestion.objects.filter(session=session, round_number=session.current_round).exists():
|
|
return JsonResponse({"error": "Lie question already started for round"}, status=409)
|
|
|
|
try:
|
|
round_config = RoundConfig.objects.select_related("category").get(
|
|
session=session,
|
|
number=session.current_round,
|
|
)
|
|
except RoundConfig.DoesNotExist:
|
|
return JsonResponse({"error": "Round config not found"}, status=404)
|
|
|
|
question_qs = Question.objects.filter(category=round_config.category, is_active=True)
|
|
question_ids = list(question_qs.values_list("id", flat=True))
|
|
if not question_ids:
|
|
return JsonResponse({"error": "No active question for category"}, status=400)
|
|
|
|
question = question_qs.get(pk=random.choice(question_ids))
|
|
|
|
lie_deadline_at = timezone.now() + timedelta(seconds=round_config.lie_seconds)
|
|
round_question = RoundQuestion.objects.create(
|
|
session=session,
|
|
round_number=session.current_round,
|
|
question=question,
|
|
correct_answer=question.correct_answer,
|
|
lie_deadline_at=lie_deadline_at,
|
|
)
|
|
|
|
payload = {
|
|
"round": round_question.round_number,
|
|
"question_id": round_question.id,
|
|
"prompt": round_question.question.prompt,
|
|
"lie_deadline_at": round_question.lie_deadline_at.isoformat(),
|
|
"lie_seconds": round_config.lie_seconds,
|
|
}
|
|
_emit_realtime_session_event(session.code, "lie_question_started", payload)
|
|
|
|
return JsonResponse({"question": payload}, status=201)
|
|
|
|
|
|
@require_POST
|
|
def submit_lie(request: HttpRequest, code: str) -> JsonResponse:
|
|
session_code = code.strip().upper()
|
|
payload = _json_body(request)
|
|
|
|
player_id = payload.get("player_id")
|
|
lie_text = str(payload.get("text", "")).strip()
|
|
|
|
if not player_id:
|
|
return JsonResponse({"error": "player_id is required"}, status=400)
|
|
|
|
if len(lie_text) < 2 or len(lie_text) > MAX_LIE_LENGTH:
|
|
return JsonResponse({"error": "text must be between 2 and 255 characters"}, status=400)
|
|
|
|
try:
|
|
session = GameSession.objects.get(code=session_code)
|
|
except GameSession.DoesNotExist:
|
|
return JsonResponse({"error": "Session not found"}, status=404)
|
|
|
|
if session.status != GameSession.Status.LIE:
|
|
return JsonResponse({"error": "Session is not in lie phase"}, status=400)
|
|
|
|
try:
|
|
player = Player.objects.get(id=player_id, session=session)
|
|
except Player.DoesNotExist:
|
|
return JsonResponse({"error": "Player not found in session"}, status=404)
|
|
|
|
try:
|
|
round_question = RoundQuestion.objects.get(session=session, round_number=session.current_round)
|
|
except RoundQuestion.DoesNotExist:
|
|
return JsonResponse({"error": "No active lie question"}, status=404)
|
|
|
|
now = timezone.now()
|
|
if round_question.lie_deadline_at and now > round_question.lie_deadline_at:
|
|
return JsonResponse({"error": "Lie timer expired"}, status=400)
|
|
|
|
lie_answer, _ = LieAnswer.objects.update_or_create(
|
|
round_question=round_question,
|
|
player=player,
|
|
defaults={"text": lie_text},
|
|
)
|
|
|
|
submitted_count = LieAnswer.objects.filter(round_question=round_question).count()
|
|
players_count = session.players.count()
|
|
|
|
event_payload = {
|
|
"round": session.current_round,
|
|
"question_id": round_question.id,
|
|
"player_id": player.id,
|
|
"submitted_count": submitted_count,
|
|
"players_count": players_count,
|
|
"lie_deadline_at": round_question.lie_deadline_at.isoformat() if round_question.lie_deadline_at else None,
|
|
}
|
|
_emit_realtime_session_event(session.code, "lie_submitted", event_payload)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"lie": {
|
|
"id": lie_answer.id,
|
|
"player_id": player.id,
|
|
"question_id": round_question.id,
|
|
},
|
|
"progress": {
|
|
"submitted_count": submitted_count,
|
|
"players_count": players_count,
|
|
},
|
|
},
|
|
status=201,
|
|
)
|