Merge pull request 'features/Expand-questions-generation' (#1) from features/Expand-questions-generation into master
Reviewed-on: #1
This commit is contained in:
commit
e14619a047
6 changed files with 171 additions and 11 deletions
|
|
@ -87,6 +87,22 @@ export function buildAudioMetadataQuestion(
|
||||||
points: 10,
|
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) {
|
if (randomTopTrack.albumName) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import type { db } from "../db";
|
import type { db } from "../db";
|
||||||
|
import { topArtist as topArtistTable, topTrack as topTrackTable } from "../db/schema";
|
||||||
import type { Question } from "../party-types";
|
import type { Question } from "../party-types";
|
||||||
import type { PartyAnalytics } from "./question-utils";
|
import {
|
||||||
import { buildQuestionWindow, getQuestionRange } from "./question-utils";
|
buildQuestionWindow,
|
||||||
|
getQuestionRange,
|
||||||
|
type PartyAnalytics,
|
||||||
|
type PartyQuestionMember,
|
||||||
|
} from "./question-utils";
|
||||||
|
|
||||||
type NumericQuestion = Omit<
|
type NumericQuestion = Omit<
|
||||||
Extract<Question, { type: "numeric" }>,
|
Extract<Question, { type: "numeric" }>,
|
||||||
|
|
@ -12,6 +18,7 @@ type BuildNumericQuestionInput = {
|
||||||
db: typeof db;
|
db: typeof db;
|
||||||
analytics: PartyAnalytics;
|
analytics: PartyAnalytics;
|
||||||
index: number;
|
index: number;
|
||||||
|
members: PartyQuestionMember[];
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getAlbumReleaseYear({
|
async function getAlbumReleaseYear({
|
||||||
|
|
@ -22,12 +29,8 @@ async function getAlbumReleaseYear({
|
||||||
const trackName = analytics?.storyClusters?.[0]?.tracks?.[0]?.name;
|
const trackName = analytics?.storyClusters?.[0]?.tracks?.[0]?.name;
|
||||||
const track = trackName
|
const track = trackName
|
||||||
? await db.query.track.findFirst({
|
? await db.query.track.findFirst({
|
||||||
where: {
|
where: { name: trackName },
|
||||||
name: trackName,
|
with: { album: true },
|
||||||
},
|
|
||||||
with: {
|
|
||||||
album: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const correct =
|
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(
|
export async function buildNumericQuestion(
|
||||||
input: BuildNumericQuestionInput,
|
input: BuildNumericQuestionInput,
|
||||||
): Promise<Question> {
|
): 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"
|
return type === "audio-metadata"
|
||||||
? buildAudioMetadataQuestion(analytics, index)
|
? buildAudioMetadataQuestion(analytics, index)
|
||||||
: type === "numeric"
|
: type === "numeric"
|
||||||
? buildNumericQuestion({ db: dbClient, analytics, index })
|
? buildNumericQuestion({ db: dbClient, analytics, index, members })
|
||||||
: buildSocialQuestion(quizState, analytics, members, index);
|
: buildSocialQuestion(quizState, analytics, members, index);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,3 +233,18 @@ function fallbackPlayerNames(count: number): string[] {
|
||||||
(_, index) => `Player ${String.fromCharCode(65 + index)}`,
|
(_, 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 type { Question, QuizState } from "../party-types";
|
||||||
import {
|
import {
|
||||||
buildMemberOptions,
|
buildMemberOptions,
|
||||||
|
buildMemberPairOptions,
|
||||||
buildQuestionWindow,
|
buildQuestionWindow,
|
||||||
getCurrentLeader,
|
getCurrentLeader,
|
||||||
getMostAlignedMember,
|
getMostAlignedMember,
|
||||||
|
|
@ -68,6 +69,32 @@ export function buildSocialQuestion(
|
||||||
correct: 0,
|
correct: 0,
|
||||||
points: 10,
|
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