fix(gameplay): harden scoreboard -> next round bootstrap invariants (#300) #305
112
lobby/tests.py
112
lobby/tests.py
@@ -867,6 +867,79 @@ class CanonicalRoundFlowTests(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_canonical_round_flow_bootstraps_second_round_without_first_round_carry_over(self):
|
||||
self.client.login(username="host_canonical", password="secret123")
|
||||
extra_question = Question.objects.create(
|
||||
category=self.category,
|
||||
prompt="Hvem malede Mona Lisa?",
|
||||
correct_answer="Da Vinci",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
start_response = self.client.post(
|
||||
reverse("lobby:start_round", kwargs={"code": self.session.code}),
|
||||
data={"category_slug": self.category.slug},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(start_response.status_code, 201)
|
||||
first_round_question_id = start_response.json()["round_question"]["id"]
|
||||
first_round_prompt = start_response.json()["round_question"]["prompt"]
|
||||
first_round_correct_answer = RoundQuestion.objects.get(pk=first_round_question_id).correct_answer
|
||||
second_question = extra_question if first_round_prompt == self.question.prompt else self.question
|
||||
|
||||
final_lie_response = None
|
||||
for index, player in enumerate(self.players, start=1):
|
||||
lie_response = self.client.post(
|
||||
reverse("lobby:submit_lie", kwargs={"code": self.session.code, "round_question_id": first_round_question_id}),
|
||||
data={"player_id": player.id, "session_token": player.session_token, "text": f"Første løgn {index}"},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(lie_response.status_code, 201)
|
||||
final_lie_response = lie_response
|
||||
|
||||
self.assertIsNotNone(final_lie_response)
|
||||
|
||||
for player, selected_text in zip(
|
||||
self.players,
|
||||
[first_round_correct_answer, first_round_correct_answer, first_round_correct_answer],
|
||||
strict=True,
|
||||
):
|
||||
guess_response = self.client.post(
|
||||
reverse("lobby:submit_guess", kwargs={"code": self.session.code, "round_question_id": first_round_question_id}),
|
||||
data={"player_id": player.id, "session_token": player.session_token, "selected_text": selected_text},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(guess_response.status_code, 201)
|
||||
|
||||
scoreboard_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
||||
self.assertEqual(scoreboard_payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||
self.assertEqual(scoreboard_payload["round_question"]["id"], first_round_question_id)
|
||||
self.assertIsNotNone(scoreboard_payload["reveal"])
|
||||
self.assertIsNotNone(scoreboard_payload["scoreboard"])
|
||||
self.assertGreaterEqual(len(scoreboard_payload["reveal"]["guesses"]), 1)
|
||||
|
||||
next_round_response = self.client.post(reverse("lobby:start_next_round", kwargs={"code": self.session.code}))
|
||||
self.assertEqual(next_round_response.status_code, 200)
|
||||
self.assertEqual(next_round_response.json()["session"]["status"], GameSession.Status.LIE)
|
||||
self.assertEqual(next_round_response.json()["session"]["current_round"], 2)
|
||||
|
||||
detail_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
||||
self.assertEqual(detail_payload["session"]["status"], GameSession.Status.LIE)
|
||||
self.assertEqual(detail_payload["session"]["current_round"], 2)
|
||||
self.assertEqual(detail_payload["phase_view_model"]["current_phase"], GameSession.Status.LIE)
|
||||
self.assertIsNone(detail_payload["reveal"])
|
||||
self.assertIsNone(detail_payload["scoreboard"])
|
||||
self.assertEqual(detail_payload["round_question"]["round_number"], 2)
|
||||
self.assertNotEqual(detail_payload["round_question"]["id"], first_round_question_id)
|
||||
self.assertEqual(detail_payload["round_question"]["prompt"], second_question.prompt)
|
||||
self.assertEqual(detail_payload["round_question"]["answers"], [])
|
||||
|
||||
round_two_question = RoundQuestion.objects.get(session=self.session, round_number=2)
|
||||
self.assertEqual(round_two_question.question, second_question)
|
||||
self.assertEqual(round_two_question.lies.count(), 0)
|
||||
self.assertEqual(round_two_question.guesses.count(), 0)
|
||||
self.assertEqual(round_two_question.mixed_answers, [])
|
||||
|
||||
@patch("lobby.views.sync_broadcast_phase_event")
|
||||
@patch("lobby.views._resolve_scores")
|
||||
@patch("lobby.views.GameSession.objects.get")
|
||||
@@ -1284,6 +1357,45 @@ class RevealRoundFlowTests(TestCase):
|
||||
self.assertEqual(mock_sync_broadcast_phase_event.call_args.args[0], self.session.code)
|
||||
self.assertEqual(mock_sync_broadcast_phase_event.call_args.args[1], "phase.lie_started")
|
||||
|
||||
def test_start_next_round_clears_existing_next_round_bootstrap_state(self):
|
||||
self.client.login(username="host_reveal", password="secret123")
|
||||
self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code}))
|
||||
|
||||
stale_round_question = RoundQuestion.objects.create(
|
||||
session=self.session,
|
||||
round_number=2,
|
||||
question=self.next_question,
|
||||
correct_answer=self.next_question.correct_answer,
|
||||
mixed_answers=["Stale truth", "Stale lie"],
|
||||
)
|
||||
LieAnswer.objects.create(round_question=stale_round_question, player=self.player_one, text="Stale lie")
|
||||
Guess.objects.create(
|
||||
round_question=stale_round_question,
|
||||
player=self.player_two,
|
||||
selected_text="Stale truth",
|
||||
is_correct=True,
|
||||
)
|
||||
|
||||
response = self.client.post(reverse("lobby:start_next_round", kwargs={"code": self.session.code}))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.session.refresh_from_db()
|
||||
stale_round_question.refresh_from_db()
|
||||
self.assertEqual(self.session.status, GameSession.Status.LIE)
|
||||
self.assertEqual(self.session.current_round, 2)
|
||||
self.assertEqual(response.json()["round_question"]["id"], stale_round_question.id)
|
||||
self.assertEqual(stale_round_question.mixed_answers, [])
|
||||
self.assertEqual(stale_round_question.lies.count(), 0)
|
||||
self.assertEqual(stale_round_question.guesses.count(), 0)
|
||||
|
||||
detail_payload = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code})).json()
|
||||
self.assertEqual(detail_payload["session"]["status"], GameSession.Status.LIE)
|
||||
self.assertEqual(detail_payload["session"]["current_round"], 2)
|
||||
self.assertEqual(detail_payload["round_question"]["id"], stale_round_question.id)
|
||||
self.assertEqual(detail_payload["round_question"]["answers"], [])
|
||||
self.assertIsNone(detail_payload["reveal"])
|
||||
self.assertIsNone(detail_payload["scoreboard"])
|
||||
|
||||
def test_start_next_round_requires_host(self):
|
||||
self.session.status = GameSession.Status.SCOREBOARD
|
||||
self.session.save(update_fields=["status"])
|
||||
|
||||
@@ -193,6 +193,16 @@ def _prepare_mixed_answers(round_question: RoundQuestion) -> list[str]:
|
||||
|
||||
|
||||
|
||||
def _reset_round_question_bootstrap_state(round_question: RoundQuestion) -> RoundQuestion:
|
||||
Guess.objects.filter(round_question=round_question).delete()
|
||||
LieAnswer.objects.filter(round_question=round_question).delete()
|
||||
if round_question.mixed_answers:
|
||||
round_question.mixed_answers = []
|
||||
round_question.save(update_fields=["mixed_answers"])
|
||||
return round_question
|
||||
|
||||
|
||||
|
||||
def _resolve_scores(session: GameSession, round_question: RoundQuestion, round_config: RoundConfig) -> tuple[list[ScoreEvent], list[dict]]:
|
||||
guesses = list(round_question.guesses.select_related("player"))
|
||||
if not guesses:
|
||||
@@ -1116,7 +1126,9 @@ def start_next_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
locked_session.current_round = next_round_number
|
||||
|
||||
try:
|
||||
round_question = _select_round_question(locked_session, next_round_config)
|
||||
round_question = _reset_round_question_bootstrap_state(
|
||||
_select_round_question(locked_session, next_round_config)
|
||||
)
|
||||
except ValueError as exc:
|
||||
return api_error(request, code=str(exc), status=400)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user