[READY][Gameplay] #310 Host transition idempotency and error catalog for scoreboard -> next round / finish #320

Merged
agw merged 45 commits from dev/issue-310-host-transition-idempotency-v2 into main 2026-03-18 06:52:04 +01:00
4 changed files with 61 additions and 46 deletions
Showing only changes of commit 319038555a - Show all commits

View File

@@ -70,6 +70,57 @@ def build_lie_started_payload(session: GameSession, round_config: RoundConfig, r
} }
def build_phase_view_model(session: GameSession, *, players_count: int, has_round_question: bool) -> dict:
status = session.status
in_lobby = status == GameSession.Status.LOBBY
in_lie = status == GameSession.Status.LIE
in_guess = status == GameSession.Status.GUESS
in_scoreboard = status == GameSession.Status.SCOREBOARD
in_finished = status == GameSession.Status.FINISHED
min_players_reached = players_count >= 3
max_players_allowed = players_count <= 5
return {
"status": status,
"current_phase": status,
"round_number": session.current_round,
"players_count": players_count,
"constraints": {
"min_players_to_start": 3,
"max_players_mvp": 5,
"min_players_reached": min_players_reached,
"max_players_allowed": max_players_allowed,
},
"readiness": {
"question_ready": has_round_question,
"scoreboard_ready": status in {GameSession.Status.REVEAL, GameSession.Status.SCOREBOARD, GameSession.Status.FINISHED},
"can_advance_to_next_round": in_scoreboard,
},
"host": {
"can_start_round": in_lobby and min_players_reached and max_players_allowed,
"can_show_question": False,
"can_mix_answers": False,
"can_calculate_scores": False,
"can_reveal_scoreboard": False,
"can_start_next_round": in_scoreboard,
"can_finish_game": in_scoreboard,
},
"player": {
"can_join": status in {
GameSession.Status.LOBBY,
GameSession.Status.LIE,
GameSession.Status.GUESS,
GameSession.Status.REVEAL,
GameSession.Status.SCOREBOARD,
},
"can_submit_lie": in_lie and has_round_question,
"can_submit_guess": in_guess and has_round_question,
"can_view_final_result": in_finished,
},
}
def build_start_next_round_response( def build_start_next_round_response(
session: GameSession, session: GameSession,
round_config: RoundConfig, round_config: RoundConfig,

View File

@@ -6,7 +6,7 @@ from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from fupogfakta.models import Category, GameSession, Guess, LieAnswer, Player, Question, RoundConfig, RoundQuestion, ScoreEvent from fupogfakta.models import Category, GameSession, Guess, LieAnswer, Player, Question, RoundConfig, RoundQuestion, ScoreEvent
from fupogfakta.payloads import build_lie_started_payload, build_reveal_payload from fupogfakta.payloads import build_lie_started_payload, build_phase_view_model, build_reveal_payload
from fupogfakta.services import ( from fupogfakta.services import (
finish_game, finish_game,
get_current_round_question, get_current_round_question,
@@ -279,9 +279,16 @@ class FupOgFaktaExtractionSliceTests(TestCase):
lie_payload = build_lie_started_payload(self.session, self.round_config, round_question) lie_payload = build_lie_started_payload(self.session, self.round_config, round_question)
reveal_payload = build_reveal_payload(round_question) reveal_payload = build_reveal_payload(round_question)
phase_view_model = build_phase_view_model(
self.session,
players_count=3,
has_round_question=True,
)
self.assertEqual(lie_payload["category"], {"slug": self.category.slug, "name": self.category.name}) self.assertEqual(lie_payload["category"], {"slug": self.category.slug, "name": self.category.name})
self.assertEqual(lie_payload["round_question_id"], round_question.id) self.assertEqual(lie_payload["round_question_id"], round_question.id)
self.assertEqual(reveal_payload["correct_answer"], "1989") self.assertEqual(reveal_payload["correct_answer"], "1989")
self.assertEqual(reveal_payload["lies"][0]["player_id"], lie.player_id) self.assertEqual(reveal_payload["lies"][0]["player_id"], lie.player_id)
self.assertEqual(reveal_payload["guesses"][0]["fooled_player_nickname"], self.bob.nickname) self.assertEqual(reveal_payload["guesses"][0]["fooled_player_nickname"], self.bob.nickname)
self.assertTrue(phase_view_model["host"]["can_start_round"])
self.assertFalse(phase_view_model["host"]["can_finish_game"])

View File

@@ -59,6 +59,7 @@ class LobbyGameplayExtractionTests(TestCase):
self.assertIs(lobby_views._promote_reveal_to_scoreboard, gameplay_services.promote_reveal_to_scoreboard) self.assertIs(lobby_views._promote_reveal_to_scoreboard, gameplay_services.promote_reveal_to_scoreboard)
self.assertIs(lobby_views._start_next_round, gameplay_services.start_next_round) self.assertIs(lobby_views._start_next_round, gameplay_services.start_next_round)
self.assertIs(lobby_views._finish_game, gameplay_services.finish_game) self.assertIs(lobby_views._finish_game, gameplay_services.finish_game)
self.assertIs(lobby_views._build_phase_view_model, gameplay_payloads.build_phase_view_model)
self.assertIs(lobby_views._build_scoreboard_phase_event, gameplay_payloads.build_scoreboard_phase_event) self.assertIs(lobby_views._build_scoreboard_phase_event, gameplay_payloads.build_scoreboard_phase_event)
def test_start_next_round_view_source_stays_http_thin(self): def test_start_next_round_view_source_stays_http_thin(self):

View File

@@ -12,6 +12,7 @@ from fupogfakta.models import Category, GameSession, Guess, LieAnswer, Player, Q
from fupogfakta.payloads import ( from fupogfakta.payloads import (
build_leaderboard as _build_leaderboard, build_leaderboard as _build_leaderboard,
build_lie_started_payload as _build_lie_started_payload, build_lie_started_payload as _build_lie_started_payload,
build_phase_view_model as _build_phase_view_model,
build_reveal_payload as _build_reveal_payload, build_reveal_payload as _build_reveal_payload,
build_scoreboard_phase_event as _build_scoreboard_phase_event, build_scoreboard_phase_event as _build_scoreboard_phase_event,
) )
@@ -82,51 +83,6 @@ def _maybe_promote_reveal_to_scoreboard(session: GameSession) -> GameSession:
def _build_phase_view_model(session: GameSession, *, players_count: int, has_round_question: bool) -> dict:
status = session.status
in_lobby = status == GameSession.Status.LOBBY
in_lie = status == GameSession.Status.LIE
in_guess = status == GameSession.Status.GUESS
in_scoreboard = status == GameSession.Status.SCOREBOARD
in_finished = status == GameSession.Status.FINISHED
min_players_reached = players_count >= 3
max_players_allowed = players_count <= 5
return {
"status": status,
"current_phase": status,
"round_number": session.current_round,
"players_count": players_count,
"constraints": {
"min_players_to_start": 3,
"max_players_mvp": 5,
"min_players_reached": min_players_reached,
"max_players_allowed": max_players_allowed,
},
"readiness": {
"question_ready": has_round_question,
"scoreboard_ready": status in {GameSession.Status.REVEAL, GameSession.Status.SCOREBOARD, GameSession.Status.FINISHED},
"can_advance_to_next_round": in_scoreboard,
},
"host": {
"can_start_round": in_lobby and min_players_reached and max_players_allowed,
"can_show_question": False,
"can_mix_answers": False,
"can_calculate_scores": False,
"can_reveal_scoreboard": False,
"can_start_next_round": in_scoreboard,
"can_finish_game": in_scoreboard,
},
"player": {
"can_join": status in JOINABLE_STATUSES,
"can_submit_lie": in_lie and has_round_question,
"can_submit_guess": in_guess and has_round_question,
"can_view_final_result": in_finished,
},
}
@require_POST @require_POST
@login_required @login_required
def create_session(request: HttpRequest) -> JsonResponse: def create_session(request: HttpRequest) -> JsonResponse: