diff --git a/api/src/party-types.ts b/api/src/party-types.ts index 6e3d538..03f23d4 100644 --- a/api/src/party-types.ts +++ b/api/src/party-types.ts @@ -83,7 +83,11 @@ export type QuizState = { }; export type PartySocketEvent = - | { type: "party_status"; party: Party|null; members: PartyMemberWithUser[] } + | { + type: "party_status"; + party: Party | null; + members: PartyMemberWithUser[]; + } | { type: "member_payload"; fromUserId: string; payload: unknown } | { type: "error"; message: string } | { type: "pong" }; diff --git a/api/src/party/question-utils.ts b/api/src/party/question-utils.ts index 3cd231a..ae8f557 100644 --- a/api/src/party/question-utils.ts +++ b/api/src/party/question-utils.ts @@ -14,6 +14,7 @@ export type PartyAnalytics = { name: string; artists?: { name: string }[]; albumName?: string; + memberScores?: { userId: string; score: number }[]; }[]; artists?: { name: string }[]; genres?: { name: string }[]; @@ -95,12 +96,28 @@ export function getTopClusterArtists(analytics: PartyAnalytics): string[] { ); } -export function getTopClusterTracks( - analytics: PartyAnalytics, -): Array<{ name: string; artists?: { name: string }[]; albumName?: string }> { +export function getTopClusterTracks(analytics: PartyAnalytics): Array<{ + name: string; + artists?: { name: string }[]; + albumName?: string; + memberScores?: { userId: string; score: number }[]; +}> { return analytics?.storyClusters?.[0]?.tracks ?? []; } +export function getTopTrackListener( + track: { memberScores?: { userId: string; score: number }[] }, + members: PartyQuestionMember[], +): PartyQuestionMember | null { + const topMember = (track.memberScores ?? []) + .slice() + .sort((a, b) => b.score - a.score) + .at(0); + + if (!topMember) return null; + return members.find((member) => member.userId === topMember.userId) ?? null; +} + export function buildOrderedOptions( values: Array, desiredCount: number, @@ -143,6 +160,17 @@ export function getCurrentLeader( ); } +export function hasClearLeader(quizState: { + scores: Record; +}): boolean { + const scores = Object.values(quizState.scores); + if (scores.length < 2) return false; + const ordered = scores.slice().sort((a, b) => b - a); + const [first, second] = ordered; + if (first === undefined || second === undefined) return false; + return first > second; +} + export function getMostDiverseMember( analytics: PartyAnalytics, members: PartyQuestionMember[], diff --git a/api/src/party/social-question-generator.ts b/api/src/party/social-question-generator.ts index b80c18f..fceae38 100644 --- a/api/src/party/social-question-generator.ts +++ b/api/src/party/social-question-generator.ts @@ -5,8 +5,12 @@ import { getCurrentLeader, getMostAlignedMember, getMostDiverseMember, + getTopClusterTracks, + getTopTrackListener, + hasClearLeader, type PartyAnalytics, type PartyQuestionMember, + pickRandom, } from "./question-utils"; export function buildSocialQuestion( @@ -16,35 +20,60 @@ export function buildSocialQuestion( index: number, ): Question { type ChoiceQuestion = Extract; - const leader = getCurrentLeader(quizState, members); - const diverse = getMostDiverseMember(analytics, members); - const aligned = getMostAlignedMember(analytics, members); - const questions: Array< Omit - > = [ - { + > = []; + + const hasMultipleMembers = members.length >= 2; + if (hasMultipleMembers && hasClearLeader(quizState)) { + const leader = getCurrentLeader(quizState, members); + questions.push({ type: "choice", text: "Who is leading the quiz right now?", options: buildMemberOptions(leader, members), correct: 0, points: 10, - }, - { + }); + } + + if (hasMultipleMembers) { + const diverse = getMostDiverseMember(analytics, members); + questions.push({ type: "choice", text: "Who looks like the most diverse listener in the party?", options: buildMemberOptions(diverse, members), correct: 0, points: 10, - }, - { + }); + + const aligned = getMostAlignedMember(analytics, members); + questions.push({ type: "choice", text: "Which member seems most aligned with the rest of the party?", options: buildMemberOptions(aligned, members), correct: 0, points: 10, - }, - ]; + }); + } + + const topTracks = getTopClusterTracks(analytics); + const randomTrack = pickRandom(topTracks); + if (randomTrack && hasMultipleMembers) { + const topListener = getTopTrackListener(randomTrack, members); + if (topListener) { + questions.push({ + type: "choice", + text: `Who listens the most to "${randomTrack.name}"?`, + options: buildMemberOptions(topListener, members), + correct: 0, + points: 10, + }); + } + } + + if (questions.length === 0) { + throw new Error("Question not found"); + } const question = questions[index % questions.length]; if (!question) throw new Error("Question not found"); diff --git a/api/src/workflows/quiz.ts b/api/src/workflows/quiz.ts index c529f8a..2604d80 100644 --- a/api/src/workflows/quiz.ts +++ b/api/src/workflows/quiz.ts @@ -101,9 +101,9 @@ export class QuizWorkflow extends ConfiguredInstance { } } break; - } + } - if (receivedPlayers.has(response.playerId)) continue; + if (receivedPlayers.has(response.playerId)) continue; receivedPlayers.add(response.playerId); const answeredAt = Date.now();