fix(gameplay): restore scoreboard phase error contract
This commit is contained in:
@@ -782,7 +782,8 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
self.player_one = Player.objects.create(session=self.session, nickname="Luna", score=9)
|
self.player_one = Player.objects.create(session=self.session, nickname="Luna", score=9)
|
||||||
self.player_two = Player.objects.create(session=self.session, nickname="Mads", score=3)
|
self.player_two = Player.objects.create(session=self.session, nickname="Mads", score=3)
|
||||||
|
|
||||||
def test_host_can_get_reveal_scoreboard(self):
|
@patch("lobby.views.sync_broadcast_phase_event")
|
||||||
|
def test_host_can_get_reveal_scoreboard(self, mock_sync_broadcast_phase_event):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -796,6 +797,17 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
payload = response.json()
|
payload = response.json()
|
||||||
self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||||
self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"])
|
self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"])
|
||||||
|
mock_sync_broadcast_phase_event.assert_called_once_with(
|
||||||
|
self.session.code,
|
||||||
|
"phase.scoreboard",
|
||||||
|
{
|
||||||
|
"leaderboard": [
|
||||||
|
{"id": self.player_one.id, "nickname": "Luna", "score": 9},
|
||||||
|
{"id": self.player_two.id, "nickname": "Mads", "score": 3},
|
||||||
|
],
|
||||||
|
"current_round": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.session.refresh_from_db()
|
self.session.refresh_from_db()
|
||||||
self.assertEqual(self.session.status, GameSession.Status.SCOREBOARD)
|
self.assertEqual(self.session.status, GameSession.Status.SCOREBOARD)
|
||||||
@@ -807,11 +819,16 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"lobby:reveal_scoreboard",
|
"lobby:reveal_scoreboard",
|
||||||
kwargs={"code": self.session.code},
|
kwargs={"code": self.session.code},
|
||||||
)
|
),
|
||||||
|
HTTP_ACCEPT_LANGUAGE="fr",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
self.assertEqual(response.json()["error"], "Only host can view scoreboard")
|
self.assertEqual(response.json(), {
|
||||||
|
"error": "Only host can view scoreboard",
|
||||||
|
"error_code": "host_only_view_scoreboard",
|
||||||
|
"locale": "en",
|
||||||
|
})
|
||||||
|
|
||||||
def test_reveal_scoreboard_is_idempotent_in_scoreboard_phase(self):
|
def test_reveal_scoreboard_is_idempotent_in_scoreboard_phase(self):
|
||||||
self.session.status = GameSession.Status.SCOREBOARD
|
self.session.status = GameSession.Status.SCOREBOARD
|
||||||
@@ -825,7 +842,8 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||||
self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"])
|
self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"])
|
||||||
|
|
||||||
def test_host_can_finish_game_from_scoreboard(self):
|
@patch("lobby.views.sync_broadcast_phase_event")
|
||||||
|
def test_host_can_finish_game_from_scoreboard(self, _mock_sync_broadcast_phase_event):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code}))
|
self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code}))
|
||||||
|
|
||||||
@@ -852,11 +870,16 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"lobby:finish_game",
|
"lobby:finish_game",
|
||||||
kwargs={"code": self.session.code},
|
kwargs={"code": self.session.code},
|
||||||
)
|
),
|
||||||
|
HTTP_ACCEPT_LANGUAGE="da",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
self.assertEqual(response.json()["error"], "Only host can finish game")
|
self.assertEqual(response.json(), {
|
||||||
|
"error": "Kun værten kan afslutte spillet",
|
||||||
|
"error_code": "host_only_finish_game",
|
||||||
|
"locale": "da",
|
||||||
|
})
|
||||||
|
|
||||||
def test_finish_game_rejects_wrong_phase(self):
|
def test_finish_game_rejects_wrong_phase(self):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
@@ -867,13 +890,19 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"lobby:finish_game",
|
"lobby:finish_game",
|
||||||
kwargs={"code": self.session.code},
|
kwargs={"code": self.session.code},
|
||||||
)
|
),
|
||||||
|
HTTP_ACCEPT_LANGUAGE="fr",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Game can only be finished from scoreboard phase")
|
self.assertEqual(response.json(), {
|
||||||
|
"error": "Game can only be finished from scoreboard phase",
|
||||||
|
"error_code": "finish_game_invalid_phase",
|
||||||
|
"locale": "en",
|
||||||
|
})
|
||||||
|
|
||||||
def test_host_can_start_next_round_from_scoreboard(self):
|
@patch("lobby.views.sync_broadcast_phase_event")
|
||||||
|
def test_host_can_start_next_round_from_scoreboard(self, _mock_sync_broadcast_phase_event):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code}))
|
self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code}))
|
||||||
|
|
||||||
@@ -893,7 +922,28 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
self.assertEqual(self.session.status, GameSession.Status.LOBBY)
|
self.assertEqual(self.session.status, GameSession.Status.LOBBY)
|
||||||
self.assertEqual(self.session.current_round, 2)
|
self.assertEqual(self.session.current_round, 2)
|
||||||
|
|
||||||
def test_reveal_scoreboard_allows_repeated_reads_after_promotion(self):
|
def test_start_next_round_requires_host(self):
|
||||||
|
self.session.status = GameSession.Status.SCOREBOARD
|
||||||
|
self.session.save(update_fields=["status"])
|
||||||
|
self.client.login(username="other_reveal", password="secret123")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:start_next_round",
|
||||||
|
kwargs={"code": self.session.code},
|
||||||
|
),
|
||||||
|
HTTP_ACCEPT_LANGUAGE="fr",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json(), {
|
||||||
|
"error": "Only host can start next round",
|
||||||
|
"error_code": "host_only_start_next_round",
|
||||||
|
"locale": "en",
|
||||||
|
})
|
||||||
|
|
||||||
|
@patch("lobby.views.sync_broadcast_phase_event")
|
||||||
|
def test_reveal_scoreboard_allows_repeated_reads_after_promotion(self, mock_sync_broadcast_phase_event):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
|
|
||||||
first_response = self.client.get(
|
first_response = self.client.get(
|
||||||
@@ -914,6 +964,7 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
self.assertEqual(first_response.json()["session"]["status"], GameSession.Status.SCOREBOARD)
|
self.assertEqual(first_response.json()["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||||
self.assertEqual(second_response.json()["session"]["status"], GameSession.Status.SCOREBOARD)
|
self.assertEqual(second_response.json()["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||||
self.assertEqual([item["nickname"] for item in second_response.json()["leaderboard"]], ["Luna", "Mads"])
|
self.assertEqual([item["nickname"] for item in second_response.json()["leaderboard"]], ["Luna", "Mads"])
|
||||||
|
self.assertEqual(mock_sync_broadcast_phase_event.call_count, 1)
|
||||||
|
|
||||||
def test_start_next_round_rejects_wrong_phase(self):
|
def test_start_next_round_rejects_wrong_phase(self):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
@@ -924,11 +975,16 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"lobby:start_next_round",
|
"lobby:start_next_round",
|
||||||
kwargs={"code": self.session.code},
|
kwargs={"code": self.session.code},
|
||||||
)
|
),
|
||||||
|
HTTP_ACCEPT_LANGUAGE="da",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Next round can only start from scoreboard phase")
|
self.assertEqual(response.json(), {
|
||||||
|
"error": "Næste runde kan kun starte fra scoreboard-fasen",
|
||||||
|
"error_code": "next_round_invalid_phase",
|
||||||
|
"locale": "da",
|
||||||
|
})
|
||||||
|
|
||||||
class UiScreenTests(TestCase):
|
class UiScreenTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from fupogfakta.models import (
|
|||||||
RoundQuestion,
|
RoundQuestion,
|
||||||
ScoreEvent,
|
ScoreEvent,
|
||||||
)
|
)
|
||||||
|
from realtime.broadcast import sync_broadcast_phase_event
|
||||||
|
|
||||||
from .i18n import api_error, lobby_i18n_errors
|
from .i18n import api_error, lobby_i18n_errors
|
||||||
|
|
||||||
@@ -707,17 +708,30 @@ def reveal_scoreboard(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can view scoreboard"}, status=403)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("host_only_view_scoreboard", "host_only_view_scoreboard"),
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
if locked_session.status not in {GameSession.Status.REVEAL, GameSession.Status.SCOREBOARD}:
|
if locked_session.status not in {GameSession.Status.REVEAL, GameSession.Status.SCOREBOARD}:
|
||||||
return JsonResponse({"error": "Scoreboard is only available in reveal/scoreboard phase"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("scoreboard_invalid_phase", "scoreboard_invalid_phase"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
if locked_session.status == GameSession.Status.REVEAL:
|
promoted_to_scoreboard = locked_session.status == GameSession.Status.REVEAL
|
||||||
|
if promoted_to_scoreboard:
|
||||||
locked_session.status = GameSession.Status.SCOREBOARD
|
locked_session.status = GameSession.Status.SCOREBOARD
|
||||||
locked_session.save(update_fields=["status"])
|
locked_session.save(update_fields=["status"])
|
||||||
|
|
||||||
@@ -727,6 +741,13 @@ def reveal_scoreboard(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
.values("id", "nickname", "score")
|
.values("id", "nickname", "score")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if promoted_to_scoreboard:
|
||||||
|
sync_broadcast_phase_event(
|
||||||
|
session.code,
|
||||||
|
"phase.scoreboard",
|
||||||
|
{"leaderboard": list(leaderboard), "current_round": locked_session.current_round},
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
"session": {
|
"session": {
|
||||||
@@ -747,15 +768,27 @@ def start_next_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can start next round"}, status=403)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("host_only_start_next_round", "host_only_start_next_round"),
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
if locked_session.status != GameSession.Status.SCOREBOARD:
|
if locked_session.status != GameSession.Status.SCOREBOARD:
|
||||||
return JsonResponse({"error": "Next round can only start from scoreboard phase"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("next_round_invalid_phase", "next_round_invalid_phase"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
locked_session.current_round += 1
|
locked_session.current_round += 1
|
||||||
locked_session.status = GameSession.Status.LOBBY
|
locked_session.status = GameSession.Status.LOBBY
|
||||||
@@ -779,15 +812,27 @@ def finish_game(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can finish game"}, status=403)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("host_only_finish_game", "host_only_finish_game"),
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
if locked_session.status != GameSession.Status.SCOREBOARD:
|
if locked_session.status != GameSession.Status.SCOREBOARD:
|
||||||
return JsonResponse({"error": "Game can only be finished from scoreboard phase"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("finish_game_invalid_phase", "finish_game_invalid_phase"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
locked_session.status = GameSession.Status.FINISHED
|
locked_session.status = GameSession.Status.FINISHED
|
||||||
locked_session.save(update_fields=["status"])
|
locked_session.save(update_fields=["status"])
|
||||||
|
|||||||
20
realtime/broadcast.py
Normal file
20
realtime/broadcast.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from channels.layers import get_channel_layer
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast_phase_event(session_code: str, event_type: str, payload: dict) -> None:
|
||||||
|
"""Send a phase event to all WebSocket clients connected to a game session."""
|
||||||
|
channel_layer = get_channel_layer()
|
||||||
|
group_name = f"game_{session_code.upper()}"
|
||||||
|
await channel_layer.group_send(
|
||||||
|
group_name,
|
||||||
|
{
|
||||||
|
"type": "phase_event",
|
||||||
|
"payload": {"type": event_type, **payload},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_broadcast_phase_event(session_code: str, event_type: str, payload: dict) -> None:
|
||||||
|
"""Sync wrapper for calling broadcast_phase_event from synchronous Django views."""
|
||||||
|
async_to_sync(broadcast_phase_event)(session_code, event_type, payload)
|
||||||
@@ -39,6 +39,18 @@
|
|||||||
"unknown": {
|
"unknown": {
|
||||||
"en": "Action failed. Refresh status and try again.",
|
"en": "Action failed. Refresh status and try again.",
|
||||||
"da": "Handlingen fejlede. Opdater status og prøv igen."
|
"da": "Handlingen fejlede. Opdater status og prøv igen."
|
||||||
|
},
|
||||||
|
"scoreboard_failed": {
|
||||||
|
"en": "Could not load scoreboard. Refresh the session and try again.",
|
||||||
|
"da": "Kunne ikke indlæse scoreboardet. Opdater sessionen og prøv igen."
|
||||||
|
},
|
||||||
|
"next_round_failed": {
|
||||||
|
"en": "Could not start next round. Refresh the session and try again.",
|
||||||
|
"da": "Kunne ikke starte næste runde. Opdater sessionen og prøv igen."
|
||||||
|
},
|
||||||
|
"finish_game_failed": {
|
||||||
|
"en": "Could not finish game. Refresh the session and try again.",
|
||||||
|
"da": "Kunne ikke afslutte spillet. Opdater sessionen og prøv igen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
@@ -296,7 +308,13 @@
|
|||||||
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
||||||
"host_only_start_round": "host_only_start_round",
|
"host_only_start_round": "host_only_start_round",
|
||||||
"host_only_show_question": "host_only_show_question",
|
"host_only_show_question": "host_only_show_question",
|
||||||
"host_only_mix_answers": "host_only_mix_answers"
|
"host_only_mix_answers": "host_only_mix_answers",
|
||||||
|
"host_only_view_scoreboard": "host_only_view_scoreboard",
|
||||||
|
"scoreboard_invalid_phase": "scoreboard_invalid_phase",
|
||||||
|
"host_only_start_next_round": "host_only_start_next_round",
|
||||||
|
"next_round_invalid_phase": "next_round_invalid_phase",
|
||||||
|
"host_only_finish_game": "host_only_finish_game",
|
||||||
|
"finish_game_invalid_phase": "finish_game_invalid_phase"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"session_code_required": {
|
"session_code_required": {
|
||||||
@@ -378,6 +396,30 @@
|
|||||||
"host_only_mix_answers": {
|
"host_only_mix_answers": {
|
||||||
"en": "Only host can mix answers",
|
"en": "Only host can mix answers",
|
||||||
"da": "Kun værten kan blande svar"
|
"da": "Kun værten kan blande svar"
|
||||||
|
},
|
||||||
|
"host_only_view_scoreboard": {
|
||||||
|
"en": "Only host can view scoreboard",
|
||||||
|
"da": "Kun værten kan se scoreboardet"
|
||||||
|
},
|
||||||
|
"scoreboard_invalid_phase": {
|
||||||
|
"en": "Scoreboard is only available in reveal/scoreboard phase",
|
||||||
|
"da": "Scoreboard er kun tilgængeligt i reveal-/scoreboard-fasen"
|
||||||
|
},
|
||||||
|
"host_only_start_next_round": {
|
||||||
|
"en": "Only host can start next round",
|
||||||
|
"da": "Kun værten kan starte næste runde"
|
||||||
|
},
|
||||||
|
"next_round_invalid_phase": {
|
||||||
|
"en": "Next round can only start from scoreboard phase",
|
||||||
|
"da": "Næste runde kan kun starte fra scoreboard-fasen"
|
||||||
|
},
|
||||||
|
"host_only_finish_game": {
|
||||||
|
"en": "Only host can finish game",
|
||||||
|
"da": "Kun værten kan afslutte spillet"
|
||||||
|
},
|
||||||
|
"finish_game_invalid_phase": {
|
||||||
|
"en": "Game can only be finished from scoreboard phase",
|
||||||
|
"da": "Spillet kan kun afsluttes fra scoreboard-fasen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -416,7 +458,13 @@
|
|||||||
"no_available_questions": "start_round_failed",
|
"no_available_questions": "start_round_failed",
|
||||||
"mix_answers_invalid_phase": "start_round_failed",
|
"mix_answers_invalid_phase": "start_round_failed",
|
||||||
"round_question_not_found": "start_round_failed",
|
"round_question_not_found": "start_round_failed",
|
||||||
"not_enough_answers_to_mix": "start_round_failed"
|
"not_enough_answers_to_mix": "start_round_failed",
|
||||||
|
"host_only_view_scoreboard": "scoreboard_failed",
|
||||||
|
"scoreboard_invalid_phase": "scoreboard_failed",
|
||||||
|
"host_only_start_next_round": "next_round_failed",
|
||||||
|
"next_round_invalid_phase": "next_round_failed",
|
||||||
|
"host_only_finish_game": "finish_game_failed",
|
||||||
|
"finish_game_invalid_phase": "finish_game_failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user