Merge main into PR #291 and resolve scoreboard phase conflicts
This commit is contained in:
237
lobby/views.py
237
lobby/views.py
@@ -21,7 +21,9 @@ from fupogfakta.models import (
|
||||
)
|
||||
from realtime.broadcast import sync_broadcast_phase_event
|
||||
|
||||
from .i18n import api_error, lobby_i18n_errors
|
||||
from realtime.broadcast import sync_broadcast_phase_event
|
||||
|
||||
from .i18n import api_error
|
||||
|
||||
SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
SESSION_CODE_LENGTH = 6
|
||||
@@ -33,7 +35,7 @@ JOINABLE_STATUSES = {
|
||||
GameSession.Status.REVEAL,
|
||||
GameSession.Status.SCOREBOARD,
|
||||
}
|
||||
ERROR_CODES = lobby_i18n_errors()
|
||||
|
||||
|
||||
|
||||
def _json_body(request: HttpRequest) -> dict:
|
||||
@@ -132,14 +134,14 @@ def join_session(request: HttpRequest) -> JsonResponse:
|
||||
if not code:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_code_required", "session_code_required"),
|
||||
code="session_code_required",
|
||||
status=400,
|
||||
)
|
||||
|
||||
if len(nickname) < 2 or len(nickname) > 40:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("nickname_invalid", "nickname_invalid"),
|
||||
code="nickname_invalid",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -148,21 +150,21 @@ def join_session(request: HttpRequest) -> JsonResponse:
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
code="session_not_found",
|
||||
status=404,
|
||||
)
|
||||
|
||||
if session.status not in JOINABLE_STATUSES:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_joinable", "session_not_joinable"),
|
||||
code="session_not_joinable",
|
||||
status=400,
|
||||
)
|
||||
|
||||
if Player.objects.filter(session=session, nickname__iexact=nickname).exists():
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("nickname_taken", "nickname_taken"),
|
||||
code="nickname_taken",
|
||||
status=409,
|
||||
)
|
||||
|
||||
@@ -194,7 +196,7 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
code="session_not_found",
|
||||
status=404,
|
||||
)
|
||||
|
||||
@@ -255,7 +257,7 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
if not category_slug:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("category_slug_required", "category_slug_required"),
|
||||
code="category_slug_required",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -266,21 +268,21 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
code="session_not_found",
|
||||
status=404,
|
||||
)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_start_round", "host_only_start_round"),
|
||||
code="host_only_start_round",
|
||||
status=403,
|
||||
)
|
||||
|
||||
if session.status != GameSession.Status.LOBBY:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("round_start_invalid_phase", "round_start_invalid_phase"),
|
||||
code="round_start_invalid_phase",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -289,14 +291,14 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
except Category.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("category_not_found", "category_not_found"),
|
||||
code="category_not_found",
|
||||
status=404,
|
||||
)
|
||||
|
||||
if not Question.objects.filter(category=category, is_active=True).exists():
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("category_has_no_questions", "category_has_no_questions"),
|
||||
code="category_has_no_questions",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -305,7 +307,7 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
if session.status != GameSession.Status.LOBBY:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("round_start_invalid_phase", "round_start_invalid_phase"),
|
||||
code="round_start_invalid_phase",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -317,13 +319,23 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
if not created:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("round_already_configured", "round_already_configured"),
|
||||
code="round_already_configured",
|
||||
status=409,
|
||||
)
|
||||
|
||||
session.status = GameSession.Status.LIE
|
||||
session.save(update_fields=["status"])
|
||||
|
||||
sync_broadcast_phase_event(
|
||||
session.code,
|
||||
"phase.lie_started",
|
||||
{
|
||||
"round_number": session.current_round,
|
||||
"category": {"slug": round_config.category.slug, "name": round_config.category.name},
|
||||
"lie_seconds": round_config.lie_seconds,
|
||||
},
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"session": {
|
||||
@@ -353,21 +365,21 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
code="session_not_found",
|
||||
status=404,
|
||||
)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_show_question", "host_only_show_question"),
|
||||
code="host_only_show_question",
|
||||
status=403,
|
||||
)
|
||||
|
||||
if session.status != GameSession.Status.LIE:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("show_question_invalid_phase", "show_question_invalid_phase"),
|
||||
code="show_question_invalid_phase",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -376,14 +388,14 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
||||
except RoundConfig.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("round_config_missing", "round_config_missing"),
|
||||
code="round_config_missing",
|
||||
status=400,
|
||||
)
|
||||
|
||||
if RoundQuestion.objects.filter(session=session, round_number=session.current_round).exists():
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("question_already_shown", "question_already_shown"),
|
||||
code="question_already_shown",
|
||||
status=409,
|
||||
)
|
||||
|
||||
@@ -396,7 +408,7 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
||||
if not available_questions.exists():
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("no_available_questions", "no_available_questions"),
|
||||
code="no_available_questions",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -410,6 +422,18 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
||||
|
||||
lie_deadline_at = round_question.shown_at + timedelta(seconds=round_config.lie_seconds)
|
||||
|
||||
sync_broadcast_phase_event(
|
||||
session.code,
|
||||
"phase.question_shown",
|
||||
{
|
||||
"round_question_id": round_question.id,
|
||||
"prompt": question.prompt,
|
||||
"shown_at": round_question.shown_at.isoformat(),
|
||||
"lie_deadline_at": lie_deadline_at.isoformat(),
|
||||
"lie_seconds": round_config.lie_seconds,
|
||||
},
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"round_question": {
|
||||
@@ -437,29 +461,29 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
|
||||
lie_text = str(payload.get("text", "")).strip()
|
||||
|
||||
if not player_id:
|
||||
return JsonResponse({"error": "player_id is required"}, status=400)
|
||||
return api_error(request, code="player_id_required", status=400)
|
||||
|
||||
if not session_token:
|
||||
return JsonResponse({"error": "session_token is required"}, status=400)
|
||||
return api_error(request, code="session_token_required", status=400)
|
||||
|
||||
if not lie_text or len(lie_text) > 255:
|
||||
return JsonResponse({"error": "text must be between 1 and 255 characters"}, status=400)
|
||||
return api_error(request, code="lie_text_invalid", status=400)
|
||||
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return JsonResponse({"error": "Session not found"}, status=404)
|
||||
return api_error(request, code="session_not_found", status=404)
|
||||
|
||||
if session.status != GameSession.Status.LIE:
|
||||
return JsonResponse({"error": "Lie submission is only allowed in lie phase"}, status=400)
|
||||
return api_error(request, code="lie_submission_invalid_phase", status=400)
|
||||
|
||||
try:
|
||||
player = Player.objects.get(pk=player_id, session=session)
|
||||
except Player.DoesNotExist:
|
||||
return JsonResponse({"error": "Player not found in session"}, status=404)
|
||||
return api_error(request, code="player_not_found_in_session", status=404)
|
||||
|
||||
if player.session_token != session_token:
|
||||
return JsonResponse({"error": "Invalid player session token"}, status=403)
|
||||
return api_error(request, code="invalid_player_session_token", status=403)
|
||||
|
||||
try:
|
||||
round_question = RoundQuestion.objects.get(
|
||||
@@ -468,21 +492,21 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
|
||||
round_number=session.current_round,
|
||||
)
|
||||
except RoundQuestion.DoesNotExist:
|
||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
||||
return api_error(request, code="round_question_not_found", status=404)
|
||||
|
||||
try:
|
||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||
except RoundConfig.DoesNotExist:
|
||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
||||
return api_error(request, code="round_config_missing", status=400)
|
||||
|
||||
lie_deadline_at = round_question.shown_at + timedelta(seconds=round_config.lie_seconds)
|
||||
if timezone.now() > lie_deadline_at:
|
||||
return JsonResponse({"error": "Lie submission window has closed"}, status=400)
|
||||
return api_error(request, code="lie_submission_closed", status=400)
|
||||
|
||||
try:
|
||||
lie = LieAnswer.objects.create(round_question=round_question, player=player, text=lie_text)
|
||||
except IntegrityError:
|
||||
return JsonResponse({"error": "Lie already submitted for this player"}, status=409)
|
||||
return api_error(request, code="lie_already_submitted", status=409)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
@@ -510,21 +534,21 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
code="session_not_found",
|
||||
status=404,
|
||||
)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_mix_answers", "host_only_mix_answers"),
|
||||
code="host_only_mix_answers",
|
||||
status=403,
|
||||
)
|
||||
|
||||
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
||||
code="mix_answers_invalid_phase",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -537,7 +561,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
except RoundQuestion.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("round_question_not_found", "round_question_not_found"),
|
||||
code="round_question_not_found",
|
||||
status=404,
|
||||
)
|
||||
|
||||
@@ -546,7 +570,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
if locked_session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
||||
code="mix_answers_invalid_phase",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -566,7 +590,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
if len(deduped_answers) < 2:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("not_enough_answers_to_mix", "not_enough_answers_to_mix"),
|
||||
code="not_enough_answers_to_mix",
|
||||
status=400,
|
||||
)
|
||||
|
||||
@@ -578,6 +602,22 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
locked_session.status = GameSession.Status.GUESS
|
||||
locked_session.save(update_fields=["status"])
|
||||
|
||||
try:
|
||||
_guess_config = RoundConfig.objects.get(session=session, number=session.current_round)
|
||||
_guess_seconds = _guess_config.guess_seconds
|
||||
except RoundConfig.DoesNotExist:
|
||||
_guess_seconds = None
|
||||
|
||||
sync_broadcast_phase_event(
|
||||
session.code,
|
||||
"phase.guess_started",
|
||||
{
|
||||
"round_question_id": round_question.id,
|
||||
"answers": [{"text": t} for t in deduped_answers],
|
||||
"guess_seconds": _guess_seconds,
|
||||
},
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"session": {
|
||||
@@ -604,29 +644,29 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
||||
selected_text = str(payload.get("selected_text", "")).strip()
|
||||
|
||||
if not player_id:
|
||||
return JsonResponse({"error": "player_id is required"}, status=400)
|
||||
return api_error(request, code="player_id_required", status=400)
|
||||
|
||||
if not session_token:
|
||||
return JsonResponse({"error": "session_token is required"}, status=400)
|
||||
return api_error(request, code="session_token_required", status=400)
|
||||
|
||||
if not selected_text or len(selected_text) > 255:
|
||||
return JsonResponse({"error": "selected_text must be between 1 and 255 characters"}, status=400)
|
||||
return api_error(request, code="selected_text_invalid", status=400)
|
||||
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return JsonResponse({"error": "Session not found"}, status=404)
|
||||
return api_error(request, code="session_not_found", status=404)
|
||||
|
||||
if session.status != GameSession.Status.GUESS:
|
||||
return JsonResponse({"error": "Guess submission is only allowed in guess phase"}, status=400)
|
||||
return api_error(request, code="guess_submission_invalid_phase", status=400)
|
||||
|
||||
try:
|
||||
player = Player.objects.get(pk=player_id, session=session)
|
||||
except Player.DoesNotExist:
|
||||
return JsonResponse({"error": "Player not found in session"}, status=404)
|
||||
return api_error(request, code="player_not_found_in_session", status=404)
|
||||
|
||||
if player.session_token != session_token:
|
||||
return JsonResponse({"error": "Invalid player session token"}, status=403)
|
||||
return api_error(request, code="invalid_player_session_token", status=403)
|
||||
|
||||
try:
|
||||
round_question = RoundQuestion.objects.get(
|
||||
@@ -635,18 +675,18 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
||||
round_number=session.current_round,
|
||||
)
|
||||
except RoundQuestion.DoesNotExist:
|
||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
||||
return api_error(request, code="round_question_not_found", status=404)
|
||||
|
||||
try:
|
||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||
except RoundConfig.DoesNotExist:
|
||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
||||
return api_error(request, code="round_config_missing", status=400)
|
||||
|
||||
guess_deadline_at = round_question.shown_at + timedelta(
|
||||
seconds=round_config.lie_seconds + round_config.guess_seconds
|
||||
)
|
||||
if timezone.now() > guess_deadline_at:
|
||||
return JsonResponse({"error": "Guess submission window has closed"}, status=400)
|
||||
return api_error(request, code="guess_submission_closed", status=400)
|
||||
|
||||
allowed_answers = {
|
||||
round_question.correct_answer.strip().casefold(),
|
||||
@@ -659,7 +699,7 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
||||
|
||||
selected_normalized = selected_text.casefold()
|
||||
if selected_normalized not in allowed_answers:
|
||||
return JsonResponse({"error": "Selected answer is not part of this round"}, status=400)
|
||||
return api_error(request, code="selected_answer_invalid", status=400)
|
||||
|
||||
correct_normalized = round_question.correct_answer.strip().casefold()
|
||||
fooled_player_id = None
|
||||
@@ -677,7 +717,7 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
||||
fooled_player_id=fooled_player_id,
|
||||
)
|
||||
except IntegrityError:
|
||||
return JsonResponse({"error": "Guess already submitted for this player"}, status=409)
|
||||
return api_error(request, code="guess_already_submitted", status=409)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
@@ -708,33 +748,22 @@ def reveal_scoreboard(request: HttpRequest, code: str) -> JsonResponse:
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
status=404,
|
||||
)
|
||||
return api_error(request, code="session_not_found", status=404)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_view_scoreboard", "host_only_view_scoreboard"),
|
||||
status=403,
|
||||
)
|
||||
return api_error(request, code="host_only_view_scoreboard", status=403)
|
||||
|
||||
with transaction.atomic():
|
||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||
if locked_session.status not in {GameSession.Status.REVEAL, GameSession.Status.SCOREBOARD}:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("scoreboard_invalid_phase", "scoreboard_invalid_phase"),
|
||||
status=400,
|
||||
)
|
||||
return api_error(request, code="scoreboard_invalid_phase", status=400)
|
||||
|
||||
promoted_to_scoreboard = locked_session.status == GameSession.Status.REVEAL
|
||||
if promoted_to_scoreboard:
|
||||
locked_session.status = GameSession.Status.SCOREBOARD
|
||||
locked_session.save(update_fields=["status"])
|
||||
|
||||
|
||||
leaderboard = list(
|
||||
Player.objects.filter(session=session)
|
||||
.order_by("-score", "nickname")
|
||||
@@ -748,6 +777,7 @@ def reveal_scoreboard(request: HttpRequest, code: str) -> JsonResponse:
|
||||
{"leaderboard": list(leaderboard), "current_round": locked_session.current_round},
|
||||
)
|
||||
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"session": {
|
||||
@@ -768,27 +798,16 @@ def start_next_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
status=404,
|
||||
)
|
||||
return api_error(request, code="session_not_found", status=404)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_start_next_round", "host_only_start_next_round"),
|
||||
status=403,
|
||||
)
|
||||
return api_error(request, code="host_only_start_next_round", status=403)
|
||||
|
||||
with transaction.atomic():
|
||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||
if locked_session.status != GameSession.Status.SCOREBOARD:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("next_round_invalid_phase", "next_round_invalid_phase"),
|
||||
status=400,
|
||||
)
|
||||
return api_error(request, code="next_round_invalid_phase", status=400)
|
||||
|
||||
|
||||
locked_session.current_round += 1
|
||||
locked_session.status = GameSession.Status.LOBBY
|
||||
@@ -812,27 +831,16 @@ def finish_game(request: HttpRequest, code: str) -> JsonResponse:
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
status=404,
|
||||
)
|
||||
return api_error(request, code="session_not_found", status=404)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_finish_game", "host_only_finish_game"),
|
||||
status=403,
|
||||
)
|
||||
return api_error(request, code="host_only_finish_game", status=403)
|
||||
|
||||
with transaction.atomic():
|
||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||
if locked_session.status != GameSession.Status.SCOREBOARD:
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("finish_game_invalid_phase", "finish_game_invalid_phase"),
|
||||
status=400,
|
||||
)
|
||||
return api_error(request, code="finish_game_invalid_phase", status=400)
|
||||
|
||||
|
||||
locked_session.status = GameSession.Status.FINISHED
|
||||
locked_session.save(update_fields=["status"])
|
||||
@@ -845,6 +853,12 @@ def finish_game(request: HttpRequest, code: str) -> JsonResponse:
|
||||
|
||||
winner = leaderboard[0] if leaderboard else None
|
||||
|
||||
sync_broadcast_phase_event(
|
||||
session.code,
|
||||
"phase.game_over",
|
||||
{"winner": winner, "leaderboard": list(leaderboard)},
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"session": {
|
||||
@@ -866,20 +880,20 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return JsonResponse({"error": "Session not found"}, status=404)
|
||||
return api_error(request, code="session_not_found", status=404)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return JsonResponse({"error": "Only host can calculate scores"}, status=403)
|
||||
return api_error(request, code="host_only_calculate_scores", status=403)
|
||||
|
||||
already_calculated = ScoreEvent.objects.filter(
|
||||
session=session,
|
||||
meta__round_question_id=round_question_id,
|
||||
).exists()
|
||||
if already_calculated:
|
||||
return JsonResponse({"error": "Scores already calculated for this round question"}, status=409)
|
||||
return api_error(request, code="scores_already_calculated", status=409)
|
||||
|
||||
if session.status != GameSession.Status.GUESS:
|
||||
return JsonResponse({"error": "Scores can only be calculated in guess phase"}, status=400)
|
||||
return api_error(request, code="calculate_scores_invalid_phase", status=400)
|
||||
|
||||
try:
|
||||
round_question = RoundQuestion.objects.get(
|
||||
@@ -888,16 +902,16 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
||||
round_number=session.current_round,
|
||||
)
|
||||
except RoundQuestion.DoesNotExist:
|
||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
||||
return api_error(request, code="round_question_not_found", status=404)
|
||||
|
||||
try:
|
||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||
except RoundConfig.DoesNotExist:
|
||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
||||
return api_error(request, code="round_config_missing", status=400)
|
||||
|
||||
guesses = list(round_question.guesses.select_related("player"))
|
||||
if not guesses:
|
||||
return JsonResponse({"error": "No guesses submitted for this round question"}, status=400)
|
||||
return api_error(request, code="no_guesses_submitted", status=400)
|
||||
|
||||
bluff_counts = {}
|
||||
for guess in guesses:
|
||||
@@ -907,7 +921,7 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
||||
with transaction.atomic():
|
||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||
if locked_session.status != GameSession.Status.GUESS:
|
||||
return JsonResponse({"error": "Scores can only be calculated in guess phase"}, status=400)
|
||||
return api_error(request, code="calculate_scores_invalid_phase", status=400)
|
||||
|
||||
score_events = []
|
||||
|
||||
@@ -951,6 +965,21 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
||||
.values("id", "nickname", "score")
|
||||
)
|
||||
|
||||
score_deltas = [
|
||||
{"player_id": ev.player_id, "delta": ev.delta, "reason": ev.reason}
|
||||
for ev in score_events
|
||||
]
|
||||
|
||||
sync_broadcast_phase_event(
|
||||
session.code,
|
||||
"phase.scores_calculated",
|
||||
{
|
||||
"round_question_id": round_question.id,
|
||||
"score_deltas": score_deltas,
|
||||
"leaderboard": list(leaderboard),
|
||||
},
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"session": {
|
||||
|
||||
Reference in New Issue
Block a user