features/Expand-questions-generation #1
6 changed files with 171 additions and 11 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
39
esp32/esp32_quiz/esp32_quiz.ino
Normal file
39
esp32/esp32_quiz/esp32_quiz.ino
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include <WiFi.h>
|
||||
#include <LiquidCrystal.h>
|
||||
|
||||
// LCD pins: RS, EN, D4, D5, D6, D7
|
||||
LiquidCrystal lcd(18, 19, 13, 14, 15, 26);
|
||||
|
||||
const int LED_PIN = 2; // built-in LED on most ESP32 devkits
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
// WiFi chip init (not connected yet)
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
Serial.println("WiFi initialized");
|
||||
|
||||
// Let LCD power up before init (ESP32 boots faster than LCD expects)
|
||||
delay(500);
|
||||
lcd.begin(16, 2);
|
||||
lcd.clear();
|
||||
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
Serial.println("led on!");
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
delay(1000);
|
||||
|
||||
lcd.clear();
|
||||
lcd.print("Test message!");
|
||||
|
||||
Serial.println("led off!");
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
delay(1000);
|
||||
|
||||
lcd.clear();
|
||||
lcd.print("Test message2!");
|
||||
}
|
||||
Loading…
Reference in a new issue