fix(gameplay): align scoreboard phase contract
This commit is contained in:
@@ -50,7 +50,7 @@ describe('SPA Angular API contract smoke (host/player foundation)', () => {
|
|||||||
|
|
||||||
if (url === '/lobby/sessions/ABCD12/scoreboard') {
|
if (url === '/lobby/sessions/ABCD12/scoreboard') {
|
||||||
return {
|
return {
|
||||||
session: { code: 'ABCD12', status: 'reveal', current_round: 1 },
|
session: { code: 'ABCD12', status: 'scoreboard', current_round: 1 },
|
||||||
leaderboard: [
|
leaderboard: [
|
||||||
{ id: 9, nickname: 'Maja', score: 200 },
|
{ id: 9, nickname: 'Maja', score: 200 },
|
||||||
{ id: 10, nickname: 'Bo', score: 150 }
|
{ id: 10, nickname: 'Bo', score: 150 }
|
||||||
@@ -104,7 +104,7 @@ describe('SPA Angular API contract smoke (host/player foundation)', () => {
|
|||||||
if (url === '/lobby/sessions/ABCD12/questions/77/scores/calculate') {
|
if (url === '/lobby/sessions/ABCD12/questions/77/scores/calculate') {
|
||||||
expect(body).toEqual({});
|
expect(body).toEqual({});
|
||||||
return {
|
return {
|
||||||
session: { code: 'ABCD12', status: 'reveal', current_round: 1 },
|
session: { code: 'ABCD12', status: 'scoreboard', current_round: 1 },
|
||||||
round_question: { id: 77, round_number: 1 },
|
round_question: { id: 77, round_number: 1 },
|
||||||
events_created: 2,
|
events_created: 2,
|
||||||
leaderboard: [
|
leaderboard: [
|
||||||
|
|||||||
@@ -142,13 +142,13 @@ describe('HostShellComponent gameplay wiring', () => {
|
|||||||
.mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 })))
|
.mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 })))
|
||||||
.mockResolvedValueOnce(
|
.mockResolvedValueOnce(
|
||||||
jsonResponse(200, {
|
jsonResponse(200, {
|
||||||
session: { code: 'ABCD12', status: 'reveal', current_round: 1 },
|
session: { code: 'ABCD12', status: 'scoreboard', current_round: 1 },
|
||||||
round_question: { id: 77, round_number: 1 },
|
round_question: { id: 77, round_number: 1 },
|
||||||
events_created: 2,
|
events_created: 2,
|
||||||
leaderboard: [{ id: 1, nickname: 'Luna', score: 320 }],
|
leaderboard: [{ id: 1, nickname: 'Luna', score: 320 }],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('reveal', { roundQuestionId: 77 })));
|
.mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('scoreboard', { roundQuestionId: 77 })));
|
||||||
|
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ describe('createAngularApiClient', () => {
|
|||||||
const get = vi.fn<AngularHttpClientLike['get']>(async <T>(url: string) => {
|
const get = vi.fn<AngularHttpClientLike['get']>(async <T>(url: string) => {
|
||||||
if (url === '/lobby/sessions/ABCD12/scoreboard') {
|
if (url === '/lobby/sessions/ABCD12/scoreboard') {
|
||||||
return {
|
return {
|
||||||
session: { code: 'ABCD12', status: 'reveal', current_round: 1 },
|
session: { code: 'ABCD12', status: 'scoreboard', current_round: 1 },
|
||||||
leaderboard: [
|
leaderboard: [
|
||||||
{ id: 2, nickname: 'Maja', score: 11 },
|
{ id: 2, nickname: 'Maja', score: 11 },
|
||||||
{ id: 3, nickname: 'Bo', score: 7 }
|
{ id: 3, nickname: 'Bo', score: 7 }
|
||||||
@@ -245,7 +245,7 @@ describe('createAngularApiClient', () => {
|
|||||||
if (url === '/lobby/sessions/ABCD12/questions/77/scores/calculate') {
|
if (url === '/lobby/sessions/ABCD12/questions/77/scores/calculate') {
|
||||||
expect(body).toEqual({});
|
expect(body).toEqual({});
|
||||||
return {
|
return {
|
||||||
session: { code: 'ABCD12', status: 'reveal', current_round: 1 },
|
session: { code: 'ABCD12', status: 'scoreboard', current_round: 1 },
|
||||||
round_question: { id: 77, round_number: 1 },
|
round_question: { id: 77, round_number: 1 },
|
||||||
events_created: 3,
|
events_created: 3,
|
||||||
leaderboard: [{ id: 2, nickname: 'Maja', score: 11 }]
|
leaderboard: [{ id: 2, nickname: 'Maja', score: 11 }]
|
||||||
|
|||||||
26
fupogfakta/migrations/0005_gamesession_scoreboard_status.py
Normal file
26
fupogfakta/migrations/0005_gamesession_scoreboard_status.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("fupogfakta", "0004_player_session_token"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="gamesession",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("lobby", "Lobby"),
|
||||||
|
("lie", "Løgnfase"),
|
||||||
|
("guess", "Gættefase"),
|
||||||
|
("reveal", "Reveal"),
|
||||||
|
("scoreboard", "Scoreboard"),
|
||||||
|
("finished", "Afsluttet"),
|
||||||
|
],
|
||||||
|
default="lobby",
|
||||||
|
max_length=16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -42,6 +42,7 @@ class GameSession(models.Model):
|
|||||||
LIE = "lie", "Løgnfase"
|
LIE = "lie", "Løgnfase"
|
||||||
GUESS = "guess", "Gættefase"
|
GUESS = "guess", "Gættefase"
|
||||||
REVEAL = "reveal", "Reveal"
|
REVEAL = "reveal", "Reveal"
|
||||||
|
SCOREBOARD = "scoreboard", "Scoreboard"
|
||||||
FINISHED = "finished", "Afsluttet"
|
FINISHED = "finished", "Afsluttet"
|
||||||
|
|
||||||
host = models.ForeignKey(User, on_delete=models.PROTECT, related_name="hosted_sessions")
|
host = models.ForeignKey(User, on_delete=models.PROTECT, related_name="hosted_sessions")
|
||||||
|
|||||||
139
lobby/tests.py
139
lobby/tests.py
@@ -401,7 +401,9 @@ class LieSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "session_token is required")
|
self.assertEqual(response.json()["error_code"], "session_token_required")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
|
self.assertEqual(response.json()["error"], "Session token is required.")
|
||||||
|
|
||||||
def test_submit_lie_rejects_invalid_session_token(self):
|
def test_submit_lie_rejects_invalid_session_token(self):
|
||||||
round_question = RoundQuestion.objects.create(
|
round_question = RoundQuestion.objects.create(
|
||||||
@@ -582,7 +584,9 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Guess submission is only allowed in guess phase")
|
self.assertEqual(response.json()["error_code"], "guess_submission_invalid_phase")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
|
self.assertEqual(response.json()["error"], "Guess submission is only allowed in guess phase.")
|
||||||
|
|
||||||
def test_submit_guess_rejects_unknown_answer(self):
|
def test_submit_guess_rejects_unknown_answer(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -641,7 +645,7 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "session_token is required")
|
self.assertEqual(response.json()["error"], "Session token is required.")
|
||||||
|
|
||||||
def test_submit_guess_rejects_invalid_session_token(self):
|
def test_submit_guess_rejects_invalid_session_token(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -686,7 +690,9 @@ class ScoreCalculationTests(TestCase):
|
|||||||
self.player_two = Player.objects.create(session=self.session, nickname="Mads")
|
self.player_two = Player.objects.create(session=self.session, nickname="Mads")
|
||||||
self.player_three = Player.objects.create(session=self.session, nickname="Nora")
|
self.player_three = Player.objects.create(session=self.session, nickname="Nora")
|
||||||
|
|
||||||
def test_host_can_calculate_scores_and_transition_to_reveal(self):
|
def test_host_can_calculate_scores_and_transition_to_scoreboard(self):
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_three, text="Padel")
|
||||||
|
|
||||||
Guess.objects.create(round_question=self.round_question, player=self.player_one, selected_text="Tennis", is_correct=True)
|
Guess.objects.create(round_question=self.round_question, player=self.player_one, selected_text="Tennis", is_correct=True)
|
||||||
Guess.objects.create(
|
Guess.objects.create(
|
||||||
round_question=self.round_question,
|
round_question=self.round_question,
|
||||||
@@ -713,8 +719,52 @@ class ScoreCalculationTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
self.assertEqual(payload["session"]["status"], GameSession.Status.REVEAL)
|
self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||||
self.assertEqual(payload["events_created"], 2)
|
self.assertEqual(payload["events_created"], 2)
|
||||||
|
self.assertEqual(payload["reveal"]["round_question_id"], self.round_question.id)
|
||||||
|
self.assertEqual(payload["reveal"]["correct_answer"], "Tennis")
|
||||||
|
self.assertEqual(
|
||||||
|
payload["reveal"]["lies"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"player_id": self.player_three.id,
|
||||||
|
"nickname": "Nora",
|
||||||
|
"text": "Padel",
|
||||||
|
"created_at": payload["reveal"]["lies"][0]["created_at"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
payload["reveal"]["guesses"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"player_id": self.player_one.id,
|
||||||
|
"nickname": "Luna",
|
||||||
|
"selected_text": "Tennis",
|
||||||
|
"is_correct": True,
|
||||||
|
"created_at": payload["reveal"]["guesses"][0]["created_at"],
|
||||||
|
"fooled_player_id": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"player_id": self.player_two.id,
|
||||||
|
"nickname": "Mads",
|
||||||
|
"selected_text": "Padel",
|
||||||
|
"is_correct": False,
|
||||||
|
"created_at": payload["reveal"]["guesses"][1]["created_at"],
|
||||||
|
"fooled_player_id": self.player_three.id,
|
||||||
|
"fooled_player_nickname": "Nora",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"player_id": self.player_three.id,
|
||||||
|
"nickname": "Nora",
|
||||||
|
"selected_text": "Padel",
|
||||||
|
"is_correct": False,
|
||||||
|
"created_at": payload["reveal"]["guesses"][2]["created_at"],
|
||||||
|
"fooled_player_id": self.player_three.id,
|
||||||
|
"fooled_player_nickname": "Nora",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
self.player_one.refresh_from_db()
|
self.player_one.refresh_from_db()
|
||||||
self.player_three.refresh_from_db()
|
self.player_three.refresh_from_db()
|
||||||
@@ -722,7 +772,7 @@ class ScoreCalculationTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(self.player_one.score, 5)
|
self.assertEqual(self.player_one.score, 5)
|
||||||
self.assertEqual(self.player_three.score, 4)
|
self.assertEqual(self.player_three.score, 4)
|
||||||
self.assertEqual(self.session.status, GameSession.Status.REVEAL)
|
self.assertEqual(self.session.status, GameSession.Status.SCOREBOARD)
|
||||||
|
|
||||||
def test_calculate_scores_requires_host(self):
|
def test_calculate_scores_requires_host(self):
|
||||||
self.client.login(username="other_score", password="secret123")
|
self.client.login(username="other_score", password="secret123")
|
||||||
@@ -735,7 +785,7 @@ class ScoreCalculationTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
self.assertEqual(response.json()["error"], "Only host can calculate scores")
|
self.assertEqual(response.json()["error"], "Only the host can calculate scores.")
|
||||||
|
|
||||||
def test_calculate_scores_rejects_duplicate_calculation(self):
|
def test_calculate_scores_rejects_duplicate_calculation(self):
|
||||||
Guess.objects.create(round_question=self.round_question, player=self.player_one, selected_text="Tennis", is_correct=True)
|
Guess.objects.create(round_question=self.round_question, player=self.player_one, selected_text="Tennis", is_correct=True)
|
||||||
@@ -756,14 +806,14 @@ class ScoreCalculationTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(first.status_code, 200)
|
self.assertEqual(first.status_code, 200)
|
||||||
self.assertEqual(second.status_code, 409)
|
self.assertEqual(second.status_code, 409)
|
||||||
self.assertEqual(second.json()["error"], "Scores already calculated for this round question")
|
self.assertEqual(second.json()["error"], "Scores have already been calculated for this round question.")
|
||||||
|
|
||||||
|
|
||||||
class RevealRoundFlowTests(TestCase):
|
class RevealRoundFlowTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.host = User.objects.create_user(username="host_reveal", password="secret123")
|
self.host = User.objects.create_user(username="host_reveal", password="secret123")
|
||||||
self.other_user = User.objects.create_user(username="other_reveal", password="secret123")
|
self.other_user = User.objects.create_user(username="other_reveal", password="secret123")
|
||||||
self.session = GameSession.objects.create(host=self.host, code="RVL123", status=GameSession.Status.REVEAL)
|
self.session = GameSession.objects.create(host=self.host, code="RVL123", status=GameSession.Status.SCOREBOARD)
|
||||||
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)
|
||||||
|
|
||||||
@@ -779,7 +829,7 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
self.assertEqual(payload["session"]["status"], GameSession.Status.REVEAL)
|
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_reveal_scoreboard_requires_host(self):
|
def test_reveal_scoreboard_requires_host(self):
|
||||||
@@ -793,9 +843,9 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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 the host can view the scoreboard.")
|
||||||
|
|
||||||
def test_host_can_finish_game_from_reveal(self):
|
def test_host_can_finish_game_from_scoreboard(self):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -825,7 +875,7 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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"], "Only the host can finish the game.")
|
||||||
|
|
||||||
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")
|
||||||
@@ -840,9 +890,9 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Game can only be finished from reveal phase")
|
self.assertEqual(response.json()["error"], "Game can only be finished from scoreboard phase.")
|
||||||
|
|
||||||
def test_host_can_start_next_round_from_reveal(self):
|
def test_host_can_start_next_round_from_scoreboard(self):
|
||||||
self.client.login(username="host_reveal", password="secret123")
|
self.client.login(username="host_reveal", password="secret123")
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -874,7 +924,7 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Next round can only start from reveal phase")
|
self.assertEqual(response.json()["error"], "Next round can only start from scoreboard phase.")
|
||||||
|
|
||||||
class UiScreenTests(TestCase):
|
class UiScreenTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -1149,7 +1199,51 @@ class SessionDetailRoundQuestionTests(TestCase):
|
|||||||
self.assertEqual(payload["round_question"]["id"], round_question.id)
|
self.assertEqual(payload["round_question"]["id"], round_question.id)
|
||||||
self.assertEqual(payload["round_question"]["prompt"], self.question.prompt)
|
self.assertEqual(payload["round_question"]["prompt"], self.question.prompt)
|
||||||
|
|
||||||
|
def test_session_detail_includes_canonical_reveal_payload_in_reveal_phase(self):
|
||||||
|
self.session.status = GameSession.Status.REVEAL
|
||||||
|
self.session.save(update_fields=["status"])
|
||||||
|
round_question = RoundQuestion.objects.create(
|
||||||
|
session=self.session,
|
||||||
|
round_number=1,
|
||||||
|
question=self.question,
|
||||||
|
correct_answer=self.question.correct_answer,
|
||||||
|
)
|
||||||
|
liar = Player.objects.create(session=self.session, nickname="Løgnhals")
|
||||||
|
guesser = Player.objects.create(session=self.session, nickname="Detektiv")
|
||||||
|
correct_player = Player.objects.create(session=self.session, nickname="Sandhed")
|
||||||
|
LieAnswer.objects.create(round_question=round_question, player=liar, text="Tesla")
|
||||||
|
Guess.objects.create(
|
||||||
|
round_question=round_question,
|
||||||
|
player=guesser,
|
||||||
|
selected_text="Tesla",
|
||||||
|
is_correct=False,
|
||||||
|
fooled_player=liar,
|
||||||
|
)
|
||||||
|
Guess.objects.create(
|
||||||
|
round_question=round_question,
|
||||||
|
player=correct_player,
|
||||||
|
selected_text="Edison",
|
||||||
|
is_correct=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code}))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["reveal"]["round_question_id"], round_question.id)
|
||||||
|
self.assertEqual(payload["reveal"]["correct_answer"], "Edison")
|
||||||
|
self.assertEqual(payload["reveal"]["lies"][0]["player_id"], liar.id)
|
||||||
|
self.assertEqual(payload["reveal"]["lies"][0]["nickname"], "Løgnhals")
|
||||||
|
self.assertEqual(payload["reveal"]["lies"][0]["text"], "Tesla")
|
||||||
|
self.assertEqual(payload["reveal"]["guesses"][0]["player_id"], guesser.id)
|
||||||
|
self.assertEqual(payload["reveal"]["guesses"][0]["selected_text"], "Tesla")
|
||||||
|
self.assertFalse(payload["reveal"]["guesses"][0]["is_correct"])
|
||||||
|
self.assertEqual(payload["reveal"]["guesses"][0]["fooled_player_id"], liar.id)
|
||||||
|
self.assertEqual(payload["reveal"]["guesses"][0]["fooled_player_nickname"], "Løgnhals")
|
||||||
|
self.assertEqual(payload["reveal"]["guesses"][1]["player_id"], correct_player.id)
|
||||||
|
self.assertEqual(payload["reveal"]["guesses"][1]["selected_text"], "Edison")
|
||||||
|
self.assertTrue(payload["reveal"]["guesses"][1]["is_correct"])
|
||||||
|
self.assertIsNone(payload["reveal"]["guesses"][1]["fooled_player_id"])
|
||||||
|
|
||||||
|
|
||||||
class SessionDetailPhaseViewModelTests(TestCase):
|
class SessionDetailPhaseViewModelTests(TestCase):
|
||||||
@@ -1218,10 +1312,19 @@ class SessionDetailPhaseViewModelTests(TestCase):
|
|||||||
reveal_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
reveal_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
||||||
reveal_phase = reveal_payload["phase_view_model"]
|
reveal_phase = reveal_payload["phase_view_model"]
|
||||||
self.assertTrue(reveal_phase["host"]["can_reveal_scoreboard"])
|
self.assertTrue(reveal_phase["host"]["can_reveal_scoreboard"])
|
||||||
self.assertTrue(reveal_phase["host"]["can_start_next_round"])
|
self.assertFalse(reveal_phase["host"]["can_start_next_round"])
|
||||||
self.assertTrue(reveal_phase["host"]["can_finish_game"])
|
self.assertFalse(reveal_phase["host"]["can_finish_game"])
|
||||||
self.assertFalse(reveal_phase["player"]["can_view_final_result"])
|
self.assertFalse(reveal_phase["player"]["can_view_final_result"])
|
||||||
|
|
||||||
|
self.session.status = GameSession.Status.SCOREBOARD
|
||||||
|
self.session.save(update_fields=["status"])
|
||||||
|
scoreboard_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
||||||
|
scoreboard_phase = scoreboard_payload["phase_view_model"]
|
||||||
|
self.assertFalse(scoreboard_phase["host"]["can_reveal_scoreboard"])
|
||||||
|
self.assertTrue(scoreboard_phase["host"]["can_start_next_round"])
|
||||||
|
self.assertTrue(scoreboard_phase["host"]["can_finish_game"])
|
||||||
|
self.assertFalse(scoreboard_phase["player"]["can_view_final_result"])
|
||||||
|
|
||||||
self.session.status = GameSession.Status.FINISHED
|
self.session.status = GameSession.Status.FINISHED
|
||||||
self.session.save(update_fields=["status"])
|
self.session.save(update_fields=["status"])
|
||||||
finished_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
finished_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
||||||
|
|||||||
239
lobby/views.py
239
lobby/views.py
@@ -34,6 +34,14 @@ JOINABLE_STATUSES = {
|
|||||||
ERROR_CODES = lobby_i18n_errors()
|
ERROR_CODES = lobby_i18n_errors()
|
||||||
|
|
||||||
|
|
||||||
|
def _error_key(code: str) -> str:
|
||||||
|
return ERROR_CODES.get(code, code)
|
||||||
|
|
||||||
|
|
||||||
|
def _api_error(request: HttpRequest, *, code: str, status: int) -> JsonResponse:
|
||||||
|
return api_error(request, key=_error_key(code), status=status)
|
||||||
|
|
||||||
|
|
||||||
def _json_body(request: HttpRequest) -> dict:
|
def _json_body(request: HttpRequest) -> dict:
|
||||||
if not request.body:
|
if not request.body:
|
||||||
return {}
|
return {}
|
||||||
@@ -61,12 +69,61 @@ def _create_unique_session_code() -> str:
|
|||||||
raise RuntimeError("Could not generate unique session code")
|
raise RuntimeError("Could not generate unique session code")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_player_ref(player: Player | None) -> dict | None:
|
||||||
|
if player is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"player_id": player.id,
|
||||||
|
"nickname": player.nickname,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _build_reveal_payload(round_question: RoundQuestion | None) -> dict | None:
|
||||||
|
if round_question is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lies = [
|
||||||
|
{
|
||||||
|
**_build_player_ref(lie.player),
|
||||||
|
"text": lie.text,
|
||||||
|
"created_at": lie.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
for lie in round_question.lies.select_related("player").order_by("created_at", "id")
|
||||||
|
]
|
||||||
|
|
||||||
|
guesses = []
|
||||||
|
for guess in round_question.guesses.select_related("player", "fooled_player").order_by("created_at", "id"):
|
||||||
|
guess_payload = {
|
||||||
|
**_build_player_ref(guess.player),
|
||||||
|
"selected_text": guess.selected_text,
|
||||||
|
"is_correct": guess.is_correct,
|
||||||
|
"created_at": guess.created_at.isoformat(),
|
||||||
|
"fooled_player_id": guess.fooled_player_id,
|
||||||
|
}
|
||||||
|
if guess.fooled_player is not None:
|
||||||
|
guess_payload["fooled_player_nickname"] = guess.fooled_player.nickname
|
||||||
|
guesses.append(guess_payload)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"round_question_id": round_question.id,
|
||||||
|
"round_number": round_question.round_number,
|
||||||
|
"prompt": round_question.question.prompt,
|
||||||
|
"correct_answer": round_question.correct_answer,
|
||||||
|
"lies": lies,
|
||||||
|
"guesses": guesses,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _build_phase_view_model(session: GameSession, *, players_count: int, has_round_question: bool) -> dict:
|
def _build_phase_view_model(session: GameSession, *, players_count: int, has_round_question: bool) -> dict:
|
||||||
status = session.status
|
status = session.status
|
||||||
in_lobby = status == GameSession.Status.LOBBY
|
in_lobby = status == GameSession.Status.LOBBY
|
||||||
in_lie = status == GameSession.Status.LIE
|
in_lie = status == GameSession.Status.LIE
|
||||||
in_guess = status == GameSession.Status.GUESS
|
in_guess = status == GameSession.Status.GUESS
|
||||||
in_reveal = status == GameSession.Status.REVEAL
|
in_reveal = status == GameSession.Status.REVEAL
|
||||||
|
in_scoreboard = status == GameSession.Status.SCOREBOARD
|
||||||
in_finished = status == GameSession.Status.FINISHED
|
in_finished = status == GameSession.Status.FINISHED
|
||||||
|
|
||||||
min_players_reached = players_count >= 3
|
min_players_reached = players_count >= 3
|
||||||
@@ -88,8 +145,8 @@ def _build_phase_view_model(session: GameSession, *, players_count: int, has_rou
|
|||||||
"can_mix_answers": in_lie or in_guess,
|
"can_mix_answers": in_lie or in_guess,
|
||||||
"can_calculate_scores": in_guess,
|
"can_calculate_scores": in_guess,
|
||||||
"can_reveal_scoreboard": in_reveal,
|
"can_reveal_scoreboard": in_reveal,
|
||||||
"can_start_next_round": in_reveal,
|
"can_start_next_round": in_scoreboard,
|
||||||
"can_finish_game": in_reveal,
|
"can_finish_game": in_scoreboard,
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"can_join": status in JOINABLE_STATUSES,
|
"can_join": status in JOINABLE_STATUSES,
|
||||||
@@ -127,41 +184,21 @@ def join_session(request: HttpRequest) -> JsonResponse:
|
|||||||
nickname = str(payload.get("nickname", "")).strip()
|
nickname = str(payload.get("nickname", "")).strip()
|
||||||
|
|
||||||
if not code:
|
if not code:
|
||||||
return api_error(
|
return _api_error(request, code="session_code_required", status=400)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("session_code_required", "session_code_required"),
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(nickname) < 2 or len(nickname) > 40:
|
if len(nickname) < 2 or len(nickname) > 40:
|
||||||
return api_error(
|
return _api_error(request, code="nickname_invalid", status=400)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("nickname_invalid", "nickname_invalid"),
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=code)
|
session = GameSession.objects.get(code=code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return _api_error(request, code="session_not_found", status=404)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
if session.status not in JOINABLE_STATUSES:
|
if session.status not in JOINABLE_STATUSES:
|
||||||
return api_error(
|
return _api_error(request, code="session_not_joinable", status=400)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("session_not_joinable", "session_not_joinable"),
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
if Player.objects.filter(session=session, nickname__iexact=nickname).exists():
|
if Player.objects.filter(session=session, nickname__iexact=nickname).exists():
|
||||||
return api_error(
|
return _api_error(request, code="nickname_taken", status=409)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("nickname_taken", "nickname_taken"),
|
|
||||||
status=409,
|
|
||||||
)
|
|
||||||
|
|
||||||
player = Player.objects.create(session=session, nickname=nickname)
|
player = Player.objects.create(session=session, nickname=nickname)
|
||||||
|
|
||||||
@@ -189,11 +226,7 @@ def session_detail(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 api_error(
|
return _api_error(request, code="session_not_found", status=404)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
players = list(
|
players = list(
|
||||||
session.players.order_by("nickname").values(
|
session.players.order_by("nickname").values(
|
||||||
@@ -238,6 +271,9 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
},
|
},
|
||||||
"players": players,
|
"players": players,
|
||||||
"round_question": round_question_payload,
|
"round_question": round_question_payload,
|
||||||
|
"reveal": _build_reveal_payload(current_round_question)
|
||||||
|
if session.status == GameSession.Status.REVEAL and current_round_question
|
||||||
|
else None,
|
||||||
"phase_view_model": phase_view_model,
|
"phase_view_model": phase_view_model,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -250,22 +286,14 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
category_slug = str(payload.get("category_slug", "")).strip()
|
category_slug = str(payload.get("category_slug", "")).strip()
|
||||||
|
|
||||||
if not category_slug:
|
if not category_slug:
|
||||||
return api_error(
|
return _api_error(request, code="category_slug_required", status=400)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("category_slug_required", "category_slug_required"),
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
session_code = _normalize_session_code(code)
|
session_code = _normalize_session_code(code)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return _api_error(request, code="session_not_found", status=404)
|
||||||
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 api_error(
|
return api_error(
|
||||||
@@ -348,11 +376,7 @@ def show_question(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 api_error(
|
return _api_error(request, code="session_not_found", status=404)
|
||||||
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 api_error(
|
return api_error(
|
||||||
@@ -434,29 +458,29 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
|
|||||||
lie_text = str(payload.get("text", "")).strip()
|
lie_text = str(payload.get("text", "")).strip()
|
||||||
|
|
||||||
if not player_id:
|
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:
|
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:
|
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:
|
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, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.status != GameSession.Status.LIE:
|
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:
|
try:
|
||||||
player = Player.objects.get(pk=player_id, session=session)
|
player = Player.objects.get(pk=player_id, session=session)
|
||||||
except Player.DoesNotExist:
|
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:
|
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:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -465,21 +489,21 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return _api_error(request, code="round_question_not_found", status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||||
except RoundConfig.DoesNotExist:
|
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)
|
lie_deadline_at = round_question.shown_at + timedelta(seconds=round_config.lie_seconds)
|
||||||
if timezone.now() > lie_deadline_at:
|
if timezone.now() > lie_deadline_at:
|
||||||
return JsonResponse({"error": "Lie submission window has closed"}, status=400)
|
return _api_error(request, code="lie_submission_window_closed", status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lie = LieAnswer.objects.create(round_question=round_question, player=player, text=lie_text)
|
lie = LieAnswer.objects.create(round_question=round_question, player=player, text=lie_text)
|
||||||
except IntegrityError:
|
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(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
@@ -505,25 +529,13 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return _api_error(request, code="session_not_found", status=404)
|
||||||
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 api_error(
|
return _api_error(request, code="host_only_mix_answers", status=403)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("host_only_mix_answers", "host_only_mix_answers"),
|
|
||||||
status=403,
|
|
||||||
)
|
|
||||||
|
|
||||||
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||||
return api_error(
|
return _api_error(request, code="mix_answers_invalid_phase", status=400)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -532,11 +544,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return api_error(
|
return _api_error(request, code="round_question_not_found", status=404)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("round_question_not_found", "round_question_not_found"),
|
|
||||||
status=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -561,11 +569,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
deduped_answers.append(text.strip())
|
deduped_answers.append(text.strip())
|
||||||
|
|
||||||
if len(deduped_answers) < 2:
|
if len(deduped_answers) < 2:
|
||||||
return api_error(
|
return _api_error(request, code="not_enough_answers_to_mix", status=400)
|
||||||
request,
|
|
||||||
key=ERROR_CODES.get("not_enough_answers_to_mix", "not_enough_answers_to_mix"),
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
random.shuffle(deduped_answers)
|
random.shuffle(deduped_answers)
|
||||||
locked_round_question.mixed_answers = deduped_answers
|
locked_round_question.mixed_answers = deduped_answers
|
||||||
@@ -601,29 +605,29 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
selected_text = str(payload.get("selected_text", "")).strip()
|
selected_text = str(payload.get("selected_text", "")).strip()
|
||||||
|
|
||||||
if not player_id:
|
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:
|
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:
|
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:
|
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, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.status != GameSession.Status.GUESS:
|
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:
|
try:
|
||||||
player = Player.objects.get(pk=player_id, session=session)
|
player = Player.objects.get(pk=player_id, session=session)
|
||||||
except Player.DoesNotExist:
|
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:
|
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:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -632,18 +636,18 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return _api_error(request, code="round_question_not_found", status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||||
except RoundConfig.DoesNotExist:
|
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(
|
guess_deadline_at = round_question.shown_at + timedelta(
|
||||||
seconds=round_config.lie_seconds + round_config.guess_seconds
|
seconds=round_config.lie_seconds + round_config.guess_seconds
|
||||||
)
|
)
|
||||||
if timezone.now() > guess_deadline_at:
|
if timezone.now() > guess_deadline_at:
|
||||||
return JsonResponse({"error": "Guess submission window has closed"}, status=400)
|
return _api_error(request, code="guess_submission_window_closed", status=400)
|
||||||
|
|
||||||
allowed_answers = {
|
allowed_answers = {
|
||||||
round_question.correct_answer.strip().casefold(),
|
round_question.correct_answer.strip().casefold(),
|
||||||
@@ -656,7 +660,7 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
|
|
||||||
selected_normalized = selected_text.casefold()
|
selected_normalized = selected_text.casefold()
|
||||||
if selected_normalized not in allowed_answers:
|
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()
|
correct_normalized = round_question.correct_answer.strip().casefold()
|
||||||
fooled_player_id = None
|
fooled_player_id = None
|
||||||
@@ -674,7 +678,7 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
fooled_player_id=fooled_player_id,
|
fooled_player_id=fooled_player_id,
|
||||||
)
|
)
|
||||||
except IntegrityError:
|
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(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
@@ -705,13 +709,13 @@ 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, code="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, code="host_only_view_scoreboard", status=403)
|
||||||
|
|
||||||
if session.status != GameSession.Status.REVEAL:
|
if session.status != GameSession.Status.SCOREBOARD:
|
||||||
return JsonResponse({"error": "Scoreboard is only available in reveal phase"}, status=400)
|
return _api_error(request, code="scoreboard_invalid_phase", status=400)
|
||||||
|
|
||||||
leaderboard = list(
|
leaderboard = list(
|
||||||
Player.objects.filter(session=session)
|
Player.objects.filter(session=session)
|
||||||
@@ -739,15 +743,15 @@ 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, code="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, code="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.REVEAL:
|
if locked_session.status != GameSession.Status.SCOREBOARD:
|
||||||
return JsonResponse({"error": "Next round can only start from reveal phase"}, status=400)
|
return _api_error(request, code="start_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
|
||||||
@@ -771,15 +775,15 @@ 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, code="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, code="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.REVEAL:
|
if locked_session.status != GameSession.Status.SCOREBOARD:
|
||||||
return JsonResponse({"error": "Game can only be finished from reveal phase"}, status=400)
|
return _api_error(request, code="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"])
|
||||||
@@ -813,20 +817,20 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
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, code="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 calculate scores"}, status=403)
|
return _api_error(request, code="host_only_calculate_scores", status=403)
|
||||||
|
|
||||||
already_calculated = ScoreEvent.objects.filter(
|
already_calculated = ScoreEvent.objects.filter(
|
||||||
session=session,
|
session=session,
|
||||||
meta__round_question_id=round_question_id,
|
meta__round_question_id=round_question_id,
|
||||||
).exists()
|
).exists()
|
||||||
if already_calculated:
|
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:
|
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:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -835,16 +839,16 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return _api_error(request, code="round_question_not_found", status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||||
except RoundConfig.DoesNotExist:
|
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"))
|
guesses = list(round_question.guesses.select_related("player"))
|
||||||
if not guesses:
|
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 = {}
|
bluff_counts = {}
|
||||||
for guess in guesses:
|
for guess in guesses:
|
||||||
@@ -854,7 +858,7 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
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.GUESS:
|
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 = []
|
score_events = []
|
||||||
|
|
||||||
@@ -889,7 +893,7 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
|
|
||||||
ScoreEvent.objects.bulk_create(score_events)
|
ScoreEvent.objects.bulk_create(score_events)
|
||||||
|
|
||||||
locked_session.status = GameSession.Status.REVEAL
|
locked_session.status = GameSession.Status.SCOREBOARD
|
||||||
locked_session.save(update_fields=["status"])
|
locked_session.save(update_fields=["status"])
|
||||||
|
|
||||||
leaderboard = list(
|
leaderboard = list(
|
||||||
@@ -902,13 +906,14 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
{
|
{
|
||||||
"session": {
|
"session": {
|
||||||
"code": session.code,
|
"code": session.code,
|
||||||
"status": GameSession.Status.REVEAL,
|
"status": GameSession.Status.SCOREBOARD,
|
||||||
"current_round": session.current_round,
|
"current_round": session.current_round,
|
||||||
},
|
},
|
||||||
"round_question": {
|
"round_question": {
|
||||||
"id": round_question.id,
|
"id": round_question.id,
|
||||||
"round_number": round_question.round_number,
|
"round_number": round_question.round_number,
|
||||||
},
|
},
|
||||||
|
"reveal": _build_reveal_payload(round_question),
|
||||||
"events_created": len(score_events),
|
"events_created": len(score_events),
|
||||||
"leaderboard": leaderboard,
|
"leaderboard": leaderboard,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,36 +277,203 @@
|
|||||||
},
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"error_codes": {
|
"error_codes": {
|
||||||
"session_code_required": "session_code_required",
|
"calculate_scores_invalid_phase": "calculate_scores_invalid_phase",
|
||||||
|
"category_has_no_questions": "category_has_no_questions",
|
||||||
|
"category_not_found": "category_not_found",
|
||||||
|
"category_slug_required": "category_slug_required",
|
||||||
|
"finish_game_invalid_phase": "finish_game_invalid_phase",
|
||||||
|
"guess_already_submitted": "guess_already_submitted",
|
||||||
|
"guess_submission_invalid_phase": "guess_submission_invalid_phase",
|
||||||
|
"guess_submission_window_closed": "guess_submission_window_closed",
|
||||||
|
"host_only_calculate_scores": "host_only_calculate_scores",
|
||||||
|
"host_only_finish_game": "host_only_finish_game",
|
||||||
|
"host_only_mix_answers": "host_only_mix_answers",
|
||||||
|
"host_only_show_question": "host_only_show_question",
|
||||||
|
"host_only_start_next_round": "host_only_start_next_round",
|
||||||
|
"host_only_start_round": "host_only_start_round",
|
||||||
|
"host_only_view_scoreboard": "host_only_view_scoreboard",
|
||||||
|
"invalid_player_session_token": "invalid_player_session_token",
|
||||||
|
"lie_already_submitted": "lie_already_submitted",
|
||||||
|
"lie_submission_invalid_phase": "lie_submission_invalid_phase",
|
||||||
|
"lie_submission_window_closed": "lie_submission_window_closed",
|
||||||
|
"lie_text_invalid": "lie_text_invalid",
|
||||||
|
"mix_answers_invalid_phase": "mix_answers_invalid_phase",
|
||||||
"nickname_invalid": "nickname_invalid",
|
"nickname_invalid": "nickname_invalid",
|
||||||
|
"nickname_taken": "nickname_taken",
|
||||||
|
"no_available_questions": "no_available_questions",
|
||||||
|
"no_guesses_submitted": "no_guesses_submitted",
|
||||||
|
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
||||||
|
"player_id_required": "player_id_required",
|
||||||
|
"player_not_found_in_session": "player_not_found_in_session",
|
||||||
|
"question_already_shown": "question_already_shown",
|
||||||
|
"round_already_configured": "round_already_configured",
|
||||||
|
"round_config_missing": "round_config_missing",
|
||||||
|
"round_question_not_found": "round_question_not_found",
|
||||||
|
"round_start_invalid_phase": "round_start_invalid_phase",
|
||||||
|
"scoreboard_invalid_phase": "scoreboard_invalid_phase",
|
||||||
|
"scores_already_calculated": "scores_already_calculated",
|
||||||
|
"selected_answer_invalid": "selected_answer_invalid",
|
||||||
|
"selected_text_invalid": "selected_text_invalid",
|
||||||
|
"session_code_required": "session_code_required",
|
||||||
"session_not_found": "session_not_found",
|
"session_not_found": "session_not_found",
|
||||||
"session_not_joinable": "session_not_joinable",
|
"session_not_joinable": "session_not_joinable",
|
||||||
"nickname_taken": "nickname_taken",
|
"session_token_required": "session_token_required",
|
||||||
"category_slug_required": "category_slug_required",
|
|
||||||
"category_not_found": "category_not_found",
|
|
||||||
"round_start_invalid_phase": "round_start_invalid_phase",
|
|
||||||
"round_already_configured": "round_already_configured",
|
|
||||||
"category_has_no_questions": "category_has_no_questions",
|
|
||||||
"show_question_invalid_phase": "show_question_invalid_phase",
|
"show_question_invalid_phase": "show_question_invalid_phase",
|
||||||
"round_config_missing": "round_config_missing",
|
"start_next_round_invalid_phase": "start_next_round_invalid_phase"
|
||||||
"question_already_shown": "question_already_shown",
|
|
||||||
"no_available_questions": "no_available_questions",
|
|
||||||
"mix_answers_invalid_phase": "mix_answers_invalid_phase",
|
|
||||||
"round_question_not_found": "round_question_not_found",
|
|
||||||
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
|
||||||
"host_only_start_round": "host_only_start_round",
|
|
||||||
"host_only_show_question": "host_only_show_question",
|
|
||||||
"host_only_mix_answers": "host_only_mix_answers"
|
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"session_code_required": {
|
"calculate_scores_invalid_phase": {
|
||||||
"en": "Session code is required",
|
"en": "Scores can only be calculated in guess phase.",
|
||||||
"da": "Sessionskode er påkrævet"
|
"da": "Score kan kun udregnes i gættefasen."
|
||||||
|
},
|
||||||
|
"category_has_no_questions": {
|
||||||
|
"en": "Category has no active questions",
|
||||||
|
"da": "Kategorien har ingen aktive spørgsmål"
|
||||||
|
},
|
||||||
|
"category_not_found": {
|
||||||
|
"en": "Category not found",
|
||||||
|
"da": "Kategori blev ikke fundet"
|
||||||
|
},
|
||||||
|
"category_slug_required": {
|
||||||
|
"en": "category_slug is required",
|
||||||
|
"da": "category_slug er påkrævet"
|
||||||
|
},
|
||||||
|
"finish_game_invalid_phase": {
|
||||||
|
"en": "Game can only be finished from scoreboard phase.",
|
||||||
|
"da": "Spillet kan kun afsluttes fra scoreboard-fasen."
|
||||||
|
},
|
||||||
|
"guess_already_submitted": {
|
||||||
|
"en": "Guess has already been submitted for this player.",
|
||||||
|
"da": "Gættet er allerede indsendt for denne spiller."
|
||||||
|
},
|
||||||
|
"guess_submission_invalid_phase": {
|
||||||
|
"en": "Guess submission is only allowed in guess phase.",
|
||||||
|
"da": "Gæt kan kun sendes i gættefasen."
|
||||||
|
},
|
||||||
|
"guess_submission_window_closed": {
|
||||||
|
"en": "Guess submission window has closed.",
|
||||||
|
"da": "Vinduet for gætindsendelse er lukket."
|
||||||
|
},
|
||||||
|
"host_only_calculate_scores": {
|
||||||
|
"en": "Only the host can calculate scores.",
|
||||||
|
"da": "Kun værten kan udregne score."
|
||||||
|
},
|
||||||
|
"host_only_finish_game": {
|
||||||
|
"en": "Only the host can finish the game.",
|
||||||
|
"da": "Kun værten kan afslutte spillet."
|
||||||
|
},
|
||||||
|
"host_only_mix_answers": {
|
||||||
|
"en": "Only host can mix answers",
|
||||||
|
"da": "Kun værten kan blande svar"
|
||||||
|
},
|
||||||
|
"host_only_show_question": {
|
||||||
|
"en": "Only host can show question",
|
||||||
|
"da": "Kun værten kan vise spørgsmålet"
|
||||||
|
},
|
||||||
|
"host_only_start_next_round": {
|
||||||
|
"en": "Only the host can start the next round.",
|
||||||
|
"da": "Kun værten kan starte næste runde."
|
||||||
|
},
|
||||||
|
"host_only_start_round": {
|
||||||
|
"en": "Only host can start round",
|
||||||
|
"da": "Kun værten kan starte runden"
|
||||||
|
},
|
||||||
|
"host_only_view_scoreboard": {
|
||||||
|
"en": "Only the host can view the scoreboard.",
|
||||||
|
"da": "Kun værten kan se scoreboardet."
|
||||||
|
},
|
||||||
|
"invalid_player_session_token": {
|
||||||
|
"en": "Player session token is invalid.",
|
||||||
|
"da": "Spillerens session-token er ugyldigt."
|
||||||
|
},
|
||||||
|
"lie_already_submitted": {
|
||||||
|
"en": "Lie has already been submitted for this player.",
|
||||||
|
"da": "Løgnen er allerede indsendt for denne spiller."
|
||||||
|
},
|
||||||
|
"lie_submission_invalid_phase": {
|
||||||
|
"en": "Lie submission is only allowed in lie phase.",
|
||||||
|
"da": "Løgn kan kun sendes i løgnefasen."
|
||||||
|
},
|
||||||
|
"lie_submission_window_closed": {
|
||||||
|
"en": "Lie submission window has closed.",
|
||||||
|
"da": "Vinduet for løgnindsendelse er lukket."
|
||||||
|
},
|
||||||
|
"lie_text_invalid": {
|
||||||
|
"en": "Text must be between 1 and 255 characters.",
|
||||||
|
"da": "Tekst skal være mellem 1 og 255 tegn."
|
||||||
|
},
|
||||||
|
"mix_answers_invalid_phase": {
|
||||||
|
"en": "Answers can only be mixed in lie or guess phase",
|
||||||
|
"da": "Svar kan kun blandes i løgne- eller gættefasen"
|
||||||
},
|
},
|
||||||
"nickname_invalid": {
|
"nickname_invalid": {
|
||||||
"en": "Nickname must be between 2 and 40 characters",
|
"en": "Nickname must be between 2 and 40 characters",
|
||||||
"da": "Kaldenavn skal være mellem 2 og 40 tegn"
|
"da": "Kaldenavn skal være mellem 2 og 40 tegn"
|
||||||
},
|
},
|
||||||
|
"nickname_taken": {
|
||||||
|
"en": "Nickname already taken",
|
||||||
|
"da": "Kaldenavnet er allerede taget"
|
||||||
|
},
|
||||||
|
"no_available_questions": {
|
||||||
|
"en": "No available questions in category",
|
||||||
|
"da": "Ingen tilgængelige spørgsmål i kategorien"
|
||||||
|
},
|
||||||
|
"no_guesses_submitted": {
|
||||||
|
"en": "No guesses have been submitted for this round question.",
|
||||||
|
"da": "Der er ikke indsendt gæt for dette rundespørgsmål."
|
||||||
|
},
|
||||||
|
"not_enough_answers_to_mix": {
|
||||||
|
"en": "Not enough answers to mix",
|
||||||
|
"da": "Ikke nok svar at blande"
|
||||||
|
},
|
||||||
|
"player_id_required": {
|
||||||
|
"en": "Player id is required.",
|
||||||
|
"da": "Spiller-id er påkrævet."
|
||||||
|
},
|
||||||
|
"player_not_found_in_session": {
|
||||||
|
"en": "Player was not found in this session.",
|
||||||
|
"da": "Spilleren blev ikke fundet i denne session."
|
||||||
|
},
|
||||||
|
"question_already_shown": {
|
||||||
|
"en": "Question already shown for this round",
|
||||||
|
"da": "Spørgsmålet er allerede vist for denne runde"
|
||||||
|
},
|
||||||
|
"round_already_configured": {
|
||||||
|
"en": "Round already configured",
|
||||||
|
"da": "Runden er allerede konfigureret"
|
||||||
|
},
|
||||||
|
"round_config_missing": {
|
||||||
|
"en": "Round config missing",
|
||||||
|
"da": "Rundekonfiguration mangler"
|
||||||
|
},
|
||||||
|
"round_question_not_found": {
|
||||||
|
"en": "Round question not found",
|
||||||
|
"da": "Rundespørgsmål blev ikke fundet"
|
||||||
|
},
|
||||||
|
"round_start_invalid_phase": {
|
||||||
|
"en": "Round can only be started from lobby",
|
||||||
|
"da": "Runden kan kun startes fra lobbyen"
|
||||||
|
},
|
||||||
|
"scoreboard_invalid_phase": {
|
||||||
|
"en": "Scoreboard is only available in scoreboard phase.",
|
||||||
|
"da": "Scoreboard er kun tilgængeligt i scoreboard-fasen."
|
||||||
|
},
|
||||||
|
"scores_already_calculated": {
|
||||||
|
"en": "Scores have already been calculated for this round question.",
|
||||||
|
"da": "Score er allerede udregnet for dette rundespørgsmål."
|
||||||
|
},
|
||||||
|
"selected_answer_invalid": {
|
||||||
|
"en": "Selected answer is not part of this round.",
|
||||||
|
"da": "Det valgte svar er ikke en del af denne runde."
|
||||||
|
},
|
||||||
|
"selected_text_invalid": {
|
||||||
|
"en": "Selected text must be between 1 and 255 characters.",
|
||||||
|
"da": "Valgt tekst skal være mellem 1 og 255 tegn."
|
||||||
|
},
|
||||||
|
"session_code_required": {
|
||||||
|
"en": "Session code is required",
|
||||||
|
"da": "Sessionskode er påkrævet"
|
||||||
|
},
|
||||||
"session_not_found": {
|
"session_not_found": {
|
||||||
"en": "Session not found",
|
"en": "Session not found",
|
||||||
"da": "Session blev ikke fundet"
|
"da": "Session blev ikke fundet"
|
||||||
@@ -315,69 +482,17 @@
|
|||||||
"en": "Session is not joinable",
|
"en": "Session is not joinable",
|
||||||
"da": "Sessionen kan ikke joine nu"
|
"da": "Sessionen kan ikke joine nu"
|
||||||
},
|
},
|
||||||
"nickname_taken": {
|
"session_token_required": {
|
||||||
"en": "Nickname already taken",
|
"en": "Session token is required.",
|
||||||
"da": "Kaldenavnet er allerede taget"
|
"da": "Session-token er påkrævet."
|
||||||
},
|
|
||||||
"category_slug_required": {
|
|
||||||
"en": "category_slug is required",
|
|
||||||
"da": "category_slug er påkrævet"
|
|
||||||
},
|
|
||||||
"category_not_found": {
|
|
||||||
"en": "Category not found",
|
|
||||||
"da": "Kategori blev ikke fundet"
|
|
||||||
},
|
|
||||||
"round_start_invalid_phase": {
|
|
||||||
"en": "Round can only be started from lobby",
|
|
||||||
"da": "Runden kan kun startes fra lobbyen"
|
|
||||||
},
|
|
||||||
"round_already_configured": {
|
|
||||||
"en": "Round already configured",
|
|
||||||
"da": "Runden er allerede konfigureret"
|
|
||||||
},
|
|
||||||
"category_has_no_questions": {
|
|
||||||
"en": "Category has no active questions",
|
|
||||||
"da": "Kategorien har ingen aktive spørgsmål"
|
|
||||||
},
|
},
|
||||||
"show_question_invalid_phase": {
|
"show_question_invalid_phase": {
|
||||||
"en": "Question can only be shown in lie phase",
|
"en": "Question can only be shown in lie phase",
|
||||||
"da": "Spørgsmålet kan kun vises i løgnefasen"
|
"da": "Spørgsmålet kan kun vises i løgnefasen"
|
||||||
},
|
},
|
||||||
"round_config_missing": {
|
"start_next_round_invalid_phase": {
|
||||||
"en": "Round config missing",
|
"en": "Next round can only start from scoreboard phase.",
|
||||||
"da": "Rundekonfiguration mangler"
|
"da": "Næste runde kan kun starte fra scoreboard-fasen."
|
||||||
},
|
|
||||||
"question_already_shown": {
|
|
||||||
"en": "Question already shown for this round",
|
|
||||||
"da": "Spørgsmålet er allerede vist for denne runde"
|
|
||||||
},
|
|
||||||
"no_available_questions": {
|
|
||||||
"en": "No available questions in category",
|
|
||||||
"da": "Ingen tilgængelige spørgsmål i kategorien"
|
|
||||||
},
|
|
||||||
"mix_answers_invalid_phase": {
|
|
||||||
"en": "Answers can only be mixed in lie or guess phase",
|
|
||||||
"da": "Svar kan kun blandes i løgne- eller gættefasen"
|
|
||||||
},
|
|
||||||
"round_question_not_found": {
|
|
||||||
"en": "Round question not found",
|
|
||||||
"da": "Rundespørgsmål blev ikke fundet"
|
|
||||||
},
|
|
||||||
"not_enough_answers_to_mix": {
|
|
||||||
"en": "Not enough answers to mix",
|
|
||||||
"da": "Ikke nok svar at blande"
|
|
||||||
},
|
|
||||||
"host_only_start_round": {
|
|
||||||
"en": "Only host can start round",
|
|
||||||
"da": "Kun værten kan starte runden"
|
|
||||||
},
|
|
||||||
"host_only_show_question": {
|
|
||||||
"en": "Only host can show question",
|
|
||||||
"da": "Kun værten kan vise spørgsmålet"
|
|
||||||
},
|
|
||||||
"host_only_mix_answers": {
|
|
||||||
"en": "Only host can mix answers",
|
|
||||||
"da": "Kun værten kan blande svar"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -396,27 +511,50 @@
|
|||||||
"fallback": "Use default locale when requested locale is unsupported or key translation is missing."
|
"fallback": "Use default locale when requested locale is unsupported or key translation is missing."
|
||||||
},
|
},
|
||||||
"backend_to_frontend_error_keys": {
|
"backend_to_frontend_error_keys": {
|
||||||
"session_code_required": "session_code_required",
|
"calculate_scores_invalid_phase": "unknown",
|
||||||
|
"category_has_no_questions": "start_round_failed",
|
||||||
|
"category_not_found": "start_round_failed",
|
||||||
|
"category_slug_required": "start_round_failed",
|
||||||
|
"finish_game_invalid_phase": "unknown",
|
||||||
|
"guess_already_submitted": "unknown",
|
||||||
|
"guess_submission_invalid_phase": "unknown",
|
||||||
|
"guess_submission_window_closed": "unknown",
|
||||||
|
"host_only_action": "start_round_failed",
|
||||||
|
"host_only_calculate_scores": "unknown",
|
||||||
|
"host_only_finish_game": "unknown",
|
||||||
|
"host_only_mix_answers": "start_round_failed",
|
||||||
|
"host_only_show_question": "start_round_failed",
|
||||||
|
"host_only_start_next_round": "unknown",
|
||||||
|
"host_only_start_round": "start_round_failed",
|
||||||
|
"host_only_view_scoreboard": "unknown",
|
||||||
|
"invalid_player_session_token": "unknown",
|
||||||
|
"lie_already_submitted": "unknown",
|
||||||
|
"lie_submission_invalid_phase": "unknown",
|
||||||
|
"lie_submission_window_closed": "unknown",
|
||||||
|
"lie_text_invalid": "unknown",
|
||||||
|
"mix_answers_invalid_phase": "start_round_failed",
|
||||||
"nickname_invalid": "nickname_invalid",
|
"nickname_invalid": "nickname_invalid",
|
||||||
|
"nickname_taken": "nickname_taken",
|
||||||
|
"no_available_questions": "start_round_failed",
|
||||||
|
"no_guesses_submitted": "unknown",
|
||||||
|
"not_enough_answers_to_mix": "start_round_failed",
|
||||||
|
"player_id_required": "unknown",
|
||||||
|
"player_not_found_in_session": "unknown",
|
||||||
|
"question_already_shown": "start_round_failed",
|
||||||
|
"round_already_configured": "start_round_failed",
|
||||||
|
"round_config_missing": "start_round_failed",
|
||||||
|
"round_question_not_found": "start_round_failed",
|
||||||
|
"round_start_invalid_phase": "start_round_failed",
|
||||||
|
"scoreboard_invalid_phase": "unknown",
|
||||||
|
"scores_already_calculated": "unknown",
|
||||||
|
"selected_answer_invalid": "unknown",
|
||||||
|
"selected_text_invalid": "unknown",
|
||||||
|
"session_code_required": "session_code_required",
|
||||||
"session_not_found": "session_not_found",
|
"session_not_found": "session_not_found",
|
||||||
"session_not_joinable": "join_failed",
|
"session_not_joinable": "join_failed",
|
||||||
"nickname_taken": "nickname_taken",
|
"session_token_required": "unknown",
|
||||||
"category_slug_required": "start_round_failed",
|
|
||||||
"category_not_found": "start_round_failed",
|
|
||||||
"round_start_invalid_phase": "start_round_failed",
|
|
||||||
"round_already_configured": "start_round_failed",
|
|
||||||
"host_only_start_round": "start_round_failed",
|
|
||||||
"host_only_show_question": "start_round_failed",
|
|
||||||
"host_only_mix_answers": "start_round_failed",
|
|
||||||
"host_only_action": "start_round_failed",
|
|
||||||
"category_has_no_questions": "start_round_failed",
|
|
||||||
"show_question_invalid_phase": "start_round_failed",
|
"show_question_invalid_phase": "start_round_failed",
|
||||||
"round_config_missing": "start_round_failed",
|
"start_next_round_invalid_phase": "unknown"
|
||||||
"question_already_shown": "start_round_failed",
|
|
||||||
"no_available_questions": "start_round_failed",
|
|
||||||
"mix_answers_invalid_phase": "start_round_failed",
|
|
||||||
"round_question_not_found": "start_round_failed",
|
|
||||||
"not_enough_answers_to_mix": "start_round_failed"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user