Big visual overhaul docker compsoe file etc
Some checks failed
CI / test-and-quality (push) Failing after 4m4s
Some checks failed
CI / test-and-quality (push) Failing after 4m4s
This commit is contained in:
@@ -1,6 +1,69 @@
|
||||
from datetime import timedelta
|
||||
from typing import Literal
|
||||
|
||||
from .models import GameSession, Player, RoundConfig, RoundQuestion
|
||||
from .models import GameSession, Player, Question, RoundConfig, RoundQuestion
|
||||
|
||||
SessionViewerRole = Literal["host", "player", "public"]
|
||||
|
||||
NON_HOST_PROMPT_PHASES = {
|
||||
GameSession.Status.REVEAL,
|
||||
GameSession.Status.SCOREBOARD,
|
||||
GameSession.Status.FINISHED,
|
||||
}
|
||||
|
||||
HOST_PHASE_THEMES = {
|
||||
GameSession.Status.LOBBY: "host-atrium",
|
||||
GameSession.Status.LIE: "host-spotlight",
|
||||
GameSession.Status.GUESS: "host-signal",
|
||||
GameSession.Status.REVEAL: "host-verdict",
|
||||
GameSession.Status.SCOREBOARD: "host-podium",
|
||||
GameSession.Status.FINISHED: "host-finale",
|
||||
}
|
||||
|
||||
HOST_PHASE_ORNAMENTS = {
|
||||
GameSession.Status.LOBBY: "atrium-banner",
|
||||
GameSession.Status.LIE: "spotlight-beam",
|
||||
GameSession.Status.GUESS: "signal-grid",
|
||||
GameSession.Status.REVEAL: "verdict-wave",
|
||||
GameSession.Status.SCOREBOARD: "podium-ribbon",
|
||||
GameSession.Status.FINISHED: "finale-burst",
|
||||
}
|
||||
|
||||
PLAYER_IDENTITY_TONES = (
|
||||
"ember",
|
||||
"lagoon",
|
||||
"gold",
|
||||
"sage",
|
||||
"coral",
|
||||
)
|
||||
|
||||
PLAYER_IDENTITY_ICONS = (
|
||||
"spark",
|
||||
"wave",
|
||||
"comet",
|
||||
"leaf",
|
||||
"crown",
|
||||
)
|
||||
|
||||
PLAYER_PHASE_THEMES = {
|
||||
"join": "player-boarding",
|
||||
"lobby": "player-ready",
|
||||
"waiting": "player-holding",
|
||||
"lie": "player-ink",
|
||||
"guess": "player-choices",
|
||||
"reveal": "player-ripple",
|
||||
"result": "player-pennant",
|
||||
}
|
||||
|
||||
PLAYER_PHASE_ORNAMENTS = {
|
||||
"join": "boarding-pass",
|
||||
"lobby": "ready-lantern",
|
||||
"waiting": "holding-ring",
|
||||
"lie": "ink-trace",
|
||||
"guess": "choice-grid",
|
||||
"reveal": "ripple-flare",
|
||||
"result": "pennant-stack",
|
||||
}
|
||||
|
||||
|
||||
def build_player_ref(player: Player | None) -> dict | None:
|
||||
@@ -13,20 +76,66 @@ def build_player_ref(player: Player | None) -> dict | None:
|
||||
}
|
||||
|
||||
|
||||
def build_round_question_payload(round_question: RoundQuestion | None) -> dict | None:
|
||||
def _player_identity_token(nickname: str, join_order: int) -> str:
|
||||
initial = nickname.strip()[:1].upper() or "P"
|
||||
return f"{initial}{join_order}"
|
||||
|
||||
|
||||
def build_player_identity_payload(player: Player, *, join_order: int) -> dict:
|
||||
return {
|
||||
"token": _player_identity_token(player.nickname, join_order),
|
||||
"tone": PLAYER_IDENTITY_TONES[(join_order - 1) % len(PLAYER_IDENTITY_TONES)],
|
||||
"icon": PLAYER_IDENTITY_ICONS[(join_order - 1) % len(PLAYER_IDENTITY_ICONS)],
|
||||
}
|
||||
|
||||
|
||||
def build_session_players_payload(session: GameSession) -> list[dict]:
|
||||
joined_players = list(session.players.order_by("created_at", "id"))
|
||||
identities_by_id = {
|
||||
player.id: build_player_identity_payload(player, join_order=index)
|
||||
for index, player in enumerate(joined_players, start=1)
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
"id": player.id,
|
||||
"nickname": player.nickname,
|
||||
"score": player.score,
|
||||
"is_connected": player.is_connected,
|
||||
"identity": identities_by_id[player.id],
|
||||
}
|
||||
for player in sorted(joined_players, key=lambda entry: (entry.nickname.casefold(), entry.id))
|
||||
]
|
||||
|
||||
|
||||
def _can_view_round_prompt(session: GameSession, viewer_role: SessionViewerRole) -> bool:
|
||||
return viewer_role == "host" or session.status in NON_HOST_PROMPT_PHASES
|
||||
|
||||
|
||||
def build_round_question_payload(
|
||||
round_question: RoundQuestion | None,
|
||||
*,
|
||||
session: GameSession,
|
||||
viewer_role: SessionViewerRole,
|
||||
) -> dict | None:
|
||||
if round_question is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": round_question.id,
|
||||
"round_number": round_question.round_number,
|
||||
"prompt": round_question.question.prompt,
|
||||
"prompt": round_question.question.prompt if _can_view_round_prompt(session, viewer_role) else None,
|
||||
"shown_at": round_question.shown_at.isoformat(),
|
||||
"answers": [{"text": text} for text in (round_question.mixed_answers or [])],
|
||||
}
|
||||
|
||||
|
||||
def build_reveal_payload(round_question: RoundQuestion | None) -> dict | None:
|
||||
def build_reveal_payload(
|
||||
round_question: RoundQuestion | None,
|
||||
*,
|
||||
session: GameSession,
|
||||
viewer_role: SessionViewerRole,
|
||||
) -> dict | None:
|
||||
if round_question is None:
|
||||
return None
|
||||
|
||||
@@ -55,13 +164,34 @@ def build_reveal_payload(round_question: RoundQuestion | None) -> dict | None:
|
||||
return {
|
||||
"round_question_id": round_question.id,
|
||||
"round_number": round_question.round_number,
|
||||
"prompt": round_question.question.prompt,
|
||||
"prompt": round_question.question.prompt if _can_view_round_prompt(session, viewer_role) else None,
|
||||
"correct_answer": round_question.correct_answer,
|
||||
"lies": lies,
|
||||
"guesses": guesses,
|
||||
}
|
||||
|
||||
|
||||
def build_voice_cues_payload(
|
||||
voice_cues: dict | None,
|
||||
*,
|
||||
session: GameSession,
|
||||
viewer_role: SessionViewerRole,
|
||||
) -> dict | None:
|
||||
if voice_cues is None:
|
||||
return None
|
||||
|
||||
if viewer_role == "host":
|
||||
return voice_cues
|
||||
|
||||
# Keep non-host payloads role-correct: players can still receive generic intro/phase
|
||||
# metadata later if needed, but prompt-bearing cues stay presenter-only until reveal.
|
||||
return {
|
||||
**voice_cues,
|
||||
"question_prompt": None,
|
||||
"question_reveal": voice_cues.get("question_reveal") if session.status in NON_HOST_PROMPT_PHASES else None,
|
||||
}
|
||||
|
||||
|
||||
def build_leaderboard(session: GameSession) -> list[dict]:
|
||||
return list(
|
||||
Player.objects.filter(session=session)
|
||||
@@ -83,6 +213,197 @@ def build_lie_started_payload(session: GameSession, round_config: RoundConfig, r
|
||||
}
|
||||
|
||||
|
||||
def _wait_cue_keys() -> tuple[str, str]:
|
||||
return "host.presenter_scene_cue_wait_label", "host.presenter_scene_cue_wait_body"
|
||||
|
||||
|
||||
def _resolve_authored_scene_ornament(
|
||||
session: GameSession,
|
||||
current_round_question: RoundQuestion | None,
|
||||
) -> str | None:
|
||||
if session.status not in {
|
||||
GameSession.Status.LIE,
|
||||
GameSession.Status.GUESS,
|
||||
GameSession.Status.REVEAL,
|
||||
}:
|
||||
return None
|
||||
if current_round_question is None:
|
||||
return None
|
||||
|
||||
authored_ornament = current_round_question.question.scene_ornament
|
||||
if authored_ornament in Question.SceneOrnament.values:
|
||||
return authored_ornament
|
||||
return None
|
||||
|
||||
|
||||
def _build_host_phase_display_payload(
|
||||
session: GameSession,
|
||||
phase_view_model: dict,
|
||||
*,
|
||||
current_round_question: RoundQuestion | None = None,
|
||||
) -> dict:
|
||||
phase = session.status
|
||||
host = phase_view_model["host"]
|
||||
cue_label_key, cue_body_key = _wait_cue_keys()
|
||||
|
||||
if phase == GameSession.Status.FINISHED:
|
||||
cue_label_key = "host.presenter_scene_cue_finished_label"
|
||||
cue_body_key = "host.presenter_scene_cue_finished_body"
|
||||
title_key = "host.presenter_scene_title_finished"
|
||||
body_key = "host.presenter_scene_body_finished"
|
||||
elif phase == GameSession.Status.LOBBY:
|
||||
if host["can_start_round"]:
|
||||
cue_label_key = "host.presenter_scene_cue_start_label"
|
||||
cue_body_key = "host.presenter_scene_cue_start_body"
|
||||
title_key = "host.presenter_scene_title_lobby"
|
||||
body_key = "host.presenter_scene_body_lobby"
|
||||
elif phase == GameSession.Status.GUESS:
|
||||
if host["can_calculate_scores"]:
|
||||
cue_label_key = "host.presenter_scene_cue_reveal_label"
|
||||
cue_body_key = "host.presenter_scene_cue_reveal_body"
|
||||
title_key = "host.presenter_scene_title_guess"
|
||||
body_key = "host.presenter_scene_body_guess"
|
||||
elif phase == GameSession.Status.REVEAL:
|
||||
if host["can_reveal_scoreboard"]:
|
||||
cue_label_key = "host.presenter_scene_cue_scoreboard_label"
|
||||
cue_body_key = "host.presenter_scene_cue_scoreboard_body"
|
||||
title_key = "host.presenter_scene_title_reveal"
|
||||
body_key = "host.presenter_scene_body_reveal"
|
||||
elif phase == GameSession.Status.SCOREBOARD:
|
||||
if host["can_start_next_round"] or host["can_finish_game"]:
|
||||
cue_label_key = "host.presenter_scene_cue_close_label"
|
||||
cue_body_key = "host.presenter_scene_cue_close_body"
|
||||
title_key = "host.presenter_scene_title_scoreboard"
|
||||
body_key = "host.presenter_scene_body_scoreboard"
|
||||
else:
|
||||
if host["can_mix_answers"]:
|
||||
cue_label_key = "host.presenter_scene_cue_mix_label"
|
||||
cue_body_key = "host.presenter_scene_cue_mix_body"
|
||||
elif host["can_show_question"]:
|
||||
cue_label_key = "host.presenter_scene_cue_show_label"
|
||||
cue_body_key = "host.presenter_scene_cue_show_body"
|
||||
title_key = "host.presenter_scene_title"
|
||||
body_key = "host.presenter_scene_body_lie"
|
||||
|
||||
return {
|
||||
"theme": HOST_PHASE_THEMES.get(phase, HOST_PHASE_THEMES[GameSession.Status.LIE]),
|
||||
"ornament": _resolve_authored_scene_ornament(session, current_round_question)
|
||||
or HOST_PHASE_ORNAMENTS.get(phase, HOST_PHASE_ORNAMENTS[GameSession.Status.LIE]),
|
||||
"title_key": title_key,
|
||||
"body_key": body_key,
|
||||
"cue_label_key": cue_label_key,
|
||||
"cue_body_key": cue_body_key,
|
||||
}
|
||||
|
||||
|
||||
def _build_player_phase_display_payload(
|
||||
session: GameSession,
|
||||
phase_view_model: dict,
|
||||
viewer_role: SessionViewerRole,
|
||||
*,
|
||||
current_round_question: RoundQuestion | None = None,
|
||||
) -> dict:
|
||||
phase = session.status
|
||||
player = phase_view_model["player"]
|
||||
authored_ornament = _resolve_authored_scene_ornament(session, current_round_question)
|
||||
|
||||
if viewer_role != "player" and player["can_join"]:
|
||||
return {
|
||||
"theme": PLAYER_PHASE_THEMES["join"],
|
||||
"ornament": PLAYER_PHASE_ORNAMENTS["join"],
|
||||
"title_key": "player.player_scene_title_join",
|
||||
"body_key": "player.player_scene_body_join",
|
||||
"cue_label_key": "player.player_scene_cue_join_label",
|
||||
"cue_body_key": "player.player_scene_cue_join_body",
|
||||
}
|
||||
|
||||
if player["can_submit_lie"]:
|
||||
return {
|
||||
"theme": PLAYER_PHASE_THEMES["lie"],
|
||||
"ornament": authored_ornament or PLAYER_PHASE_ORNAMENTS["lie"],
|
||||
"title_key": "player.submit_lie",
|
||||
"body_key": "player.phase_summary_lie",
|
||||
"cue_label_key": "player.active_scene_cue_lie_label",
|
||||
"cue_body_key": "player.active_scene_cue_lie_body",
|
||||
}
|
||||
|
||||
if player["can_submit_guess"]:
|
||||
return {
|
||||
"theme": PLAYER_PHASE_THEMES["guess"],
|
||||
"ornament": authored_ornament or PLAYER_PHASE_ORNAMENTS["guess"],
|
||||
"title_key": "player.submit_guess",
|
||||
"body_key": "player.phase_summary_guess",
|
||||
"cue_label_key": "player.active_scene_cue_guess_label",
|
||||
"cue_body_key": "player.active_scene_cue_guess_body",
|
||||
}
|
||||
|
||||
if phase == GameSession.Status.REVEAL:
|
||||
return {
|
||||
"theme": PLAYER_PHASE_THEMES["reveal"],
|
||||
"ornament": authored_ornament or PLAYER_PHASE_ORNAMENTS["reveal"],
|
||||
"title_key": "player.reveal_title",
|
||||
"body_key": "player.phase_summary_reveal",
|
||||
"cue_label_key": "player.active_scene_cue_reveal_label",
|
||||
"cue_body_key": "player.active_scene_cue_reveal_body",
|
||||
}
|
||||
|
||||
if phase in {GameSession.Status.SCOREBOARD, GameSession.Status.FINISHED} or player["can_view_final_result"]:
|
||||
is_finished = phase == GameSession.Status.FINISHED or player["can_view_final_result"]
|
||||
return {
|
||||
"theme": PLAYER_PHASE_THEMES["result"],
|
||||
"ornament": PLAYER_PHASE_ORNAMENTS["result"],
|
||||
"title_key": "player.final_leaderboard" if is_finished else "player.scoreboard_title",
|
||||
"body_key": "player.phase_summary_finished" if is_finished else "player.phase_summary_scoreboard",
|
||||
"cue_label_key": "player.active_scene_cue_result_label",
|
||||
"cue_body_key": "player.active_scene_cue_result_body",
|
||||
}
|
||||
|
||||
if phase == GameSession.Status.LOBBY:
|
||||
return {
|
||||
"theme": PLAYER_PHASE_THEMES["lobby"],
|
||||
"ornament": PLAYER_PHASE_ORNAMENTS["lobby"],
|
||||
"title_key": "player.player_scene_title_lobby",
|
||||
"body_key": "player.player_scene_body_lobby",
|
||||
"cue_label_key": "player.player_scene_cue_lobby_label",
|
||||
"cue_body_key": "player.player_scene_cue_lobby_body",
|
||||
}
|
||||
|
||||
waiting_title_key = {
|
||||
GameSession.Status.LIE: "player.player_scene_title_waiting_lie",
|
||||
GameSession.Status.GUESS: "player.player_scene_title_waiting_guess",
|
||||
}.get(phase, "player.player_scene_title_waiting")
|
||||
return {
|
||||
"theme": PLAYER_PHASE_THEMES["waiting"],
|
||||
"ornament": authored_ornament or PLAYER_PHASE_ORNAMENTS["waiting"],
|
||||
"title_key": waiting_title_key,
|
||||
"body_key": "player.player_scene_body_waiting",
|
||||
"cue_label_key": "player.player_scene_cue_waiting_label",
|
||||
"cue_body_key": "player.player_scene_cue_waiting_body",
|
||||
}
|
||||
|
||||
|
||||
def build_phase_display_payload(
|
||||
session: GameSession,
|
||||
*,
|
||||
viewer_role: SessionViewerRole,
|
||||
phase_view_model: dict,
|
||||
current_round_question: RoundQuestion | None = None,
|
||||
) -> dict:
|
||||
if viewer_role == "host":
|
||||
return _build_host_phase_display_payload(
|
||||
session,
|
||||
phase_view_model,
|
||||
current_round_question=current_round_question,
|
||||
)
|
||||
|
||||
return _build_player_phase_display_payload(
|
||||
session,
|
||||
phase_view_model,
|
||||
viewer_role,
|
||||
current_round_question=current_round_question,
|
||||
)
|
||||
|
||||
|
||||
def build_phase_view_model(session: GameSession, *, players_count: int, has_round_question: bool) -> dict:
|
||||
status = session.status
|
||||
in_lobby = status == GameSession.Status.LOBBY
|
||||
@@ -112,10 +433,10 @@ def build_phase_view_model(session: GameSession, *, players_count: int, has_roun
|
||||
},
|
||||
"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_show_question": in_lie and has_round_question,
|
||||
"can_mix_answers": (in_lie or in_guess) and has_round_question,
|
||||
"can_calculate_scores": in_guess and has_round_question,
|
||||
"can_reveal_scoreboard": status == GameSession.Status.REVEAL,
|
||||
"can_start_next_round": in_scoreboard,
|
||||
"can_finish_game": in_scoreboard,
|
||||
},
|
||||
@@ -139,19 +460,41 @@ def build_session_detail_gameplay_payload(
|
||||
*,
|
||||
current_round_question: RoundQuestion | None,
|
||||
players_count: int,
|
||||
viewer_role: SessionViewerRole,
|
||||
voice_cues: dict | None = None,
|
||||
) -> dict:
|
||||
phase_view_model = build_phase_view_model(
|
||||
session,
|
||||
players_count=players_count,
|
||||
has_round_question=bool(current_round_question),
|
||||
)
|
||||
return {
|
||||
"round_question": build_round_question_payload(current_round_question),
|
||||
"reveal": build_reveal_payload(current_round_question)
|
||||
"round_question": build_round_question_payload(
|
||||
current_round_question,
|
||||
session=session,
|
||||
viewer_role=viewer_role,
|
||||
),
|
||||
"reveal": build_reveal_payload(
|
||||
current_round_question,
|
||||
session=session,
|
||||
viewer_role=viewer_role,
|
||||
)
|
||||
if session.status in {GameSession.Status.REVEAL, GameSession.Status.SCOREBOARD} and current_round_question
|
||||
else None,
|
||||
"scoreboard": build_scoreboard_phase_event(session)["payload"]["leaderboard"]
|
||||
if session.status in {GameSession.Status.SCOREBOARD, GameSession.Status.FINISHED}
|
||||
else None,
|
||||
"phase_view_model": build_phase_view_model(
|
||||
"voice_cues": build_voice_cues_payload(
|
||||
voice_cues,
|
||||
session=session,
|
||||
viewer_role=viewer_role,
|
||||
),
|
||||
"phase_view_model": phase_view_model,
|
||||
"phase_display": build_phase_display_payload(
|
||||
session,
|
||||
players_count=players_count,
|
||||
has_round_question=bool(current_round_question),
|
||||
viewer_role=viewer_role,
|
||||
phase_view_model=phase_view_model,
|
||||
current_round_question=current_round_question,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user