Expand questions generation

This commit is contained in:
vlbuzilov 2026-05-06 10:07:02 +02:00
parent 4fbd2eaf10
commit 5f6c7c6551
5 changed files with 132 additions and 11 deletions

View file

@ -87,6 +87,22 @@ export function buildAudioMetadataQuestion(
points: 10,
});
}
const trackNames = topTracks.map((t) => t.name);
const trackNameOptions = buildOptionsWithCorrect(
randomTopTrack.name,
trackNames,
4,
);
if (trackNameOptions) {
questions.push({
type: "choice",
text: `What is the name of this track by ${correctArtist}?`,
options: trackNameOptions,
correct: 0,
points: 10,
});
}
}
if (randomTopTrack.albumName) {

View file

@ -1,7 +1,13 @@
import { and, eq, inArray } from "drizzle-orm";
import type { db } from "../db";
import { topArtist as topArtistTable, topTrack as topTrackTable } from "../db/schema";
import type { Question } from "../party-types";
import type { PartyAnalytics } from "./question-utils";
import { buildQuestionWindow, getQuestionRange } from "./question-utils";
import {
buildQuestionWindow,
getQuestionRange,
type PartyAnalytics,
type PartyQuestionMember,
} from "./question-utils";
type NumericQuestion = Omit<
Extract<Question, { type: "numeric" }>,
@ -12,6 +18,7 @@ type BuildNumericQuestionInput = {
db: typeof db;
analytics: PartyAnalytics;
index: number;
members: PartyQuestionMember[];
};
async function getAlbumReleaseYear({
@ -22,12 +29,8 @@ async function getAlbumReleaseYear({
const trackName = analytics?.storyClusters?.[0]?.tracks?.[0]?.name;
const track = trackName
? await db.query.track.findFirst({
where: {
name: trackName,
},
with: {
album: true,
},
where: { name: trackName },
with: { album: true },
})
: null;
const correct =
@ -43,8 +46,68 @@ async function getAlbumReleaseYear({
};
}
async function countTopTrackListeners({
db,
analytics,
members,
}: BuildNumericQuestionInput): Promise<NumericQuestion | null> {
const trackName = analytics?.storyClusters?.[0]?.tracks?.[0]?.name;
if (!trackName || members.length === 0) return null;
const dbTrack = await db.query.track.findFirst({ where: { name: trackName } });
if (!dbTrack) return null;
const memberIds = members.map((m) => m.userId);
const entries = await db
.select({ userId: topTrackTable.userId })
.from(topTrackTable)
.where(and(eq(topTrackTable.trackId, dbTrack.id), inArray(topTrackTable.userId, memberIds)));
const correct = new Set(entries.map((e) => e.userId)).size;
return {
type: "numeric",
text: `For how many players in the party is "${trackName}" a top track?`,
correct,
range: { min: 0, max: members.length },
points: 10,
};
}
async function countFavouriteArtistListeners({
db,
analytics,
members,
}: BuildNumericQuestionInput): Promise<NumericQuestion | null> {
const artistName = analytics?.storyClusters?.[0]?.artists?.[0]?.name;
if (!artistName || members.length === 0) return null;
const dbArtist = await db.query.artist.findFirst({ where: { name: artistName } });
if (!dbArtist) return null;
const memberIds = members.map((m) => m.userId);
const entries = await db
.select({ userId: topArtistTable.userId })
.from(topArtistTable)
.where(and(eq(topArtistTable.artistId, dbArtist.id), inArray(topArtistTable.userId, memberIds)));
const correct = new Set(entries.map((e) => e.userId)).size;
return {
type: "numeric",
text: `How many players in the party have "${artistName}" as a favourite artist?`,
correct,
range: { min: 0, max: members.length },
points: 10,
};
}
export async function buildNumericQuestion(
input: BuildNumericQuestionInput,
): Promise<Question> {
return buildQuestionWindow(await getAlbumReleaseYear(input));
}
const questions: NumericQuestion[] = [];
questions.push(await getAlbumReleaseYear(input));
const topTrackQ = await countTopTrackListeners(input);
if (topTrackQ) questions.push(topTrackQ);
const artistQ = await countFavouriteArtistListeners(input);
if (artistQ) questions.push(artistQ);
const question = questions[input.index % questions.length] ?? questions[0];
if (!question) throw new Error("Question not found");
return buildQuestionWindow(question);
}

View file

@ -29,6 +29,6 @@ export async function generatePartyQuestion({
return type === "audio-metadata"
? buildAudioMetadataQuestion(analytics, index)
: type === "numeric"
? buildNumericQuestion({ db: dbClient, analytics, index })
? buildNumericQuestion({ db: dbClient, analytics, index, members })
: buildSocialQuestion(quizState, analytics, members, index);
}

View file

@ -233,3 +233,18 @@ function fallbackPlayerNames(count: number): string[] {
(_, index) => `Player ${String.fromCharCode(65 + index)}`,
);
}
export function buildMemberPairOptions(
members: PartyQuestionMember[],
correctPair: string,
): string[] | null {
if (members.length < 3) return null;
const pairs: string[] = [correctPair];
for (let i = 0; i < members.length; i++) {
for (let j = i + 1; j < members.length; j++) {
const pair = `${members[i]!.name} & ${members[j]!.name}`;
if (pair !== correctPair) pairs.push(pair);
}
}
return pairs.length >= 2 ? pairs.slice(0, 4) : null;
}

View file

@ -1,6 +1,7 @@
import type { Question, QuizState } from "../party-types";
import {
buildMemberOptions,
buildMemberPairOptions,
buildQuestionWindow,
getCurrentLeader,
getMostAlignedMember,
@ -68,6 +69,32 @@ export function buildSocialQuestion(
correct: 0,
points: 10,
});
questions.push({
type: "choice",
text: `"${randomTrack.name}" appears most in which player's listening history?`,
options: buildMemberOptions(topListener, members),
correct: 0,
points: 10,
});
}
}
if (members.length >= 3 && analytics?.pairwise?.length) {
const topPair = analytics.pairwise[0];
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({
type: "choice",
text: "Which two players share the most musical taste?",
options: pairOptions,
correct: 0,
points: 10,
});
}
}
}