From 427df3b7e5bcf196aab5393b06b1cee149fa377c Mon Sep 17 00:00:00 2001 From: dn285 Date: Tue, 27 Oct 2026 01:13:24 -0500 Subject: [PATCH 1/2] Add an End row to the geoword Packet page's clues A tossup with buzzes after its audio ended gets an End row under its last clue, with its correct/total count, so you can see how many players converted once every clue had played. It acts like any other clue: choosing it moves the audio and the graph's playhead to the end, and scrolls the buzz table to those buzzes and marks them. Unlike the others it doesn't play, since playing from the end would start the audio over. getBuzzClue places a buzz on a clue, or on the End row, for both the counts and the table, so they always agree. Co-Authored-By: Claude Opus 5.5 --- client/play/geoword/results/packet/html.js | 26 +++++++++++---------- client/play/geoword/results/packet/index.js | 8 ++++++- client/play/geoword/results/packet/time.js | 10 ++++++++ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/client/play/geoword/results/packet/html.js b/client/play/geoword/results/packet/html.js index f5c3972e8..7e79523bc 100644 --- a/client/play/geoword/results/packet/html.js +++ b/client/play/geoword/results/packet/html.js @@ -1,4 +1,4 @@ -import { formatTime, getBuzzTime, getClueAt, hasClueTimes } from './time.js'; +import { formatTime, getBuzzClue, getBuzzTime, hasClueTimes } from './time.js'; import { escapeHTML } from '../../../../../shared/string-utils.js'; /** @@ -10,27 +10,29 @@ export function getCluesHTML (tossup, buzzpoints) { const label = `aria-label="Clues for question ${tossup.questionNumber}"`; if (hasClueTimes(tossup)) { - const counts = tossup.clues.map(() => ({ correct: 0, total: 0 })); + // One count per clue, then one for buzzes after the audio ended + const counts = [...tossup.clues, 'End'].map(() => ({ correct: 0, total: 0 })); if (tossup.duration) { for (const buzz of buzzpoints) { - const time = getBuzzTime(buzz, tossup.duration); - if (time === null) { continue; } - const count = counts[getClueAt(tossup.clipBoundaries, time)]; + const count = counts[getBuzzClue(buzz, tossup)]; count.total++; if (buzz.points > 0) { count.correct++; } } } let html = `
`; - tossup.clues.forEach((clue, index) => { - const start = tossup.clipBoundaries[index]; - const { correct, total } = counts[index]; + counts.forEach(({ correct, total }, index) => { + const isEnd = index === tossup.clues.length; + if (isEnd && total === 0) { return; } + // The End row starts where the audio ends, rounded down, since the audio keeps its + // position to the microsecond and would land just short of it + const start = isEnd ? Math.floor(tossup.duration * 1000) / 1000 : tossup.clipBoundaries[index]; const count = total === 0 ? '' : `${correct}/${total}`; html += ` - `; }); @@ -61,7 +63,7 @@ export function getBuzzTableHTML (tossup, buzzpoints) { const time = getBuzzTime(buzz, tossup.duration); data = `data-buzz-time="${time ?? tossup.duration}" tabindex="0"`; if (time !== null) { data += ` data-correct="${buzz.points > 0}"`; } - if (time !== null && hasClueTimes(tossup)) { data += ` data-clue="${getClueAt(tossup.clipBoundaries, time)}"`; } + if (hasClueTimes(tossup)) { data += ` data-clue="${getBuzzClue(buzz, tossup)}"`; } } const who = buzz.isYou ? 'you' : buzz.isTeammate ? 'teammate' : ''; const pointsClass = buzz.points > 0 ? 'text-success' : buzz.points < 0 ? 'text-danger' : 'text-body-secondary'; diff --git a/client/play/geoword/results/packet/index.js b/client/play/geoword/results/packet/index.js index 35b77e2ab..2310936b0 100644 --- a/client/play/geoword/results/packet/index.js +++ b/client/play/geoword/results/packet/index.js @@ -196,7 +196,13 @@ tabContent.addEventListener('click', event => { const section = button.closest('section'); const audio = section.querySelector('audio'); audio.currentTime = parseFloat(button.dataset.start); - audio.play().catch(console.error); + // Playing from the end would start the audio over + if ('end' in button.dataset) { + audio.pause(); + showPosition(section); + } else { + audio.play().catch(console.error); + } selectBuzz(section, null); scrollTableToClue(section, [...section.querySelectorAll('button[data-start]')].indexOf(button)); }); diff --git a/client/play/geoword/results/packet/time.js b/client/play/geoword/results/packet/time.js index efd97a8cd..16745c5bb 100644 --- a/client/play/geoword/results/packet/time.js +++ b/client/play/geoword/results/packet/time.js @@ -29,3 +29,13 @@ export function getBuzzTime (buzz, duration) { export function getClueAt (starts, time) { return Math.max(0, starts.findLastIndex(start => start <= time)); } + +/** + * @param {object} buzz + * @param {object} tossup - with clue times and a duration + * @returns {number} the buzz's clue, or the number of clues if it came after the audio ended + */ +export function getBuzzClue (buzz, { clipBoundaries, duration }) { + const time = getBuzzTime(buzz, duration); + return time === null ? clipBoundaries.length : getClueAt(clipBoundaries, time); +} From 2250a58294810958c49a972d7c520de38772a7b6 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Tue, 27 Oct 2026 22:34:24 -0400 Subject: [PATCH 2/2] remove comments --- client/play/geoword/results/packet/html.js | 3 --- client/play/geoword/results/packet/index.js | 1 - 2 files changed, 4 deletions(-) diff --git a/client/play/geoword/results/packet/html.js b/client/play/geoword/results/packet/html.js index 7e79523bc..420c2ecb9 100644 --- a/client/play/geoword/results/packet/html.js +++ b/client/play/geoword/results/packet/html.js @@ -10,7 +10,6 @@ export function getCluesHTML (tossup, buzzpoints) { const label = `aria-label="Clues for question ${tossup.questionNumber}"`; if (hasClueTimes(tossup)) { - // One count per clue, then one for buzzes after the audio ended const counts = [...tossup.clues, 'End'].map(() => ({ correct: 0, total: 0 })); if (tossup.duration) { for (const buzz of buzzpoints) { @@ -23,8 +22,6 @@ export function getCluesHTML (tossup, buzzpoints) { counts.forEach(({ correct, total }, index) => { const isEnd = index === tossup.clues.length; if (isEnd && total === 0) { return; } - // The End row starts where the audio ends, rounded down, since the audio keeps its - // position to the microsecond and would land just short of it const start = isEnd ? Math.floor(tossup.duration * 1000) / 1000 : tossup.clipBoundaries[index]; const count = total === 0 ? '' diff --git a/client/play/geoword/results/packet/index.js b/client/play/geoword/results/packet/index.js index 2310936b0..960b7be28 100644 --- a/client/play/geoword/results/packet/index.js +++ b/client/play/geoword/results/packet/index.js @@ -196,7 +196,6 @@ tabContent.addEventListener('click', event => { const section = button.closest('section'); const audio = section.querySelector('audio'); audio.currentTime = parseFloat(button.dataset.start); - // Playing from the end would start the audio over if ('end' in button.dataset) { audio.pause(); showPosition(section);