From 4fbd2eaf103dae635762459320447c2fdbf89479 Mon Sep 17 00:00:00 2001 From: vlbuzilov Date: Tue, 5 May 2026 23:12:08 +0200 Subject: [PATCH 1/2] code template for esp32 added, mb need to change ino for rs --- esp32/esp32_quiz/esp32_quiz.ino | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 esp32/esp32_quiz/esp32_quiz.ino diff --git a/esp32/esp32_quiz/esp32_quiz.ino b/esp32/esp32_quiz/esp32_quiz.ino new file mode 100644 index 0000000..292cc8f --- /dev/null +++ b/esp32/esp32_quiz/esp32_quiz.ino @@ -0,0 +1,39 @@ +#include +#include + +// 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!"); +} -- 2.45.2 From 5f6c7c655183af7e9b37df97ee424f73190cf775 Mon Sep 17 00:00:00 2001 From: vlbuzilov Date: Wed, 6 May 2026 10:07:02 +0200 Subject: [PATCH 2/2] Expand questions generation --- api/src/party/audio-question-generator.ts | 16 ++++ api/src/party/numeric-question-generator.ts | 83 ++++++++++++++++++--- api/src/party/question-generator.ts | 2 +- api/src/party/question-utils.ts | 15 ++++ api/src/party/social-question-generator.ts | 27 +++++++ 5 files changed, 132 insertions(+), 11 deletions(-) diff --git a/api/src/party/audio-question-generator.ts b/api/src/party/audio-question-generator.ts index e05dc74..f76bb80 100644 --- a/api/src/party/audio-question-generator.ts +++ b/api/src/party/audio-question-generator.ts @@ -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) { diff --git a/api/src/party/numeric-question-generator.ts b/api/src/party/numeric-question-generator.ts index a91d3ff..5092ee9 100644 --- a/api/src/party/numeric-question-generator.ts +++ b/api/src/party/numeric-question-generator.ts @@ -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, @@ -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 { + 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 { + 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 { - 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); +} \ No newline at end of file diff --git a/api/src/party/question-generator.ts b/api/src/party/question-generator.ts index 5fe3379..ea9206f 100644 --- a/api/src/party/question-generator.ts +++ b/api/src/party/question-generator.ts @@ -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); } diff --git a/api/src/party/question-utils.ts b/api/src/party/question-utils.ts index 47591a7..5c1dec4 100644 --- a/api/src/party/question-utils.ts +++ b/api/src/party/question-utils.ts @@ -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; +} diff --git a/api/src/party/social-question-generator.ts b/api/src/party/social-question-generator.ts index fceae38..8559c5d 100644 --- a/api/src/party/social-question-generator.ts +++ b/api/src/party/social-question-generator.ts @@ -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, + }); + } } } -- 2.45.2