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