itpdp/api/src/party/social-question-generator.ts
2026-05-26 19:53:28 +02:00

150 lines
4 KiB
TypeScript

import type { db as Db } from "../db";
import type { Question, QuizState } from "../party-types";
import {
buildMemberOptions,
buildMemberPairOptions,
buildQuestionWindow,
getCurrentLeader,
getFairQuestionTracks,
getMostDiverseMember,
getTopTrackListener,
getTrackFairness,
hasClearLeader,
type PartyAnalytics,
type PartyQuestionMember,
pickQuestionCandidate,
type QuestionCandidate,
resolveQuestionSong,
} from "./question-utils";
export async function buildSocialQuestion(
dbClient: typeof Db,
quizState: QuizState,
analytics: PartyAnalytics,
members: PartyQuestionMember[],
index: number,
): Promise<Question | null> {
type ChoiceQuestion = Extract<Question, { type: "choice" }>;
const questions: Array<
QuestionCandidate<Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp">>
> = [];
const topSong = await resolveQuestionSong(dbClient, analytics);
const hasMultipleMembers = members.length >= 2;
if (hasMultipleMembers && hasClearLeader(quizState)) {
const leader = getCurrentLeader(quizState, members);
const options = buildMemberOptions(leader, members);
if (options) {
questions.push({
key: "social:leader",
subjectKey: `member:${leader.userId}`,
question: {
type: "choice",
text: "Who is leading the quiz right now?",
options,
correct: 0,
points: 10,
song: topSong ?? undefined,
},
});
}
}
if (hasMultipleMembers) {
const diverse = getMostDiverseMember(analytics, members);
if (diverse) {
const options = buildMemberOptions(diverse, members);
if (options) {
questions.push({
key: "social:diverse",
subjectKey: `member:${diverse.userId}`,
question: {
type: "choice",
text: "Who looks like the most diverse listener in the party?",
options,
correct: 0,
points: 10,
song: topSong ?? undefined,
},
});
}
}
}
const topTracks = getFairQuestionTracks(
analytics,
members,
quizState.history,
);
if (hasMultipleMembers) {
for (const topTrack of topTracks) {
const fairness = getTrackFairness(topTrack, members, quizState.history);
if (
fairness.memberCount < 2 &&
topTracks.some((track) => {
return (
getTrackFairness(track, members, quizState.history).memberCount > 1
);
})
) {
continue;
}
const topListener = getTopTrackListener(topTrack, members);
if (!topListener) continue;
const trackSong = await resolveQuestionSong(dbClient, analytics, {
trackName: topTrack.name,
artistNames: topTrack.artists?.map((artist) => artist.name),
albumName: topTrack.albumName,
});
const options = buildMemberOptions(topListener, members);
if (options) {
questions.push({
key: `social:track-listener:${topTrack.name}`,
subjectKey: `track:${topTrack.name}`,
fairness,
question: {
type: "choice",
text: `Who listens the most to "${topTrack.name}"?`,
options,
correct: 0,
points: 10,
song: trackSong ?? topSong ?? undefined,
},
});
}
}
}
if (members.length >= 3 && analytics?.groupSummary?.mostAlignedPair) {
const topPair = analytics.groupSummary.mostAlignedPair;
const memberA = members.find((m) => m.userId === topPair.userIdA);
const memberB = members.find((m) => m.userId === topPair.userIdB);
if (memberA && memberB) {
const correctPair = `${memberA.name} & ${memberB.name}`;
const pairOptions = buildMemberPairOptions(members, correctPair);
if (pairOptions) {
questions.push({
key: `social:pair:${memberA.userId}:${memberB.userId}`,
subjectKey: `pair:${[memberA.userId, memberB.userId].sort().join("|")}`,
question: {
type: "choice",
text: "Which two players share the most musical taste?",
options: pairOptions,
correct: 0,
points: 10,
song: topSong ?? undefined,
},
});
}
}
}
if (questions.length === 0) {
return null;
}
const question = pickQuestionCandidate(questions, quizState.history, index);
if (!question) return null;
return buildQuestionWindow(question);
}