import type { db as Db } from "../db"; export type PartyQuestionMember = { userId: string; name: string; }; export type PartyAnalytics = { groupSummary?: { mostSharedGenres?: { name: string }[]; }; storyClusters?: { tracks?: { name: string }[]; artists?: { name: string }[]; }[]; memberProfiles?: { userId: string }[]; pairwise?: { userIdA: string; userIdB: string }[]; } | null; export const QUESTION_DURATION_MS = 60_000; export const MIN_PARTY_SIZE = 2; export const MAX_PARTY_SIZE = 4; export async function fetchPartyMembers( db: typeof Db, partyId: string, ): Promise { const members = await db.query.partyMember.findMany({ where: { partyId, }, with: { user: true, }, }); return members.map((member) => ({ userId: member.userId, name: member.user?.name ?? member.userId, })); } export function getPartySize(memberCount: number): number { return Math.max(MIN_PARTY_SIZE, Math.min(MAX_PARTY_SIZE, memberCount)); } export function buildQuestionWindow( question: T, timestamp = Date.now(), ): T & { startTimestamp: number; endTimestamp: number } { return { ...question, startTimestamp: timestamp, endTimestamp: timestamp + QUESTION_DURATION_MS, }; } export function getQuestionDistanceScore( selectedValue: number, correctValue: number, maxDistance: number, points: number, ): number { if (!Number.isFinite(selectedValue)) return 0; if (maxDistance <= 0) return selectedValue === correctValue ? points : 0; const distance = Math.abs(selectedValue - correctValue); const ratio = Math.max(0, 1 - distance / maxDistance); return Math.round(points * ratio); } export function getQuestionRange( correctValue: number, tolerance: number, ): { min: number; max: number } { return { min: Math.floor(correctValue - tolerance), max: Math.ceil(correctValue + tolerance), }; } export function getTopGenreName(analytics: PartyAnalytics): string { return analytics?.groupSummary?.mostSharedGenres?.[0]?.name ?? "Hip-Hop"; } export function getTopArtistName(analytics: PartyAnalytics): string { return analytics?.storyClusters?.[0]?.artists?.[0]?.name ?? "Artist A"; } export function getTopTrackName(analytics: PartyAnalytics): string { return analytics?.storyClusters?.[0]?.tracks?.[0]?.name ?? "Track A"; } export function getCurrentLeader( quizState: { scores: Record }, members: PartyQuestionMember[], ): PartyQuestionMember { const leaderId = Object.entries(quizState.scores) .sort(([, a], [, b]) => b - a) .at(0)?.[0]; return ( members.find((member) => member.userId === leaderId) ?? members[0] ?? { userId: "", name: "Player A" } ); } export function getMostDiverseMember( analytics: PartyAnalytics, members: PartyQuestionMember[], ): PartyQuestionMember { const userId = analytics?.memberProfiles?.[0]?.userId; return ( members.find((member) => member.userId === userId) ?? members[1] ?? members[0] ?? { userId: "", name: "Player B" } ); } export function getMostAlignedMember( analytics: PartyAnalytics, members: PartyQuestionMember[], ): PartyQuestionMember { const userId = analytics?.pairwise?.[0]?.userIdA; return ( members.find((member) => member.userId === userId) ?? members[2] ?? members[0] ?? { userId: "", name: "Player C" } ); } export function buildMemberOptions( correctMember: PartyQuestionMember, members: PartyQuestionMember[], ): string[] { const desiredCount = getPartySize(members.length); const options = uniqueStrings([ correctMember.name, ...members.map((member) => member.name), ]); if (options.length < desiredCount) { for (const fallback of fallbackPlayerNames(desiredCount)) { if (options.length >= desiredCount) break; if (!options.includes(fallback)) options.push(fallback); } } const ordered = [ correctMember.name, ...options.filter((name) => name !== correctMember.name), ]; return ordered.slice(0, desiredCount); } function uniqueStrings(values: string[]): string[] { return values.filter((value, index, list) => list.indexOf(value) === index); } function fallbackPlayerNames(count: number): string[] { return Array.from( { length: count }, (_, index) => `Player ${String.fromCharCode(65 + index)}`, ); }