Compare commits
2 Commits
feature/vi
...
dev/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c524d7d2d | |||
| f0ebc25da7 |
@@ -4,6 +4,7 @@ import type {
|
|||||||
HealthResponse,
|
HealthResponse,
|
||||||
JoinSessionResponse,
|
JoinSessionResponse,
|
||||||
MixAnswersResponse,
|
MixAnswersResponse,
|
||||||
|
RevealPayload,
|
||||||
ScoreboardResponse,
|
ScoreboardResponse,
|
||||||
SessionDetailResponse,
|
SessionDetailResponse,
|
||||||
ShowQuestionResponse,
|
ShowQuestionResponse,
|
||||||
@@ -68,6 +69,57 @@ export function mapHealthResponse(payload: unknown): HealthResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapRevealPayload(payload: unknown, path: string): RevealPayload {
|
||||||
|
const reveal = asRecord(payload, path);
|
||||||
|
const liesRaw = reveal.lies;
|
||||||
|
const guessesRaw = reveal.guesses;
|
||||||
|
|
||||||
|
if (!Array.isArray(liesRaw)) {
|
||||||
|
throw new Error(`Invalid API contract: expected array at ${path}.lies`);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(guessesRaw)) {
|
||||||
|
throw new Error(`Invalid API contract: expected array at ${path}.guesses`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
round_question_id: readNumber(reveal, 'round_question_id', path),
|
||||||
|
round_number: readNumber(reveal, 'round_number', path),
|
||||||
|
prompt: readString(reveal, 'prompt', path),
|
||||||
|
correct_answer: readString(reveal, 'correct_answer', path),
|
||||||
|
lies: liesRaw.map((lie, index) => {
|
||||||
|
const liePath = `${path}.lies[${index}]`;
|
||||||
|
const record = asRecord(lie, liePath);
|
||||||
|
return {
|
||||||
|
player_id: readNumber(record, 'player_id', liePath),
|
||||||
|
nickname: readString(record, 'nickname', liePath),
|
||||||
|
text: readString(record, 'text', liePath),
|
||||||
|
created_at: readString(record, 'created_at', liePath)
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
guesses: guessesRaw.map((guess, index) => {
|
||||||
|
const guessPath = `${path}.guesses[${index}]`;
|
||||||
|
const record = asRecord(guess, guessPath);
|
||||||
|
const fooledPlayerId = record.fooled_player_id;
|
||||||
|
if (fooledPlayerId !== null && !isNumber(fooledPlayerId)) {
|
||||||
|
throw new Error(`Invalid API contract: expected number|null at ${guessPath}.fooled_player_id`);
|
||||||
|
}
|
||||||
|
const fooledPlayerNickname = record.fooled_player_nickname;
|
||||||
|
if (fooledPlayerNickname !== undefined && !isString(fooledPlayerNickname)) {
|
||||||
|
throw new Error(`Invalid API contract: expected string|undefined at ${guessPath}.fooled_player_nickname`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
player_id: readNumber(record, 'player_id', guessPath),
|
||||||
|
nickname: readString(record, 'nickname', guessPath),
|
||||||
|
selected_text: readString(record, 'selected_text', guessPath),
|
||||||
|
is_correct: readBoolean(record, 'is_correct', guessPath),
|
||||||
|
created_at: readString(record, 'created_at', guessPath),
|
||||||
|
fooled_player_id: fooledPlayerId,
|
||||||
|
...(fooledPlayerNickname === undefined ? {} : { fooled_player_nickname: fooledPlayerNickname })
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mapSessionDetail(payload: unknown): SessionDetailResponse {
|
function mapSessionDetail(payload: unknown): SessionDetailResponse {
|
||||||
const root = asRecord(payload, 'session_detail');
|
const root = asRecord(payload, 'session_detail');
|
||||||
const session = asRecord(root.session, 'session_detail.session');
|
const session = asRecord(root.session, 'session_detail.session');
|
||||||
@@ -97,6 +149,7 @@ function mapSessionDetail(payload: unknown): SessionDetailResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const revealRaw = root.reveal;
|
||||||
const phase = asRecord(root.phase_view_model, 'session_detail.phase_view_model');
|
const phase = asRecord(root.phase_view_model, 'session_detail.phase_view_model');
|
||||||
const constraints = asRecord(phase.constraints, 'session_detail.phase_view_model.constraints');
|
const constraints = asRecord(phase.constraints, 'session_detail.phase_view_model.constraints');
|
||||||
const host = asRecord(phase.host, 'session_detail.phase_view_model.host');
|
const host = asRecord(phase.host, 'session_detail.phase_view_model.host');
|
||||||
@@ -129,6 +182,7 @@ function mapSessionDetail(payload: unknown): SessionDetailResponse {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
round_question: roundQuestion,
|
round_question: roundQuestion,
|
||||||
|
reveal: revealRaw === null || revealRaw === undefined ? null : mapRevealPayload(revealRaw, 'session_detail.reveal'),
|
||||||
phase_view_model: {
|
phase_view_model: {
|
||||||
status: readString(phase, 'status', 'session_detail.phase_view_model'),
|
status: readString(phase, 'status', 'session_detail.phase_view_model'),
|
||||||
round_number: readNumber(phase, 'round_number', 'session_detail.phase_view_model'),
|
round_number: readNumber(phase, 'round_number', 'session_detail.phase_view_model'),
|
||||||
@@ -276,6 +330,7 @@ export function mapCalculateScoresResponse(payload: unknown): CalculateScoresRes
|
|||||||
round_number: readNumber(roundQuestion, 'round_number', 'calculate_scores.round_question')
|
round_number: readNumber(roundQuestion, 'round_number', 'calculate_scores.round_question')
|
||||||
},
|
},
|
||||||
events_created: readNumber(root, 'events_created', 'calculate_scores'),
|
events_created: readNumber(root, 'events_created', 'calculate_scores'),
|
||||||
|
reveal: mapRevealPayload(root.reveal, 'calculate_scores.reveal'),
|
||||||
leaderboard: leaderboardRaw.map((entry, index) => mapLeaderboardEntry(entry, `calculate_scores.leaderboard[${index}]`))
|
leaderboard: leaderboardRaw.map((entry, index) => mapLeaderboardEntry(entry, `calculate_scores.leaderboard[${index}]`))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,10 +57,38 @@ export interface PhaseViewModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RevealPlayerRef {
|
||||||
|
player_id: number;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RevealLie extends RevealPlayerRef {
|
||||||
|
text: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RevealGuess extends RevealPlayerRef {
|
||||||
|
selected_text: string;
|
||||||
|
is_correct: boolean;
|
||||||
|
created_at: string;
|
||||||
|
fooled_player_id: number | null;
|
||||||
|
fooled_player_nickname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RevealPayload {
|
||||||
|
round_question_id: number;
|
||||||
|
round_number: number;
|
||||||
|
prompt: string;
|
||||||
|
correct_answer: string;
|
||||||
|
lies: RevealLie[];
|
||||||
|
guesses: RevealGuess[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionDetailResponse {
|
export interface SessionDetailResponse {
|
||||||
session: SessionSummary;
|
session: SessionSummary;
|
||||||
players: SessionPlayer[];
|
players: SessionPlayer[];
|
||||||
round_question: SessionRoundQuestion | null;
|
round_question: SessionRoundQuestion | null;
|
||||||
|
reveal: RevealPayload | null;
|
||||||
phase_view_model: PhaseViewModel;
|
phase_view_model: PhaseViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +166,7 @@ export interface CalculateScoresResponse {
|
|||||||
round_number: number;
|
round_number: number;
|
||||||
};
|
};
|
||||||
events_created: number;
|
events_created: number;
|
||||||
|
reveal: RevealPayload;
|
||||||
leaderboard: Array<{ id: number; nickname: string; score: number }>;
|
leaderboard: Array<{ id: number; nickname: string; score: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('createAngularApiClient', () => {
|
|||||||
{ id: 3, nickname: 'Bo', score: 0, is_connected: false }
|
{ id: 3, nickname: 'Bo', score: 0, is_connected: false }
|
||||||
],
|
],
|
||||||
round_question: null,
|
round_question: null,
|
||||||
|
reveal: null,
|
||||||
phase_view_model: {
|
phase_view_model: {
|
||||||
status: 'lobby',
|
status: 'lobby',
|
||||||
round_number: 1,
|
round_number: 1,
|
||||||
@@ -84,6 +85,7 @@ describe('createAngularApiClient', () => {
|
|||||||
if (session.ok) {
|
if (session.ok) {
|
||||||
expect(session.data.session.code).toBe('ABCD12');
|
expect(session.data.session.code).toBe('ABCD12');
|
||||||
expect(session.data.session.host_id).toBe(1);
|
expect(session.data.session.host_id).toBe(1);
|
||||||
|
expect(session.data.reveal).toBeNull();
|
||||||
expect(session.data.phase_view_model.host.can_start_round).toBe(true);
|
expect(session.data.phase_view_model.host.can_start_round).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ describe('createAngularApiClient', () => {
|
|||||||
session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 2 },
|
session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 2 },
|
||||||
players: [],
|
players: [],
|
||||||
round_question: null,
|
round_question: null,
|
||||||
|
reveal: null,
|
||||||
phase_view_model: {
|
phase_view_model: {
|
||||||
status: 'lobby',
|
status: 'lobby',
|
||||||
round_number: 1,
|
round_number: 1,
|
||||||
@@ -189,6 +192,117 @@ describe('createAngularApiClient', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps canonical reveal payload from session detail in reveal phase', async () => {
|
||||||
|
const get = vi.fn<AngularHttpClientLike['get']>(async <T>(url: string) => {
|
||||||
|
if (url === '/lobby/sessions/ABCD12') {
|
||||||
|
return {
|
||||||
|
session: { code: 'ABCD12', status: 'reveal', host_id: 1, current_round: 1, players_count: 3 },
|
||||||
|
players: [
|
||||||
|
{ id: 2, nickname: 'Maja', score: 4, is_connected: true },
|
||||||
|
{ id: 3, nickname: 'Bo', score: 2, is_connected: true },
|
||||||
|
{ id: 4, nickname: 'Ida', score: 5, is_connected: true }
|
||||||
|
],
|
||||||
|
round_question: {
|
||||||
|
id: 77,
|
||||||
|
round_number: 1,
|
||||||
|
prompt: 'Hvem opfandt pæren?',
|
||||||
|
shown_at: '2026-03-01T16:00:00Z',
|
||||||
|
answers: [{ text: 'Edison' }, { text: 'Tesla' }]
|
||||||
|
},
|
||||||
|
reveal: {
|
||||||
|
round_question_id: 77,
|
||||||
|
round_number: 1,
|
||||||
|
prompt: 'Hvem opfandt pæren?',
|
||||||
|
correct_answer: 'Edison',
|
||||||
|
lies: [
|
||||||
|
{
|
||||||
|
player_id: 3,
|
||||||
|
nickname: 'Bo',
|
||||||
|
text: 'Tesla',
|
||||||
|
created_at: '2026-03-01T16:00:20Z'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
guesses: [
|
||||||
|
{
|
||||||
|
player_id: 2,
|
||||||
|
nickname: 'Maja',
|
||||||
|
selected_text: 'Tesla',
|
||||||
|
is_correct: false,
|
||||||
|
fooled_player_id: 3,
|
||||||
|
fooled_player_nickname: 'Bo',
|
||||||
|
created_at: '2026-03-01T16:01:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
player_id: 4,
|
||||||
|
nickname: 'Ida',
|
||||||
|
selected_text: 'Edison',
|
||||||
|
is_correct: true,
|
||||||
|
fooled_player_id: null,
|
||||||
|
created_at: '2026-03-01T16:01:02Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
phase_view_model: {
|
||||||
|
status: 'reveal',
|
||||||
|
round_number: 1,
|
||||||
|
players_count: 3,
|
||||||
|
constraints: {
|
||||||
|
min_players_to_start: 2,
|
||||||
|
max_players_mvp: 8,
|
||||||
|
min_players_reached: true,
|
||||||
|
max_players_allowed: true
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
can_start_round: false,
|
||||||
|
can_show_question: false,
|
||||||
|
can_mix_answers: false,
|
||||||
|
can_calculate_scores: false,
|
||||||
|
can_reveal_scoreboard: true,
|
||||||
|
can_start_next_round: true,
|
||||||
|
can_finish_game: true
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
can_join: false,
|
||||||
|
can_submit_lie: false,
|
||||||
|
can_submit_guess: false,
|
||||||
|
can_view_final_result: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw { status: 404, error: { error: 'Not found' } };
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = createAngularApiClient({ get, post: vi.fn() } as AngularHttpClientLike);
|
||||||
|
const session = await client.getSession('abcd12');
|
||||||
|
|
||||||
|
expect(session.ok).toBe(true);
|
||||||
|
if (session.ok) {
|
||||||
|
expect(session.data.reveal?.correct_answer).toBe('Edison');
|
||||||
|
expect(session.data.reveal?.lies[0]).toMatchObject({
|
||||||
|
player_id: 3,
|
||||||
|
nickname: 'Bo',
|
||||||
|
text: 'Tesla'
|
||||||
|
});
|
||||||
|
expect(session.data.reveal?.guesses[0]).toMatchObject({
|
||||||
|
player_id: 2,
|
||||||
|
nickname: 'Maja',
|
||||||
|
selected_text: 'Tesla',
|
||||||
|
is_correct: false,
|
||||||
|
fooled_player_id: 3,
|
||||||
|
fooled_player_nickname: 'Bo'
|
||||||
|
});
|
||||||
|
expect(session.data.reveal?.guesses[1]).toMatchObject({
|
||||||
|
player_id: 4,
|
||||||
|
nickname: 'Ida',
|
||||||
|
selected_text: 'Edison',
|
||||||
|
is_correct: true,
|
||||||
|
fooled_player_id: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('returns parse error when successful payload breaks typed contract', async () => {
|
it('returns parse error when successful payload breaks typed contract', async () => {
|
||||||
const http = {
|
const http = {
|
||||||
get: vi.fn<AngularHttpClientLike['get']>(async <T>() => ({ ok: true } as T)),
|
get: vi.fn<AngularHttpClientLike['get']>(async <T>() => ({ ok: true } as T)),
|
||||||
@@ -248,6 +362,39 @@ describe('createAngularApiClient', () => {
|
|||||||
session: { code: 'ABCD12', status: 'reveal', current_round: 1 },
|
session: { code: 'ABCD12', status: 'reveal', current_round: 1 },
|
||||||
round_question: { id: 77, round_number: 1 },
|
round_question: { id: 77, round_number: 1 },
|
||||||
events_created: 3,
|
events_created: 3,
|
||||||
|
reveal: {
|
||||||
|
round_question_id: 77,
|
||||||
|
round_number: 1,
|
||||||
|
prompt: 'Hvem opfandt pæren?',
|
||||||
|
correct_answer: 'Edison',
|
||||||
|
lies: [
|
||||||
|
{
|
||||||
|
player_id: 3,
|
||||||
|
nickname: 'Bo',
|
||||||
|
text: 'Tesla',
|
||||||
|
created_at: '2026-03-01T16:00:20Z'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
guesses: [
|
||||||
|
{
|
||||||
|
player_id: 2,
|
||||||
|
nickname: 'Maja',
|
||||||
|
selected_text: 'Tesla',
|
||||||
|
is_correct: false,
|
||||||
|
fooled_player_id: 3,
|
||||||
|
fooled_player_nickname: 'Bo',
|
||||||
|
created_at: '2026-03-01T16:01:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
player_id: 4,
|
||||||
|
nickname: 'Ida',
|
||||||
|
selected_text: 'Edison',
|
||||||
|
is_correct: true,
|
||||||
|
fooled_player_id: null,
|
||||||
|
created_at: '2026-03-01T16:01:02Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
leaderboard: [{ id: 2, nickname: 'Maja', score: 11 }]
|
leaderboard: [{ id: 2, nickname: 'Maja', score: 11 }]
|
||||||
} as T;
|
} as T;
|
||||||
}
|
}
|
||||||
@@ -305,6 +452,29 @@ describe('createAngularApiClient', () => {
|
|||||||
|
|
||||||
const calculateScores = await client.calculateScores('abcd12', 77);
|
const calculateScores = await client.calculateScores('abcd12', 77);
|
||||||
expect(calculateScores.ok).toBe(true);
|
expect(calculateScores.ok).toBe(true);
|
||||||
|
if (calculateScores.ok) {
|
||||||
|
expect(calculateScores.data.reveal.correct_answer).toBe('Edison');
|
||||||
|
expect(calculateScores.data.reveal.lies[0]).toMatchObject({
|
||||||
|
player_id: 3,
|
||||||
|
nickname: 'Bo',
|
||||||
|
text: 'Tesla'
|
||||||
|
});
|
||||||
|
expect(calculateScores.data.reveal.guesses[0]).toMatchObject({
|
||||||
|
player_id: 2,
|
||||||
|
nickname: 'Maja',
|
||||||
|
selected_text: 'Tesla',
|
||||||
|
is_correct: false,
|
||||||
|
fooled_player_id: 3,
|
||||||
|
fooled_player_nickname: 'Bo'
|
||||||
|
});
|
||||||
|
expect(calculateScores.data.reveal.guesses[1]).toMatchObject({
|
||||||
|
player_id: 4,
|
||||||
|
nickname: 'Ida',
|
||||||
|
selected_text: 'Edison',
|
||||||
|
is_correct: true,
|
||||||
|
fooled_player_id: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const scoreboard = await client.getScoreboard('abcd12');
|
const scoreboard = await client.getScoreboard('abcd12');
|
||||||
expect(scoreboard.ok).toBe(true);
|
expect(scoreboard.ok).toBe(true);
|
||||||
|
|||||||
107
lobby/tests.py
107
lobby/tests.py
@@ -687,6 +687,7 @@ class ScoreCalculationTests(TestCase):
|
|||||||
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_reveal(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,
|
||||||
@@ -715,6 +716,57 @@ class ScoreCalculationTests(TestCase):
|
|||||||
payload = response.json()
|
payload = response.json()
|
||||||
self.assertEqual(payload["session"]["status"], GameSession.Status.REVEAL)
|
self.assertEqual(payload["session"]["status"], GameSession.Status.REVEAL)
|
||||||
self.assertEqual(payload["events_created"], 2)
|
self.assertEqual(payload["events_created"], 2)
|
||||||
|
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(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"player_id": item["player_id"],
|
||||||
|
"nickname": item["nickname"],
|
||||||
|
"selected_text": item["selected_text"],
|
||||||
|
"is_correct": item["is_correct"],
|
||||||
|
"fooled_player_id": item["fooled_player_id"],
|
||||||
|
"fooled_player_nickname": item.get("fooled_player_nickname"),
|
||||||
|
}
|
||||||
|
for item in payload["reveal"]["guesses"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"player_id": self.player_one.id,
|
||||||
|
"nickname": "Luna",
|
||||||
|
"selected_text": "Tennis",
|
||||||
|
"is_correct": True,
|
||||||
|
"fooled_player_id": None,
|
||||||
|
"fooled_player_nickname": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"player_id": self.player_two.id,
|
||||||
|
"nickname": "Mads",
|
||||||
|
"selected_text": "Padel",
|
||||||
|
"is_correct": False,
|
||||||
|
"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,
|
||||||
|
"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()
|
||||||
@@ -1149,7 +1201,62 @@ 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_reveal_payload_for_active_round_question(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="Bluffer")
|
||||||
|
guesser = Player.objects.create(session=self.session, nickname="Guesser")
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
[
|
||||||
|
{"player_id": lie["player_id"], "nickname": lie["nickname"], "text": lie["text"]}
|
||||||
|
for lie in payload["reveal"]["lies"]
|
||||||
|
],
|
||||||
|
[{"player_id": liar.id, "nickname": "Bluffer", "text": "Tesla"}],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"player_id": guess["player_id"],
|
||||||
|
"nickname": guess["nickname"],
|
||||||
|
"selected_text": guess["selected_text"],
|
||||||
|
"is_correct": guess["is_correct"],
|
||||||
|
"fooled_player_id": guess["fooled_player_id"],
|
||||||
|
"fooled_player_nickname": guess.get("fooled_player_nickname"),
|
||||||
|
}
|
||||||
|
for guess in payload["reveal"]["guesses"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"player_id": guesser.id,
|
||||||
|
"nickname": "Guesser",
|
||||||
|
"selected_text": "Tesla",
|
||||||
|
"is_correct": False,
|
||||||
|
"fooled_player_id": liar.id,
|
||||||
|
"fooled_player_nickname": "Bluffer",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SessionDetailPhaseViewModelTests(TestCase):
|
class SessionDetailPhaseViewModelTests(TestCase):
|
||||||
|
|||||||
@@ -61,6 +61,52 @@ 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
|
||||||
@@ -238,6 +284,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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -909,6 +958,7 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
"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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user