From 2950cd4d6606cb024f23a0d88e8f1c370538fe86 Mon Sep 17 00:00:00 2001 From: Theodore Chen Date: Fri, 13 Feb 2026 12:49:23 -0500 Subject: [PATCH 1/7] school stuff idk not done yet --- client/play/TossupBonusClient.js | 56 ++- client/play/TossupClient.js | 256 ++++++----- client/play/tossups/SoloTossupClient.js | 415 ++++++++--------- client/play/tossups/index.html | 4 + client/play/tossups/index.jsx | 179 ++++---- quizbowl/TossupRoom.js | 588 ++++++++++++------------ 6 files changed, 774 insertions(+), 724 deletions(-) diff --git a/client/play/TossupBonusClient.js b/client/play/TossupBonusClient.js index ca0e380bc..e7c4cb871 100644 --- a/client/play/TossupBonusClient.js +++ b/client/play/TossupBonusClient.js @@ -3,32 +3,42 @@ import { TossupClientMixin } from './TossupClient.js'; import QuestionClient from './QuestionClient.js'; export default class TossupBonusClient extends BonusClientMixin(TossupClientMixin(QuestionClient)) { - constructor (room, userId, socket) { - super(room, userId, socket); - attachEventListeners(room, socket); - } + constructor(room, userId, socket) { + super(room, userId, socket); + attachEventListeners(room, socket); + } - onmessage (message) { - const data = JSON.parse(message); - switch (data.type) { - case 'toggle-enable-bonuses': return this.toggleEnableBonuses(data); - default: return super.onmessage(message); - } - } + onmessage(message) { + const data = JSON.parse(message); + switch (data.type) { + case 'toggle-enable-bonuses': return this.toggleEnableBonuses(data); + case 'toggle-stop-on-power': return this.toggleStopOnPower(data); + default: return super.onmessage(message); + } + } - startNextTossup ({ tossup, packetLength }) { - super.startNextTossup({ tossup, packetLength }); - document.getElementById('reveal').disabled = true; - } + startNextTossup({ tossup, packetLength }) { + super.startNextTossup({ tossup, packetLength }); + document.getElementById('reveal').disabled = true; + } - toggleEnableBonuses ({ enableBonuses }) { - document.getElementById('toggle-enable-bonuses').checked = enableBonuses; - } + toggleEnableBonuses({ enableBonuses }) { + document.getElementById('toggle-enable-bonuses').checked = enableBonuses; + } + + toggleStopOnPower({ stopOnPower }) { + document.getElementById('toggle-stop-on-power').checked = stopOnPower; + } } -function attachEventListeners (room, socket) { - document.getElementById('toggle-enable-bonuses').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'toggle-enable-bonuses', enableBonuses: this.checked }); - }); +function attachEventListeners(room, socket) { + document.getElementById('toggle-enable-bonuses').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-enable-bonuses', enableBonuses: this.checked }); + }); + + document.getElementById('toggle-stop-on-power').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-stop-on-power', stopOnPower: this.checked }); + }); } diff --git a/client/play/TossupClient.js b/client/play/TossupClient.js index 8c62e6401..f43738d1c 100644 --- a/client/play/TossupClient.js +++ b/client/play/TossupClient.js @@ -4,131 +4,141 @@ import audio from '../audio/index.js'; import { MODE_ENUM } from '../../quizbowl/constants.js'; export const TossupClientMixin = (ClientClass) => class extends ClientClass { - constructor (room, userId, socket) { - super(room, userId, socket); - attachEventListeners(room, socket); - } - - onmessage (message) { - const data = JSON.parse(message); - switch (data.type) { - case 'buzz': return this.buzz(data); - case 'end-current-tossup': return this.endCurrentTossup(data); - case 'give-tossup-answer': return this.giveTossupAnswer(data); - case 'pause': return this.pause(data); - case 'reveal-tossup-answer': return this.revealTossupAnswer(data); - case 'set-reading-speed': return this.setReadingSpeed(data); - case 'start-next-tossup': return this.startNextTossup(data); - case 'toggle-powermark-only': return this.togglePowermarkOnly(data); - case 'toggle-rebuzz': return this.toggleRebuzz(data); - case 'update-question': return this.updateQuestion(data); - default: return super.onmessage(message); - } - } - - buzz ({ userId }) { - document.getElementById('buzz').disabled = true; - document.getElementById('next').disabled = true; - document.getElementById('pause').disabled = true; - if (userId === this.USER_ID && audio.soundEffects) { audio.buzz.play(); } - } - - endCurrentTossup ({ starred, tossup }) { - addTossupGameCard({ starred, tossup }); - } - - giveTossupAnswer ({ directive, directedPrompt, score, userId }) { - super.giveAnswer({ directive, directedPrompt, score, userId }); - - if (directive !== 'prompt') { - document.getElementById('next').disabled = false; - } - } - - pause ({ paused }) { - document.getElementById('pause').textContent = paused ? 'Resume' : 'Pause'; - } - - revealTossupAnswer ({ answer, question }) { - document.getElementById('question').innerHTML = question; - document.getElementById('answer').innerHTML = 'ANSWER: ' + answer; - document.getElementById('pause').disabled = true; - } - - setMode ({ mode }) { - super.setMode({ mode }); - switch (mode) { - case MODE_ENUM.SET_NAME: - document.getElementById('toggle-powermark-only').disabled = true; - document.getElementById('toggle-standard-only').disabled = true; - break; - case MODE_ENUM.RANDOM: - document.getElementById('toggle-powermark-only').disabled = false; - document.getElementById('toggle-standard-only').disabled = false; - break; - } - } - - setReadingSpeed ({ readingSpeed }) { - document.getElementById('reading-speed').value = readingSpeed; - document.getElementById('reading-speed-display').textContent = readingSpeed; - } - - startNextTossup ({ tossup, packetLength }) { - this.startNextQuestion({ question: tossup, packetLength }); - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('buzz').disabled = false; - document.getElementById('pause').textContent = 'Pause'; - document.getElementById('pause').disabled = false; - this.room.tossup = tossup; - } - - togglePowermarkOnly ({ powermarkOnly }) { - document.getElementById('toggle-powermark-only').checked = powermarkOnly; - } - - toggleRebuzz ({ rebuzz }) { - document.getElementById('toggle-rebuzz').checked = rebuzz; - } - - updateQuestion ({ word }) { - if (word === '(*)' || word === '[*]') { return; } - document.getElementById('question').innerHTML += word + ' '; - } + constructor(room, userId, socket) { + super(room, userId, socket); + attachEventListeners(room, socket); + } + + onmessage(message) { + const data = JSON.parse(message); + switch (data.type) { + case 'buzz': return this.buzz(data); + case 'end-current-tossup': return this.endCurrentTossup(data); + case 'give-tossup-answer': return this.giveTossupAnswer(data); + case 'pause': return this.pause(data); + case 'reveal-tossup-answer': return this.revealTossupAnswer(data); + case 'set-reading-speed': return this.setReadingSpeed(data); + case 'start-next-tossup': return this.startNextTossup(data); + case 'toggle-powermark-only': return this.togglePowermarkOnly(data); + case 'toggle-rebuzz': return this.toggleRebuzz(data); + case 'toggle-stop-on-power': return this.toggleStopOnPower(data); + case 'update-question': return this.updateQuestion(data); + default: return super.onmessage(message); + } + } + + buzz({ userId }) { + document.getElementById('buzz').disabled = true; + document.getElementById('next').disabled = true; + document.getElementById('pause').disabled = true; + if (userId === this.USER_ID && audio.soundEffects) { audio.buzz.play(); } + } + + endCurrentTossup({ starred, tossup }) { + addTossupGameCard({ starred, tossup }); + } + + giveTossupAnswer({ directive, directedPrompt, score, userId }) { + super.giveAnswer({ directive, directedPrompt, score, userId }); + + if (directive !== 'prompt') { + document.getElementById('next').disabled = false; + } + } + + pause({ paused }) { + document.getElementById('pause').textContent = paused ? 'Resume' : 'Pause'; + } + + revealTossupAnswer({ answer, question }) { + document.getElementById('question').innerHTML = question; + document.getElementById('answer').innerHTML = 'ANSWER: ' + answer; + document.getElementById('pause').disabled = true; + } + + setMode({ mode }) { + super.setMode({ mode }); + switch (mode) { + case MODE_ENUM.SET_NAME: + document.getElementById('toggle-powermark-only').disabled = true; + document.getElementById('toggle-standard-only').disabled = true; + break; + case MODE_ENUM.RANDOM: + document.getElementById('toggle-powermark-only').disabled = false; + document.getElementById('toggle-standard-only').disabled = false; + break; + } + } + + setReadingSpeed({ readingSpeed }) { + document.getElementById('reading-speed').value = readingSpeed; + document.getElementById('reading-speed-display').textContent = readingSpeed; + } + + startNextTossup({ tossup, packetLength }) { + this.startNextQuestion({ question: tossup, packetLength }); + document.getElementById('buzz').textContent = 'Buzz'; + document.getElementById('buzz').disabled = false; + document.getElementById('pause').textContent = 'Pause'; + document.getElementById('pause').disabled = false; + this.room.tossup = tossup; + } + + togglePowermarkOnly({ powermarkOnly }) { + document.getElementById('toggle-powermark-only').checked = powermarkOnly; + } + + toggleRebuzz({ rebuzz }) { + document.getElementById('toggle-rebuzz').checked = rebuzz; + } + + toggleStopOnPower({ stopOnPower }) { + document.getElementById('toggle-stop-on-power').checked = stopOnPower; + } + + updateQuestion({ word }) { + if (word === '(*)' || word === '[*]') { return; } + document.getElementById('question').innerHTML += word + ' '; + } }; -function attachEventListeners (room, socket) { - document.getElementById('buzz').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'buzz' }); - socket.sendToServer({ type: 'give-answer-live-update', givenAnswer: '' }); - }); - - document.getElementById('pause').addEventListener('click', function () { - this.blur(); - const seconds = parseFloat(document.querySelector('.timer .face').textContent); - const tenths = parseFloat(document.querySelector('.timer .fraction').textContent); - const pausedTime = (seconds + tenths) * 10; - socket.sendToServer({ type: 'pause', pausedTime }); - }); - - document.getElementById('reading-speed').addEventListener('change', function () { - socket.sendToServer({ type: 'set-reading-speed', readingSpeed: this.value }); - }); - - document.getElementById('reading-speed').addEventListener('input', function () { - document.getElementById('reading-speed-display').textContent = this.value; - }); - - document.getElementById('toggle-powermark-only').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'toggle-powermark-only', powermarkOnly: this.checked }); - }); - - document.getElementById('toggle-rebuzz').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'toggle-rebuzz', rebuzz: this.checked }); - }); +function attachEventListeners(room, socket) { + document.getElementById('buzz').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'buzz' }); + socket.sendToServer({ type: 'give-answer-live-update', givenAnswer: '' }); + }); + + document.getElementById('pause').addEventListener('click', function() { + this.blur(); + const seconds = parseFloat(document.querySelector('.timer .face').textContent); + const tenths = parseFloat(document.querySelector('.timer .fraction').textContent); + const pausedTime = (seconds + tenths) * 10; + socket.sendToServer({ type: 'pause', pausedTime }); + }); + + document.getElementById('reading-speed').addEventListener('change', function() { + socket.sendToServer({ type: 'set-reading-speed', readingSpeed: this.value }); + }); + + document.getElementById('reading-speed').addEventListener('input', function() { + document.getElementById('reading-speed-display').textContent = this.value; + }); + + document.getElementById('toggle-powermark-only').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-powermark-only', powermarkOnly: this.checked }); + }); + + document.getElementById('toggle-rebuzz').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-rebuzz', rebuzz: this.checked }); + }); + + document.getElementById('toggle-stop-on-power').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-stop-on-power', stopOnPower: this.checked }); + }); } const TossupClient = TossupClientMixin(QuestionClient); diff --git a/client/play/tossups/SoloTossupClient.js b/client/play/tossups/SoloTossupClient.js index 1f8ee763a..54c3d8e3f 100644 --- a/client/play/tossups/SoloTossupClient.js +++ b/client/play/tossups/SoloTossupClient.js @@ -8,209 +8,214 @@ const queryVersion = '2025-05-07'; const settingsVersion = '2024-11-02'; export default class SoloTossupClient extends TossupClient { - constructor (room, userId, socket, aiBot) { - super(room, userId, socket); - this.aiBot = aiBot; - } - - onmessage (message) { - const data = JSON.parse(message); - switch (data.type) { - case 'clear-stats': return this.clearStats(data); - case 'toggle-ai-mode': return this.toggleAiMode(data); - case 'toggle-correct': return this.toggleCorrect(data); - case 'toggle-type-to-answer': return this.toggleTypeToAnswer(data); - default: return super.onmessage(message); - } - } - - buzz ({ timer, userId, username }) { - if (userId !== this.USER_ID) { return; } - - super.buzz({ userId }); - - if (this.room.settings.typeToAnswer) { - document.getElementById('answer-input-group').classList.remove('d-none'); - document.getElementById('answer-input').focus(); - } else { - document.getElementById('buzz').disabled = false; - document.getElementById('buzz').textContent = 'Reveal'; - } - } - - clearStats ({ userId }) { - this.updateStatDisplay(this.room.players[userId]); - } - - endCurrentTossup ({ isSkip, starred, tossup }) { - super.endCurrentTossup({ starred, tossup }); - if (!isSkip && this.room.previousTossup.userId === this.USER_ID && (this.room.mode !== MODE_ENUM.LOCAL)) { - const previous = this.room.previousTossup; - const pointValue = previous.isCorrect ? (previous.inPower ? previous.powerValue : 10) : (previous.endOfQuestion ? 0 : previous.negValue); - questionStats.recordTossup({ - _id: previous.tossup._id, - celerity: previous.celerity, - isCorrect: previous.isCorrect, - multiplayer: false, - pointValue - }); - } - } - - async giveTossupAnswer ({ directive, directedPrompt, perQuestionCelerity, score, tossup, userId }) { - super.giveTossupAnswer({ directive, directedPrompt, score, userId }); - - if (directive === 'prompt') { return; } - - if (userId === this.USER_ID) { - this.updateStatDisplay(this.room.players[this.USER_ID]); - } else if (this.aiBot.active) { - upsertPlayerItem(this.aiBot.player); - } - - if (this.room.settings.rebuzz && directive === 'reject') { - document.getElementById('buzz').disabled = false; - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('pause').disabled = false; - } - } - - async startNextTossup ({ packetLength, tossup }) { - super.startNextTossup({ tossup, packetLength }); - document.getElementById('next').disabled = false; - document.getElementById('toggle-correct').textContent = 'I was wrong'; - document.getElementById('toggle-correct').classList.add('d-none'); - document.getElementById('next').textContent = 'Skip'; - } - - revealTossupAnswer ({ answer, question }) { - super.revealTossupAnswer({ answer, question }); - - document.getElementById('buzz').disabled = true; - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('next').disabled = false; - document.getElementById('next').textContent = 'Next'; - - document.getElementById('toggle-correct').classList.remove('d-none'); - document.getElementById('toggle-correct').textContent = this.room.previousTossup.isCorrect ? 'I was wrong' : 'I was right'; - } - - setCategories ({ alternateSubcategories, categories, subcategories, percentView, categoryPercents }) { - super.setCategories(); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setDifficulties ({ difficulties }) { - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setMaxYear ({ maxYear }) { - super.setMaxYear({ maxYear }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setMinYear ({ minYear }) { - super.setMinYear({ minYear }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setPacketNumbers ({ packetNumbers }) { - super.setPacketNumbers({ packetNumbers }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setReadingSpeed ({ readingSpeed }) { - super.setReadingSpeed({ readingSpeed }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - async setSetName ({ setName, setLength }) { - super.setSetName({ setName, setLength }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setStrictness ({ strictness }) { - super.setStrictness({ strictness }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - toggleAiMode ({ aiMode }) { - if (aiMode) { upsertPlayerItem(this.aiBot.player); } - - this.aiBot.active = aiMode; - document.getElementById('ai-settings').disabled = !aiMode; - document.getElementById('toggle-ai-mode').checked = aiMode; - document.getElementById('player-list-group').classList.toggle('d-none', !aiMode); - document.getElementById('player-list-group-hr').classList.toggle('d-none', !aiMode); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - toggleCorrect ({ correct, userId }) { - this.updateStatDisplay(this.room.players[this.USER_ID]); - document.getElementById('toggle-correct').textContent = correct ? 'I was wrong' : 'I was right'; - } - - togglePowermarkOnly ({ powermarkOnly }) { - super.togglePowermarkOnly({ powermarkOnly }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - toggleRebuzz ({ rebuzz }) { - super.toggleRebuzz({ rebuzz }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - setMode ({ mode }) { - switch (mode) { - case MODE_ENUM.SET_NAME: - document.getElementById('local-packet-settings').classList.add('d-none'); - break; - case MODE_ENUM.RANDOM: - document.getElementById('local-packet-settings').classList.add('d-none'); - break; - case MODE_ENUM.STARRED: - document.getElementById('difficulty-settings').classList.add('d-none'); - document.getElementById('local-packet-settings').classList.add('d-none'); - document.getElementById('set-settings').classList.add('d-none'); - document.getElementById('toggle-powermark-only').disabled = true; - document.getElementById('toggle-standard-only').disabled = true; - break; - case MODE_ENUM.LOCAL: - document.getElementById('difficulty-settings').classList.add('d-none'); - document.getElementById('local-packet-settings').classList.remove('d-none'); - document.getElementById('set-settings').classList.add('d-none'); - document.getElementById('toggle-powermark-only').disabled = true; - document.getElementById('toggle-standard-only').disabled = true; - break; - } - super.setMode({ mode }); - window.localStorage.setItem('singleplayer-tossup-mode', JSON.stringify({ mode, version: modeVersion })); - } - - toggleStandardOnly ({ standardOnly }) { - super.toggleStandardOnly({ standardOnly }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - toggleTimer ({ timer }) { - super.toggleTimer({ timer }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - toggleTypeToAnswer ({ typeToAnswer }) { - document.getElementById('type-to-answer').checked = typeToAnswer; - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - /** - * Updates the displayed stat line. - */ - updateStatDisplay ({ powers, tens, negs, tuh, points, celerity }) { - const averageCelerity = celerity.correct.average.toFixed(3); - const plural = (tuh === 1) ? '' : 's'; - document.getElementById('statline').innerHTML = `${powers}/${tens}/${negs} with ${tuh} tossup${plural} seen (${points} pts, celerity: ${averageCelerity})`; - - // disable clear stats button if no stats - document.getElementById('clear-stats').disabled = (tuh === 0); - } + constructor(room, userId, socket, aiBot) { + super(room, userId, socket); + this.aiBot = aiBot; + } + + onmessage(message) { + const data = JSON.parse(message); + switch (data.type) { + case 'clear-stats': return this.clearStats(data); + case 'toggle-ai-mode': return this.toggleAiMode(data); + case 'toggle-correct': return this.toggleCorrect(data); + case 'toggle-type-to-answer': return this.toggleTypeToAnswer(data); + default: return super.onmessage(message); + } + } + + buzz({ timer, userId, username }) { + if (userId !== this.USER_ID) { return; } + + super.buzz({ userId }); + + if (this.room.settings.typeToAnswer) { + document.getElementById('answer-input-group').classList.remove('d-none'); + document.getElementById('answer-input').focus(); + } else { + document.getElementById('buzz').disabled = false; + document.getElementById('buzz').textContent = 'Reveal'; + } + } + + clearStats({ userId }) { + this.updateStatDisplay(this.room.players[userId]); + } + + endCurrentTossup({ isSkip, starred, tossup }) { + super.endCurrentTossup({ starred, tossup }); + if (!isSkip && this.room.previousTossup.userId === this.USER_ID && (this.room.mode !== MODE_ENUM.LOCAL)) { + const previous = this.room.previousTossup; + const pointValue = previous.isCorrect ? (previous.inPower ? previous.powerValue : 10) : (previous.endOfQuestion ? 0 : previous.negValue); + questionStats.recordTossup({ + _id: previous.tossup._id, + celerity: previous.celerity, + isCorrect: previous.isCorrect, + multiplayer: false, + pointValue + }); + } + } + + async giveTossupAnswer({ directive, directedPrompt, perQuestionCelerity, score, tossup, userId }) { + super.giveTossupAnswer({ directive, directedPrompt, score, userId }); + + if (directive === 'prompt') { return; } + + if (userId === this.USER_ID) { + this.updateStatDisplay(this.room.players[this.USER_ID]); + } else if (this.aiBot.active) { + upsertPlayerItem(this.aiBot.player); + } + + if (this.room.settings.rebuzz && directive === 'reject') { + document.getElementById('buzz').disabled = false; + document.getElementById('buzz').textContent = 'Buzz'; + document.getElementById('pause').disabled = false; + } + } + + async startNextTossup({ packetLength, tossup }) { + super.startNextTossup({ tossup, packetLength }); + document.getElementById('next').disabled = false; + document.getElementById('toggle-correct').textContent = 'I was wrong'; + document.getElementById('toggle-correct').classList.add('d-none'); + document.getElementById('next').textContent = 'Skip'; + } + + revealTossupAnswer({ answer, question }) { + super.revealTossupAnswer({ answer, question }); + + document.getElementById('buzz').disabled = true; + document.getElementById('buzz').textContent = 'Buzz'; + document.getElementById('next').disabled = false; + document.getElementById('next').textContent = 'Next'; + + document.getElementById('toggle-correct').classList.remove('d-none'); + document.getElementById('toggle-correct').textContent = this.room.previousTossup.isCorrect ? 'I was wrong' : 'I was right'; + } + + setCategories({ alternateSubcategories, categories, subcategories, percentView, categoryPercents }) { + super.setCategories(); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + setDifficulties({ difficulties }) { + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + setMaxYear({ maxYear }) { + super.setMaxYear({ maxYear }); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + setMinYear({ minYear }) { + super.setMinYear({ minYear }); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + setPacketNumbers({ packetNumbers }) { + super.setPacketNumbers({ packetNumbers }); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + setReadingSpeed({ readingSpeed }) { + super.setReadingSpeed({ readingSpeed }); + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); + } + + async setSetName({ setName, setLength }) { + super.setSetName({ setName, setLength }); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + setStrictness({ strictness }) { + super.setStrictness({ strictness }); + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); + } + + toggleAiMode({ aiMode }) { + if (aiMode) { upsertPlayerItem(this.aiBot.player); } + + this.aiBot.active = aiMode; + document.getElementById('ai-settings').disabled = !aiMode; + document.getElementById('toggle-ai-mode').checked = aiMode; + document.getElementById('player-list-group').classList.toggle('d-none', !aiMode); + document.getElementById('player-list-group-hr').classList.toggle('d-none', !aiMode); + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); + } + + toggleCorrect({ correct, userId }) { + this.updateStatDisplay(this.room.players[this.USER_ID]); + document.getElementById('toggle-correct').textContent = correct ? 'I was wrong' : 'I was right'; + } + + togglePowermarkOnly({ powermarkOnly }) { + super.togglePowermarkOnly({ powermarkOnly }); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + toggleRebuzz({ rebuzz }) { + super.toggleRebuzz({ rebuzz }); + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); + } + + toggleStopOnPower({ stopOnPower }) { + super.toggleStopOnPower({ stopOnPower }); + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); + } + + setMode({ mode }) { + switch (mode) { + case MODE_ENUM.SET_NAME: + document.getElementById('local-packet-settings').classList.add('d-none'); + break; + case MODE_ENUM.RANDOM: + document.getElementById('local-packet-settings').classList.add('d-none'); + break; + case MODE_ENUM.STARRED: + document.getElementById('difficulty-settings').classList.add('d-none'); + document.getElementById('local-packet-settings').classList.add('d-none'); + document.getElementById('set-settings').classList.add('d-none'); + document.getElementById('toggle-powermark-only').disabled = true; + document.getElementById('toggle-standard-only').disabled = true; + break; + case MODE_ENUM.LOCAL: + document.getElementById('difficulty-settings').classList.add('d-none'); + document.getElementById('local-packet-settings').classList.remove('d-none'); + document.getElementById('set-settings').classList.add('d-none'); + document.getElementById('toggle-powermark-only').disabled = true; + document.getElementById('toggle-standard-only').disabled = true; + break; + } + super.setMode({ mode }); + window.localStorage.setItem('singleplayer-tossup-mode', JSON.stringify({ mode, version: modeVersion })); + } + + toggleStandardOnly({ standardOnly }) { + super.toggleStandardOnly({ standardOnly }); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); + } + + toggleTimer({ timer }) { + super.toggleTimer({ timer }); + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); + } + + toggleTypeToAnswer({ typeToAnswer }) { + document.getElementById('type-to-answer').checked = typeToAnswer; + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); + } + + /** + * Updates the displayed stat line. + */ + updateStatDisplay({ powers, tens, negs, tuh, points, celerity }) { + const averageCelerity = celerity.correct.average.toFixed(3); + const plural = (tuh === 1) ? '' : 's'; + document.getElementById('statline').innerHTML = `${powers}/${tens}/${negs} with ${tuh} tossup${plural} seen (${points} pts, celerity: ${averageCelerity})`; + + // disable clear stats button if no stats + document.getElementById('clear-stats').disabled = (tuh === 0); + } } diff --git a/client/play/tossups/index.html b/client/play/tossups/index.html index e6409c303..fdae302ee 100644 --- a/client/play/tossups/index.html +++ b/client/play/tossups/index.html @@ -173,6 +173,10 @@

+
+ + +
diff --git a/client/play/tossups/index.jsx b/client/play/tossups/index.jsx index 621ca9c48..6ec1099ac 100644 --- a/client/play/tossups/index.jsx +++ b/client/play/tossups/index.jsx @@ -24,122 +24,123 @@ const client = new SoloTossupClient(room, USER_ID, socket, aiBot); socket.send = (message) => client.onmessage(message); room.sockets[USER_ID] = socket; -document.getElementById('choose-ai').addEventListener('change', function () { - const prefix = 'ai-choice-'; - const choice = this.querySelector('input:checked').id.slice(prefix.length); - aiBot.setAIBot(aiBots[choice][0]); +document.getElementById('choose-ai').addEventListener('change', function() { + const prefix = 'ai-choice-'; + const choice = this.querySelector('input:checked').id.slice(prefix.length); + aiBot.setAIBot(aiBots[choice][0]); }); -document.getElementById('local-packet-input').addEventListener('change', function (event) { - const file = this.files[0]; - if (!file) { return; } - const reader = new window.FileReader(); - reader.onload = function (e) { - try { - const packet = JSON.parse(e.target.result); - socket.sendToServer({ type: 'upload-local-packet', packet, filename: file.name }); - } catch (error) { - window.alert('Invalid packet format'); - } - }; - reader.readAsText(file); +document.getElementById('local-packet-input').addEventListener('change', function(event) { + const file = this.files[0]; + if (!file) { return; } + const reader = new window.FileReader(); + reader.onload = function(e) { + try { + const packet = JSON.parse(e.target.result); + socket.sendToServer({ type: 'upload-local-packet', packet, filename: file.name }); + } catch (error) { + window.alert('Invalid packet format'); + } + }; + reader.readAsText(file); }); -document.getElementById('toggle-ai-mode').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'toggle-ai-mode', aiMode: this.checked }); +document.getElementById('toggle-ai-mode').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-ai-mode', aiMode: this.checked }); }); -document.getElementById('toggle-correct').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'toggle-correct', correct: this.textContent === 'I was right' }); +document.getElementById('toggle-correct').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-correct', correct: this.textContent === 'I was right' }); }); -document.getElementById('toggle-randomize-order').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'toggle-randomize-order', randomizeOrder: this.checked }); +document.getElementById('toggle-randomize-order').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-randomize-order', randomizeOrder: this.checked }); }); -document.getElementById('type-to-answer').addEventListener('click', function () { - this.blur(); - socket.sendToServer({ type: 'toggle-type-to-answer', typeToAnswer: this.checked }); +document.getElementById('type-to-answer').addEventListener('click', function() { + this.blur(); + socket.sendToServer({ type: 'toggle-type-to-answer', typeToAnswer: this.checked }); }); document.addEventListener('keydown', (event) => { - if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return; - - switch (event.key?.toLowerCase()) { - case ' ': - document.getElementById('buzz').click(); - // Prevent spacebar from scrolling the page - if (event.target === document.body) { event.preventDefault(); } - break; - - case 'e': return document.getElementById('toggle-settings').click(); - case 'k': return document.getElementsByClassName('card-header-clickable')[0].click(); - case 'n': return document.getElementById('next').click(); - case 'p': return document.getElementById('pause').click(); - case 's': return document.getElementById('next').click(); - case 't': return document.getElementsByClassName('star-tossup')[0].click(); - case 'y': return navigator.clipboard.writeText(room.tossup._id ?? ''); - } + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return; + + switch (event.key?.toLowerCase()) { + case ' ': + document.getElementById('buzz').click(); + // Prevent spacebar from scrolling the page + if (event.target === document.body) { event.preventDefault(); } + break; + + case 'e': return document.getElementById('toggle-settings').click(); + case 'k': return document.getElementsByClassName('card-header-clickable')[0].click(); + case 'n': return document.getElementById('next').click(); + case 'p': return document.getElementById('pause').click(); + case 's': return document.getElementById('next').click(); + case 't': return document.getElementsByClassName('star-tossup')[0].click(); + case 'y': return navigator.clipboard.writeText(room.tossup._id ?? ''); + } }); if (window.localStorage.getItem('singleplayer-tossup-mode')) { - try { - const savedQuery = JSON.parse(window.localStorage.getItem('singleplayer-tossup-mode')); - if (savedQuery.version !== modeVersion) { throw new Error(); } - socket.sendToServer({ type: 'set-mode', ...savedQuery }); - } catch { - window.localStorage.removeItem('singleplayer-tossup-mode'); - } + try { + const savedQuery = JSON.parse(window.localStorage.getItem('singleplayer-tossup-mode')); + if (savedQuery.version !== modeVersion) { throw new Error(); } + socket.sendToServer({ type: 'set-mode', ...savedQuery }); + } catch { + window.localStorage.removeItem('singleplayer-tossup-mode'); + } } let startingDifficulties = []; if (window.localStorage.getItem('singleplayer-tossup-query')) { - try { - const savedQuery = JSON.parse(window.localStorage.getItem('singleplayer-tossup-query')); - if (savedQuery.version !== queryVersion) { throw new Error(); } - room.categoryManager.import(savedQuery); - room.query = savedQuery; - // need to set min year first to avoid conflicts between saved max year and default min year - socket.sendToServer({ type: 'set-min-year', ...savedQuery, doNotFetch: true }); - socket.sendToServer({ type: 'set-max-year', ...savedQuery, doNotFetch: true }); - socket.sendToServer({ type: 'set-packet-numbers', ...savedQuery, doNotFetch: true }); - socket.sendToServer({ type: 'set-set-name', ...savedQuery, doNotFetch: true }); - socket.sendToServer({ type: 'toggle-standard-only', ...savedQuery, doNotFetch: true }); - socket.sendToServer({ type: 'toggle-powermark-only', ...savedQuery }); - startingDifficulties = savedQuery.difficulties; - } catch { - window.localStorage.removeItem('singleplayer-tossup-query'); - } + try { + const savedQuery = JSON.parse(window.localStorage.getItem('singleplayer-tossup-query')); + if (savedQuery.version !== queryVersion) { throw new Error(); } + room.categoryManager.import(savedQuery); + room.query = savedQuery; + // need to set min year first to avoid conflicts between saved max year and default min year + socket.sendToServer({ type: 'set-min-year', ...savedQuery, doNotFetch: true }); + socket.sendToServer({ type: 'set-max-year', ...savedQuery, doNotFetch: true }); + socket.sendToServer({ type: 'set-packet-numbers', ...savedQuery, doNotFetch: true }); + socket.sendToServer({ type: 'set-set-name', ...savedQuery, doNotFetch: true }); + socket.sendToServer({ type: 'toggle-standard-only', ...savedQuery, doNotFetch: true }); + socket.sendToServer({ type: 'toggle-powermark-only', ...savedQuery }); + startingDifficulties = savedQuery.difficulties; + } catch { + window.localStorage.removeItem('singleplayer-tossup-query'); + } } if (window.localStorage.getItem('singleplayer-tossup-settings')) { - try { - const savedSettings = JSON.parse(window.localStorage.getItem('singleplayer-tossup-settings')); - if (savedSettings.version !== settingsVersion) { throw new Error(); } - socket.sendToServer({ type: 'set-strictness', ...savedSettings }); - socket.sendToServer({ type: 'set-reading-speed', ...savedSettings }); - socket.sendToServer({ type: 'toggle-ai-mode', ...savedSettings }); - socket.sendToServer({ type: 'toggle-rebuzz', ...savedSettings }); - socket.sendToServer({ type: 'toggle-timer', ...savedSettings }); - socket.sendToServer({ type: 'toggle-type-to-answer', ...savedSettings }); - } catch { - window.localStorage.removeItem('singleplayer-tossup-settings'); - } + try { + const savedSettings = JSON.parse(window.localStorage.getItem('singleplayer-tossup-settings')); + if (savedSettings.version !== settingsVersion) { throw new Error(); } + socket.sendToServer({ type: 'set-strictness', ...savedSettings }); + socket.sendToServer({ type: 'set-reading-speed', ...savedSettings }); + socket.sendToServer({ type: 'toggle-ai-mode', ...savedSettings }); + socket.sendToServer({ type: 'toggle-rebuzz', ...savedSettings }); + socket.sendToServer({ type: 'toggle-stop-on-power', ...savedSettings }); + socket.sendToServer({ type: 'toggle-timer', ...savedSettings }); + socket.sendToServer({ type: 'toggle-type-to-answer', ...savedSettings }); + } catch { + window.localStorage.removeItem('singleplayer-tossup-settings'); + } } ReactDOM.createRoot(document.getElementById('category-modal-root')).render( - socket.sendToServer({ type: 'set-categories', ...room.categoryManager.export() })} - /> + socket.sendToServer({ type: 'set-categories', ...room.categoryManager.export() })} + /> ); ReactDOM.createRoot(document.getElementById('difficulty-dropdown-root')).render( - socket.sendToServer({ type: 'set-difficulties', difficulties: getDropdownValues('difficulties') })} - /> + socket.sendToServer({ type: 'set-difficulties', difficulties: getDropdownValues('difficulties') })} + /> ); diff --git a/quizbowl/TossupRoom.js b/quizbowl/TossupRoom.js index ea301e50a..526cac019 100644 --- a/quizbowl/TossupRoom.js +++ b/quizbowl/TossupRoom.js @@ -3,290 +3,310 @@ import insertTokensIntoHTML from './insert-tokens-into-html.js'; import QuestionRoom from './QuestionRoom.js'; export const TossupRoomMixin = (QuestionRoomClass) => class extends QuestionRoomClass { - constructor (name, categoryManager, supportedQuestionTypes = ['tossup']) { - super(name, categoryManager, supportedQuestionTypes); - - this.timeoutID = null; - /** - * @type {string | null} - * The userId of the player who buzzed in. - * We should ensure that buzzedIn is null before calling any readQuestion. - */ - this.buzzedIn = null; - this.buzzes = []; - this.buzzpointIndices = []; - this.liveAnswer = ''; - this.paused = false; - this.questionSplit = []; - this.tossup = {}; - this.tossupProgress = TOSSUP_PROGRESS_ENUM.NOT_STARTED; - this.wordIndex = 0; - - this.query = { - ...this.query, - powermarkOnly: false - }; - - this.settings = { - ...this.settings, - rebuzz: false, - readingSpeed: 50 - }; - - this.previousTossup = { - celerity: 0, - endOfQuestion: false, - isCorrect: true, - inPower: false, - negValue: -5, - powerValue: 15, - tossup: {}, - userId: null - }; - } - - async message (userId, message) { - switch (message.type) { - case 'buzz': return this.buzz(userId, message); - case 'give-answer': return this.giveTossupAnswer(userId, message); - case 'next': return this.next(userId, message); - case 'pause': return this.pause(userId, message); - case 'set-reading-speed': return this.setReadingSpeed(userId, message); - case 'toggle-powermark-only': return this.togglePowermarkOnly(userId, message); - case 'toggle-rebuzz': return this.toggleRebuzz(userId, message); - default: return super.message(userId, message); - } - } - - buzz (userId) { - if (!this.settings.rebuzz && this.buzzes.includes(userId)) { return; } - if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.READING) { return; } - - const username = this.players[userId].username; - if (this.buzzedIn) { - return this.emitMessage({ type: 'lost-buzzer-race', userId, username }); - } - - clearTimeout(this.timeoutID); - this.buzzedIn = userId; - this.buzzes.push(userId); - this.buzzpointIndices.push(this.questionSplit.slice(0, this.wordIndex).join(' ').length); - this.paused = false; - - this.emitMessage({ type: 'buzz', userId, username }); - this.emitMessage({ type: 'update-question', word: '(#)' }); - - this.startServerTimer( - ANSWER_TIME_LIMIT * 10, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }) - ); - } - - endCurrentTossup (userId) { - if (this.buzzedIn) { return false; } // prevents skipping when someone has buzzed in - if (this.queryingQuestion) { return false; } - const isSkip = this.tossupProgress === TOSSUP_PROGRESS_ENUM.READING; - if (isSkip && !this.settings.skip) { return false; } - - clearInterval(this.timer.interval); - clearTimeout(this.timeoutID); - this.emitMessage({ type: 'timer-update', timeRemaining: 0 }); - - this.buzzedIn = null; - this.buzzes = []; - this.buzzpointIndices = []; - this.paused = false; - - if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { this.revealTossupAnswer(); } - - const starred = this.mode === MODE_ENUM.STARRED ? true : (this.mode === MODE_ENUM.LOCAL ? false : null); - this.emitMessage({ type: 'end-current-tossup', isSkip, starred, tossup: this.tossup }); - return true; - } - - giveTossupAnswer (userId, { givenAnswer }) { - if (typeof givenAnswer !== 'string') { return false; } - if (this.buzzedIn !== userId) { return false; } - - this.liveAnswer = ''; - clearInterval(this.timer.interval); - this.emitMessage({ type: 'timer-update', timeRemaining: ANSWER_TIME_LIMIT * 10 }); - - if (Object.keys(this.tossup || {}).length === 0) { return; } - - const { celerity, directive, directedPrompt, points } = this.scoreTossup({ givenAnswer }); - - switch (directive) { - case 'accept': - this.buzzedIn = null; - this.revealTossupAnswer(); - this.players[userId].updateStats(points, celerity); - Object.values(this.players).forEach(player => { player.tuh++; }); - break; - case 'reject': - this.buzzedIn = null; - this.players[userId].updateStats(points, celerity); - if (!this.settings.rebuzz && this.buzzes.length === Object.keys(this.sockets).length) { - this.revealTossupAnswer(); - Object.values(this.players).forEach(player => { player.tuh++; }); - } else { - this.readQuestion(Date.now()); - } - break; - case 'prompt': - this.startServerTimer( - ANSWER_TIME_LIMIT * 10, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }) - ); - } - - this.emitMessage({ - type: 'give-tossup-answer', - userId, - username: this.players[userId].username, - givenAnswer, - directive, - directedPrompt, - score: points, - celerity: this.players[userId].celerity.correct.average, - // the below fields are used to record buzzpoint data - tossup: this.tossup, - perQuestionCelerity: celerity - }); - } - - async next (userId) { - if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.NOT_STARTED) { - return await this.startNextTossup(userId); - } - const allowed = this.endCurrentTossup(userId); - if (allowed) { await this.startNextTossup(userId); } - } - - pause (userId) { - if (this.buzzedIn) { return false; } - if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { return false; } - - this.paused = !this.paused; - if (this.paused) { - clearTimeout(this.timeoutID); - clearInterval(this.timer.interval); - } else if (this.wordIndex >= this.questionSplit.length) { - this.startServerTimer( - this.timer.timeRemaining, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.revealTossupAnswer() - ); - } else { - this.readQuestion(Date.now()); - } - const username = this.players[userId].username; - this.emitMessage({ type: 'pause', paused: this.paused, username }); - } - - async readQuestion (expectedReadTime) { - if (Object.keys(this.tossup || {}).length === 0) { return; } - if (this.wordIndex >= this.questionSplit.length) { - this.startServerTimer( - DEAD_TIME_LIMIT * 10, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.revealTossupAnswer() - ); - return; - } - - const word = this.questionSplit[this.wordIndex]; - this.wordIndex++; - this.emitMessage({ type: 'update-question', word }); - - // calculate time needed before reading next word - let time = Math.log(word.length) + 1; - if ((word.endsWith('.') && word.charCodeAt(word.length - 2) > 96 && word.charCodeAt(word.length - 2) < 123) || - word.slice(-2) === '.\u201d' || word.slice(-2) === '!\u201d' || word.slice(-2) === '?\u201d') { - time += 2.5; - } else if (word.endsWith(',') || word.slice(-2) === ',\u201d') { - time += 1.5; - } else if (word === '(*)' || word === '[*]') { - time = 0; - } - - time = time * 0.9 * (140 - this.settings.readingSpeed); - const delay = time - Date.now() + expectedReadTime; - - this.timeoutID = setTimeout(() => { - this.readQuestion(time + expectedReadTime); - }, delay); - } - - revealTossupAnswer () { - if (Object.keys(this.tossup || {}).length === 0) return; - this.tossupProgress = TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED; - this.tossup.markedQuestion = insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, { ' (#) ': this.buzzpointIndices }); - this.emitMessage({ - type: 'reveal-tossup-answer', - question: insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, { ' (#) ': this.buzzpointIndices }), - answer: this.tossup.answer - }); - } - - scoreTossup ({ givenAnswer }) { - const celerity = this.questionSplit.slice(this.wordIndex).join(' ').length / this.tossup.question.length; - const endOfQuestion = (this.wordIndex === this.questionSplit.length); - const inPower = Math.max(this.questionSplit.indexOf('(*)'), this.questionSplit.indexOf('[*]')) >= this.wordIndex; - const { directive, directedPrompt } = this.checkAnswer(this.tossup.answer, givenAnswer, this.settings.strictness); - const isCorrect = directive === 'accept'; - const points = isCorrect ? (inPower ? this.previousTossup.powerValue : 10) : (endOfQuestion ? 0 : this.previousTossup.negValue); - - this.previousTossup = { - ...this.previousTossup, - celerity, - endOfQuestion, - inPower, - isCorrect, - tossup: this.tossup, - userId: this.buzzedIn - }; - - return { celerity, directive, directedPrompt, endOfQuestion, inPower, points }; - } - - setReadingSpeed (userId, { readingSpeed }) { - if (isNaN(readingSpeed)) { return false; } - if (readingSpeed > 100) { readingSpeed = 100; } - if (readingSpeed < 0) { readingSpeed = 0; } - - this.settings.readingSpeed = readingSpeed; - const username = this.players[userId].username; - this.emitMessage({ type: 'set-reading-speed', username, readingSpeed }); - } - - async startNextTossup (userId) { - const username = this.players[userId].username; - this.tossup = await this.getNextQuestion('tossups'); - this.queryingQuestion = false; - if (!this.tossup) { return; } - this.emitMessage({ type: 'start-next-tossup', packetLength: this.packet.tossups.length, tossup: this.tossup, userId, username }); - this.questionSplit = this.tossup.question_sanitized.split(' ').filter(word => word !== ''); - this.wordIndex = 0; - this.tossupProgress = TOSSUP_PROGRESS_ENUM.READING; - clearTimeout(this.timeoutID); - this.readQuestion(Date.now()); - } - - togglePowermarkOnly (userId, { powermarkOnly }) { - this.query.powermarkOnly = powermarkOnly; - const username = this.players[userId].username; - this.adjustQuery(['powermarkOnly'], [powermarkOnly]); - this.emitMessage({ type: 'toggle-powermark-only', powermarkOnly, username }); - } - - toggleRebuzz (userId, { rebuzz }) { - this.settings.rebuzz = rebuzz; - const username = this.players[userId].username; - this.emitMessage({ type: 'toggle-rebuzz', rebuzz, username }); - } + constructor(name, categoryManager, supportedQuestionTypes = ['tossup']) { + super(name, categoryManager, supportedQuestionTypes); + + this.timeoutID = null; + /** + * @type {string | null} + * The userId of the player who buzzed in. + * We should ensure that buzzedIn is null before calling any readQuestion. + */ + this.buzzedIn = null; + this.buzzes = []; + this.buzzpointIndices = []; + this.liveAnswer = ''; + this.paused = false; + this.questionSplit = []; + this.tossup = {}; + this.tossupProgress = TOSSUP_PROGRESS_ENUM.NOT_STARTED; + this.wordIndex = 0; + + this.query = { + ...this.query, + powermarkOnly: false + }; + + this.settings = { + ...this.settings, + rebuzz: false, + stopOnPower: false, + readingSpeed: 50 + }; + + this.previousTossup = { + celerity: 0, + endOfQuestion: false, + isCorrect: true, + inPower: false, + negValue: -5, + powerValue: 15, + tossup: {}, + userId: null + }; + } + + async message(userId, message) { + switch (message.type) { + case 'buzz': return this.buzz(userId, message); + case 'give-answer': return this.giveTossupAnswer(userId, message); + case 'next': return this.next(userId, message); + case 'pause': return this.pause(userId, message); + case 'set-reading-speed': return this.setReadingSpeed(userId, message); + case 'toggle-powermark-only': return this.togglePowermarkOnly(userId, message); + case 'toggle-rebuzz': return this.toggleRebuzz(userId, message); + case 'toggle-stop-on-power': return this.toggleStopOnPower(userId, message); + default: return super.message(userId, message); + } + } + + buzz(userId) { + if (!this.settings.rebuzz && this.buzzes.includes(userId)) { return; } + if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.READING) { return; } + + const username = this.players[userId].username; + if (this.buzzedIn) { + return this.emitMessage({ type: 'lost-buzzer-race', userId, username }); + } + + clearTimeout(this.timeoutID); + this.buzzedIn = userId; + this.buzzes.push(userId); + this.buzzpointIndices.push(this.questionSplit.slice(0, this.wordIndex).join(' ').length); + this.paused = false; + + this.emitMessage({ type: 'buzz', userId, username }); + this.emitMessage({ type: 'update-question', word: '(#)' }); + + this.startServerTimer( + ANSWER_TIME_LIMIT * 10, + (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), + () => this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }) + ); + } + + endCurrentTossup(userId) { + if (this.buzzedIn) { return false; } // prevents skipping when someone has buzzed in + if (this.queryingQuestion) { return false; } + const isSkip = this.tossupProgress === TOSSUP_PROGRESS_ENUM.READING; + if (isSkip && !this.settings.skip) { return false; } + + clearInterval(this.timer.interval); + clearTimeout(this.timeoutID); + this.emitMessage({ type: 'timer-update', timeRemaining: 0 }); + + this.buzzedIn = null; + this.buzzes = []; + this.buzzpointIndices = []; + this.paused = false; + + if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { this.revealTossupAnswer(); } + + const starred = this.mode === MODE_ENUM.STARRED ? true : (this.mode === MODE_ENUM.LOCAL ? false : null); + this.emitMessage({ type: 'end-current-tossup', isSkip, starred, tossup: this.tossup }); + return true; + } + + giveTossupAnswer(userId, { givenAnswer }) { + if (typeof givenAnswer !== 'string') { return false; } + if (this.buzzedIn !== userId) { return false; } + + this.liveAnswer = ''; + clearInterval(this.timer.interval); + this.emitMessage({ type: 'timer-update', timeRemaining: ANSWER_TIME_LIMIT * 10 }); + + if (Object.keys(this.tossup || {}).length === 0) { return; } + + const { celerity, directive, directedPrompt, points } = this.scoreTossup({ givenAnswer }); + + switch (directive) { + case 'accept': + this.buzzedIn = null; + this.revealTossupAnswer(); + this.players[userId].updateStats(points, celerity); + Object.values(this.players).forEach(player => { player.tuh++; }); + break; + case 'reject': + this.buzzedIn = null; + this.players[userId].updateStats(points, celerity); + if (!this.settings.rebuzz && this.buzzes.length === Object.keys(this.sockets).length) { + this.revealTossupAnswer(); + Object.values(this.players).forEach(player => { player.tuh++; }); + } else { + this.readQuestion(Date.now()); + } + break; + case 'prompt': + this.startServerTimer( + ANSWER_TIME_LIMIT * 10, + (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), + () => this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }) + ); + } + + this.emitMessage({ + type: 'give-tossup-answer', + userId, + username: this.players[userId].username, + givenAnswer, + directive, + directedPrompt, + score: points, + celerity: this.players[userId].celerity.correct.average, + // the below fields are used to record buzzpoint data + tossup: this.tossup, + perQuestionCelerity: celerity + }); + } + + async next(userId) { + if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.NOT_STARTED) { + return await this.startNextTossup(userId); + } + const allowed = this.endCurrentTossup(userId); + if (allowed) { await this.startNextTossup(userId); } + } + + pause(userId) { + if (this.buzzedIn) { return false; } + if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { return false; } + + this.paused = !this.paused; + if (this.paused) { + clearTimeout(this.timeoutID); + clearInterval(this.timer.interval); + } else if (this.wordIndex >= this.questionSplit.length) { + this.startServerTimer( + this.timer.timeRemaining, + (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), + () => this.revealTossupAnswer() + ); + } else { + this.readQuestion(Date.now()); + } + const username = this.players[userId].username; + this.emitMessage({ type: 'pause', paused: this.paused, username }); + } + + async readQuestion(expectedReadTime) { + + if (Object.keys(this.tossup || {}).length === 0) { return; } + if (this.wordIndex >= this.questionSplit.length) { + this.startServerTimer( + DEAD_TIME_LIMIT * 10, + (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), + () => this.revealTossupAnswer() + ); + return; + } + + const word = this.questionSplit[this.wordIndex]; + + // stop reading and start timer if power and stopOnPower is enabled + if ((word === '(*)' || word === '[*]') && this.stopOnPower) { + this.startServerTimer( + DEAD_TIME_LIMIT * 10, + (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), + () => this.revealTossupAnswer() + ); + return; + } + + this.wordIndex++; + this.emitMessage({ type: 'update-question', word }); + + // calculate time needed before reading next word + let time = Math.log(word.length) + 1; + if ((word.endsWith('.') && word.charCodeAt(word.length - 2) > 96 && word.charCodeAt(word.length - 2) < 123) || + word.slice(-2) === '.\u201d' || word.slice(-2) === '!\u201d' || word.slice(-2) === '?\u201d') { + time += 2.5; + } else if (word.endsWith(',') || word.slice(-2) === ',\u201d') { + time += 1.5; + } else if (word === '(*)' || word === '[*]') { + time = 0; + } + + time = time * 0.9 * (140 - this.settings.readingSpeed); + const delay = time - Date.now() + expectedReadTime; + + this.timeoutID = setTimeout(() => { + this.readQuestion(time + expectedReadTime); + }, delay); + } + + revealTossupAnswer() { + if (Object.keys(this.tossup || {}).length === 0) return; + this.tossupProgress = TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED; + this.tossup.markedQuestion = insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, { ' (#) ': this.buzzpointIndices }); + this.emitMessage({ + type: 'reveal-tossup-answer', + question: insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, { ' (#) ': this.buzzpointIndices }), + answer: this.tossup.answer + }); + } + + scoreTossup({ givenAnswer }) { + const celerity = this.questionSplit.slice(this.wordIndex).join(' ').length / this.tossup.question.length; + const endOfQuestion = (this.wordIndex === this.questionSplit.length); + const inPower = Math.max(this.questionSplit.indexOf('(*)'), this.questionSplit.indexOf('[*]')) >= this.wordIndex; + const { directive, directedPrompt } = this.checkAnswer(this.tossup.answer, givenAnswer, this.settings.strictness); + const isCorrect = directive === 'accept'; + const points = isCorrect ? (inPower ? this.previousTossup.powerValue : 10) : (endOfQuestion ? 0 : this.previousTossup.negValue); + + this.previousTossup = { + ...this.previousTossup, + celerity, + endOfQuestion, + inPower, + isCorrect, + tossup: this.tossup, + userId: this.buzzedIn + }; + + return { celerity, directive, directedPrompt, endOfQuestion, inPower, points }; + } + + setReadingSpeed(userId, { readingSpeed }) { + if (isNaN(readingSpeed)) { return false; } + if (readingSpeed > 100) { readingSpeed = 100; } + if (readingSpeed < 0) { readingSpeed = 0; } + + this.settings.readingSpeed = readingSpeed; + const username = this.players[userId].username; + this.emitMessage({ type: 'set-reading-speed', username, readingSpeed }); + } + + async startNextTossup(userId) { + const username = this.players[userId].username; + this.tossup = await this.getNextQuestion('tossups'); + this.queryingQuestion = false; + if (!this.tossup) { return; } + this.emitMessage({ type: 'start-next-tossup', packetLength: this.packet.tossups.length, tossup: this.tossup, userId, username }); + this.questionSplit = this.tossup.question_sanitized.split(' ').filter(word => word !== ''); + this.wordIndex = 0; + this.tossupProgress = TOSSUP_PROGRESS_ENUM.READING; + clearTimeout(this.timeoutID); + this.readQuestion(Date.now()); + } + + togglePowermarkOnly(userId, { powermarkOnly }) { + this.query.powermarkOnly = powermarkOnly; + const username = this.players[userId].username; + this.adjustQuery(['powermarkOnly'], [powermarkOnly]); + this.emitMessage({ type: 'toggle-powermark-only', powermarkOnly, username }); + } + + toggleRebuzz(userId, { rebuzz }) { + this.settings.rebuzz = rebuzz; + const username = this.players[userId].username; + this.emitMessage({ type: 'toggle-rebuzz', rebuzz, username }); + } + + toggleStopOnPower(userId, { stopOnPower }) { + this.settings.stopOnPower = stopOnPower; + const username = this.players[userId].username; + this.emitMessage({ type: 'toggle-stop-on-power', stopOnPower, username }); + } }; const TossupRoom = TossupRoomMixin(QuestionRoom); From 16b486bb14490b72f0c70e3ea4bc5425c6b5e45d Mon Sep 17 00:00:00 2001 From: Theodore Chen Date: Sun, 15 Feb 2026 21:01:43 -0500 Subject: [PATCH 2/7] added stop on power to solo --- client/play/TossupBonusClient.js | 16 +- client/play/TossupClient.js | 323 +++++----- client/play/tossups/SoloTossupClient.js | 536 ++++++++++------- quizbowl/TossupRoom.js | 747 ++++++++++++++---------- 4 files changed, 948 insertions(+), 674 deletions(-) diff --git a/client/play/TossupBonusClient.js b/client/play/TossupBonusClient.js index e7c4cb871..9a0c9156a 100644 --- a/client/play/TossupBonusClient.js +++ b/client/play/TossupBonusClient.js @@ -12,7 +12,7 @@ export default class TossupBonusClient extends BonusClientMixin(TossupClientMixi const data = JSON.parse(message); switch (data.type) { case 'toggle-enable-bonuses': return this.toggleEnableBonuses(data); - case 'toggle-stop-on-power': return this.toggleStopOnPower(data); + // case 'toggle-stop-on-power': return this.toggleStopOnPower(data); default: return super.onmessage(message); } } @@ -26,9 +26,9 @@ export default class TossupBonusClient extends BonusClientMixin(TossupClientMixi document.getElementById('toggle-enable-bonuses').checked = enableBonuses; } - toggleStopOnPower({ stopOnPower }) { - document.getElementById('toggle-stop-on-power').checked = stopOnPower; - } + // toggleStopOnPower({ stopOnPower }) { + // document.getElementById('toggle-stop-on-power').checked = stopOnPower; + // } } function attachEventListeners(room, socket) { @@ -37,8 +37,8 @@ function attachEventListeners(room, socket) { socket.sendToServer({ type: 'toggle-enable-bonuses', enableBonuses: this.checked }); }); - document.getElementById('toggle-stop-on-power').addEventListener('click', function() { - this.blur(); - socket.sendToServer({ type: 'toggle-stop-on-power', stopOnPower: this.checked }); - }); + // document.getElementById('toggle-stop-on-power').addEventListener('click', function() { + // this.blur(); + // socket.sendToServer({ type: 'toggle-stop-on-power', stopOnPower: this.checked }); + // }); } diff --git a/client/play/TossupClient.js b/client/play/TossupClient.js index f43738d1c..416b83ec7 100644 --- a/client/play/TossupClient.js +++ b/client/play/TossupClient.js @@ -1,144 +1,189 @@ -import addTossupGameCard from './tossups/add-tossup-game-card.js'; -import QuestionClient from './QuestionClient.js'; -import audio from '../audio/index.js'; -import { MODE_ENUM } from '../../quizbowl/constants.js'; - -export const TossupClientMixin = (ClientClass) => class extends ClientClass { - constructor(room, userId, socket) { - super(room, userId, socket); - attachEventListeners(room, socket); - } - - onmessage(message) { - const data = JSON.parse(message); - switch (data.type) { - case 'buzz': return this.buzz(data); - case 'end-current-tossup': return this.endCurrentTossup(data); - case 'give-tossup-answer': return this.giveTossupAnswer(data); - case 'pause': return this.pause(data); - case 'reveal-tossup-answer': return this.revealTossupAnswer(data); - case 'set-reading-speed': return this.setReadingSpeed(data); - case 'start-next-tossup': return this.startNextTossup(data); - case 'toggle-powermark-only': return this.togglePowermarkOnly(data); - case 'toggle-rebuzz': return this.toggleRebuzz(data); - case 'toggle-stop-on-power': return this.toggleStopOnPower(data); - case 'update-question': return this.updateQuestion(data); - default: return super.onmessage(message); - } - } - - buzz({ userId }) { - document.getElementById('buzz').disabled = true; - document.getElementById('next').disabled = true; - document.getElementById('pause').disabled = true; - if (userId === this.USER_ID && audio.soundEffects) { audio.buzz.play(); } - } - - endCurrentTossup({ starred, tossup }) { - addTossupGameCard({ starred, tossup }); - } - - giveTossupAnswer({ directive, directedPrompt, score, userId }) { - super.giveAnswer({ directive, directedPrompt, score, userId }); - - if (directive !== 'prompt') { - document.getElementById('next').disabled = false; - } - } - - pause({ paused }) { - document.getElementById('pause').textContent = paused ? 'Resume' : 'Pause'; - } - - revealTossupAnswer({ answer, question }) { - document.getElementById('question').innerHTML = question; - document.getElementById('answer').innerHTML = 'ANSWER: ' + answer; - document.getElementById('pause').disabled = true; - } - - setMode({ mode }) { - super.setMode({ mode }); - switch (mode) { - case MODE_ENUM.SET_NAME: - document.getElementById('toggle-powermark-only').disabled = true; - document.getElementById('toggle-standard-only').disabled = true; - break; - case MODE_ENUM.RANDOM: - document.getElementById('toggle-powermark-only').disabled = false; - document.getElementById('toggle-standard-only').disabled = false; - break; - } - } - - setReadingSpeed({ readingSpeed }) { - document.getElementById('reading-speed').value = readingSpeed; - document.getElementById('reading-speed-display').textContent = readingSpeed; - } - - startNextTossup({ tossup, packetLength }) { - this.startNextQuestion({ question: tossup, packetLength }); - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('buzz').disabled = false; - document.getElementById('pause').textContent = 'Pause'; - document.getElementById('pause').disabled = false; - this.room.tossup = tossup; - } - - togglePowermarkOnly({ powermarkOnly }) { - document.getElementById('toggle-powermark-only').checked = powermarkOnly; - } - - toggleRebuzz({ rebuzz }) { - document.getElementById('toggle-rebuzz').checked = rebuzz; - } - - toggleStopOnPower({ stopOnPower }) { - document.getElementById('toggle-stop-on-power').checked = stopOnPower; - } - - updateQuestion({ word }) { - if (word === '(*)' || word === '[*]') { return; } - document.getElementById('question').innerHTML += word + ' '; - } -}; +import addTossupGameCard from "./tossups/add-tossup-game-card.js"; +import QuestionClient from "./QuestionClient.js"; +import audio from "../audio/index.js"; +import { MODE_ENUM } from "../../quizbowl/constants.js"; + +export const TossupClientMixin = (ClientClass) => + class extends ClientClass { + constructor(room, userId, socket) { + super(room, userId, socket); + attachEventListeners(room, socket); + } + + onmessage(message) { + const data = JSON.parse(message); + switch (data.type) { + case "buzz": + return this.buzz(data); + case "end-current-tossup": + return this.endCurrentTossup(data); + case "give-tossup-answer": + return this.giveTossupAnswer(data); + case "pause": + return this.pause(data); + case "reveal-tossup-answer": + return this.revealTossupAnswer(data); + case "set-reading-speed": + return this.setReadingSpeed(data); + case "start-next-tossup": + return this.startNextTossup(data); + case "toggle-powermark-only": + return this.togglePowermarkOnly(data); + case "toggle-rebuzz": + return this.toggleRebuzz(data); + case "toggle-stop-on-power": + return this.toggleStopOnPower(data); + case "update-question": + return this.updateQuestion(data); + default: + return super.onmessage(message); + } + } + + buzz({ userId }) { + document.getElementById("buzz").disabled = true; + document.getElementById("next").disabled = true; + document.getElementById("pause").disabled = true; + if (userId === this.USER_ID && audio.soundEffects) { + audio.buzz.play(); + } + } + + endCurrentTossup({ starred, tossup }) { + addTossupGameCard({ starred, tossup }); + } + + giveTossupAnswer({ directive, directedPrompt, score, userId }) { + super.giveAnswer({ directive, directedPrompt, score, userId }); + + if (directive !== "prompt") { + document.getElementById("next").disabled = false; + } + } + + pause({ paused }) { + document.getElementById("pause").textContent = paused + ? "Resume" + : "Pause"; + } + + revealTossupAnswer({ answer, question }) { + document.getElementById("question").innerHTML = question; + document.getElementById("answer").innerHTML = "ANSWER: " + answer; + document.getElementById("pause").disabled = true; + } + + setMode({ mode }) { + super.setMode({ mode }); + switch (mode) { + case MODE_ENUM.SET_NAME: + document.getElementById("toggle-powermark-only").disabled = true; + document.getElementById("toggle-standard-only").disabled = true; + break; + case MODE_ENUM.RANDOM: + document.getElementById("toggle-powermark-only").disabled = false; + document.getElementById("toggle-standard-only").disabled = false; + break; + } + } + + setReadingSpeed({ readingSpeed }) { + document.getElementById("reading-speed").value = readingSpeed; + document.getElementById("reading-speed-display").textContent = + readingSpeed; + } + + startNextTossup({ tossup, packetLength }) { + this.startNextQuestion({ question: tossup, packetLength }); + document.getElementById("buzz").textContent = "Buzz"; + document.getElementById("buzz").disabled = false; + document.getElementById("pause").textContent = "Pause"; + document.getElementById("pause").disabled = false; + this.room.tossup = tossup; + } + + togglePowermarkOnly({ powermarkOnly }) { + document.getElementById("toggle-powermark-only").checked = powermarkOnly; + } + + toggleRebuzz({ rebuzz }) { + document.getElementById("toggle-rebuzz").checked = rebuzz; + } + + toggleStopOnPower({ stopOnPower }) { + console.log("TossupClient.toggleStopOnPower called"); + document.getElementById("toggle-stop-on-power").checked = stopOnPower; + } + + updateQuestion({ word }) { + if (word === "(*)" || word === "[*]") { + return; + } + document.getElementById("question").innerHTML += word + " "; + } + }; function attachEventListeners(room, socket) { - document.getElementById('buzz').addEventListener('click', function() { - this.blur(); - socket.sendToServer({ type: 'buzz' }); - socket.sendToServer({ type: 'give-answer-live-update', givenAnswer: '' }); - }); - - document.getElementById('pause').addEventListener('click', function() { - this.blur(); - const seconds = parseFloat(document.querySelector('.timer .face').textContent); - const tenths = parseFloat(document.querySelector('.timer .fraction').textContent); - const pausedTime = (seconds + tenths) * 10; - socket.sendToServer({ type: 'pause', pausedTime }); - }); - - document.getElementById('reading-speed').addEventListener('change', function() { - socket.sendToServer({ type: 'set-reading-speed', readingSpeed: this.value }); - }); - - document.getElementById('reading-speed').addEventListener('input', function() { - document.getElementById('reading-speed-display').textContent = this.value; - }); - - document.getElementById('toggle-powermark-only').addEventListener('click', function() { - this.blur(); - socket.sendToServer({ type: 'toggle-powermark-only', powermarkOnly: this.checked }); - }); - - document.getElementById('toggle-rebuzz').addEventListener('click', function() { - this.blur(); - socket.sendToServer({ type: 'toggle-rebuzz', rebuzz: this.checked }); - }); - - document.getElementById('toggle-stop-on-power').addEventListener('click', function() { - this.blur(); - socket.sendToServer({ type: 'toggle-stop-on-power', stopOnPower: this.checked }); - }); + document.getElementById("buzz").addEventListener("click", function () { + this.blur(); + socket.sendToServer({ type: "buzz" }); + socket.sendToServer({ type: "give-answer-live-update", givenAnswer: "" }); + }); + + document.getElementById("pause").addEventListener("click", function () { + this.blur(); + const seconds = parseFloat( + document.querySelector(".timer .face").textContent, + ); + const tenths = parseFloat( + document.querySelector(".timer .fraction").textContent, + ); + const pausedTime = (seconds + tenths) * 10; + socket.sendToServer({ type: "pause", pausedTime }); + }); + + document + .getElementById("reading-speed") + .addEventListener("change", function () { + socket.sendToServer({ + type: "set-reading-speed", + readingSpeed: this.value, + }); + }); + + document + .getElementById("reading-speed") + .addEventListener("input", function () { + document.getElementById("reading-speed-display").textContent = this.value; + }); + + document + .getElementById("toggle-powermark-only") + .addEventListener("click", function () { + this.blur(); + socket.sendToServer({ + type: "toggle-powermark-only", + powermarkOnly: this.checked, + }); + }); + + document + .getElementById("toggle-rebuzz") + .addEventListener("click", function () { + this.blur(); + socket.sendToServer({ type: "toggle-rebuzz", rebuzz: this.checked }); + }); + + document + .getElementById("toggle-stop-on-power") + .addEventListener("click", function () { + console.log("stop-on-power checkbox has been checked"); + this.blur(); + socket.sendToServer({ + type: "toggle-stop-on-power", + stopOnPower: this.checked, + }); + }); } const TossupClient = TossupClientMixin(QuestionClient); diff --git a/client/play/tossups/SoloTossupClient.js b/client/play/tossups/SoloTossupClient.js index 54c3d8e3f..d9e651f34 100644 --- a/client/play/tossups/SoloTossupClient.js +++ b/client/play/tossups/SoloTossupClient.js @@ -1,221 +1,323 @@ -import { MODE_ENUM } from '../../../quizbowl/constants.js'; -import questionStats from '../../scripts/auth/question-stats.js'; -import upsertPlayerItem from '../upsert-player-item.js'; -import TossupClient from '../TossupClient.js'; +import { MODE_ENUM } from "../../../quizbowl/constants.js"; +import questionStats from "../../scripts/auth/question-stats.js"; +import upsertPlayerItem from "../upsert-player-item.js"; +import TossupClient from "../TossupClient.js"; -const modeVersion = '2025-01-14'; -const queryVersion = '2025-05-07'; -const settingsVersion = '2024-11-02'; +const modeVersion = "2025-01-14"; +const queryVersion = "2025-05-07"; +const settingsVersion = "2024-11-02"; export default class SoloTossupClient extends TossupClient { - constructor(room, userId, socket, aiBot) { - super(room, userId, socket); - this.aiBot = aiBot; - } - - onmessage(message) { - const data = JSON.parse(message); - switch (data.type) { - case 'clear-stats': return this.clearStats(data); - case 'toggle-ai-mode': return this.toggleAiMode(data); - case 'toggle-correct': return this.toggleCorrect(data); - case 'toggle-type-to-answer': return this.toggleTypeToAnswer(data); - default: return super.onmessage(message); - } - } - - buzz({ timer, userId, username }) { - if (userId !== this.USER_ID) { return; } - - super.buzz({ userId }); - - if (this.room.settings.typeToAnswer) { - document.getElementById('answer-input-group').classList.remove('d-none'); - document.getElementById('answer-input').focus(); - } else { - document.getElementById('buzz').disabled = false; - document.getElementById('buzz').textContent = 'Reveal'; - } - } - - clearStats({ userId }) { - this.updateStatDisplay(this.room.players[userId]); - } - - endCurrentTossup({ isSkip, starred, tossup }) { - super.endCurrentTossup({ starred, tossup }); - if (!isSkip && this.room.previousTossup.userId === this.USER_ID && (this.room.mode !== MODE_ENUM.LOCAL)) { - const previous = this.room.previousTossup; - const pointValue = previous.isCorrect ? (previous.inPower ? previous.powerValue : 10) : (previous.endOfQuestion ? 0 : previous.negValue); - questionStats.recordTossup({ - _id: previous.tossup._id, - celerity: previous.celerity, - isCorrect: previous.isCorrect, - multiplayer: false, - pointValue - }); - } - } - - async giveTossupAnswer({ directive, directedPrompt, perQuestionCelerity, score, tossup, userId }) { - super.giveTossupAnswer({ directive, directedPrompt, score, userId }); - - if (directive === 'prompt') { return; } - - if (userId === this.USER_ID) { - this.updateStatDisplay(this.room.players[this.USER_ID]); - } else if (this.aiBot.active) { - upsertPlayerItem(this.aiBot.player); - } - - if (this.room.settings.rebuzz && directive === 'reject') { - document.getElementById('buzz').disabled = false; - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('pause').disabled = false; - } - } - - async startNextTossup({ packetLength, tossup }) { - super.startNextTossup({ tossup, packetLength }); - document.getElementById('next').disabled = false; - document.getElementById('toggle-correct').textContent = 'I was wrong'; - document.getElementById('toggle-correct').classList.add('d-none'); - document.getElementById('next').textContent = 'Skip'; - } - - revealTossupAnswer({ answer, question }) { - super.revealTossupAnswer({ answer, question }); - - document.getElementById('buzz').disabled = true; - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('next').disabled = false; - document.getElementById('next').textContent = 'Next'; - - document.getElementById('toggle-correct').classList.remove('d-none'); - document.getElementById('toggle-correct').textContent = this.room.previousTossup.isCorrect ? 'I was wrong' : 'I was right'; - } - - setCategories({ alternateSubcategories, categories, subcategories, percentView, categoryPercents }) { - super.setCategories(); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setDifficulties({ difficulties }) { - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setMaxYear({ maxYear }) { - super.setMaxYear({ maxYear }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setMinYear({ minYear }) { - super.setMinYear({ minYear }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setPacketNumbers({ packetNumbers }) { - super.setPacketNumbers({ packetNumbers }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setReadingSpeed({ readingSpeed }) { - super.setReadingSpeed({ readingSpeed }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - async setSetName({ setName, setLength }) { - super.setSetName({ setName, setLength }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - setStrictness({ strictness }) { - super.setStrictness({ strictness }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - toggleAiMode({ aiMode }) { - if (aiMode) { upsertPlayerItem(this.aiBot.player); } - - this.aiBot.active = aiMode; - document.getElementById('ai-settings').disabled = !aiMode; - document.getElementById('toggle-ai-mode').checked = aiMode; - document.getElementById('player-list-group').classList.toggle('d-none', !aiMode); - document.getElementById('player-list-group-hr').classList.toggle('d-none', !aiMode); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - toggleCorrect({ correct, userId }) { - this.updateStatDisplay(this.room.players[this.USER_ID]); - document.getElementById('toggle-correct').textContent = correct ? 'I was wrong' : 'I was right'; - } - - togglePowermarkOnly({ powermarkOnly }) { - super.togglePowermarkOnly({ powermarkOnly }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - toggleRebuzz({ rebuzz }) { - super.toggleRebuzz({ rebuzz }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - toggleStopOnPower({ stopOnPower }) { - super.toggleStopOnPower({ stopOnPower }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - setMode({ mode }) { - switch (mode) { - case MODE_ENUM.SET_NAME: - document.getElementById('local-packet-settings').classList.add('d-none'); - break; - case MODE_ENUM.RANDOM: - document.getElementById('local-packet-settings').classList.add('d-none'); - break; - case MODE_ENUM.STARRED: - document.getElementById('difficulty-settings').classList.add('d-none'); - document.getElementById('local-packet-settings').classList.add('d-none'); - document.getElementById('set-settings').classList.add('d-none'); - document.getElementById('toggle-powermark-only').disabled = true; - document.getElementById('toggle-standard-only').disabled = true; - break; - case MODE_ENUM.LOCAL: - document.getElementById('difficulty-settings').classList.add('d-none'); - document.getElementById('local-packet-settings').classList.remove('d-none'); - document.getElementById('set-settings').classList.add('d-none'); - document.getElementById('toggle-powermark-only').disabled = true; - document.getElementById('toggle-standard-only').disabled = true; - break; - } - super.setMode({ mode }); - window.localStorage.setItem('singleplayer-tossup-mode', JSON.stringify({ mode, version: modeVersion })); - } - - toggleStandardOnly({ standardOnly }) { - super.toggleStandardOnly({ standardOnly }); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...this.room.query, version: queryVersion })); - } - - toggleTimer({ timer }) { - super.toggleTimer({ timer }); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - toggleTypeToAnswer({ typeToAnswer }) { - document.getElementById('type-to-answer').checked = typeToAnswer; - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion })); - } - - /** - * Updates the displayed stat line. - */ - updateStatDisplay({ powers, tens, negs, tuh, points, celerity }) { - const averageCelerity = celerity.correct.average.toFixed(3); - const plural = (tuh === 1) ? '' : 's'; - document.getElementById('statline').innerHTML = `${powers}/${tens}/${negs} with ${tuh} tossup${plural} seen (${points} pts, celerity: ${averageCelerity})`; - - // disable clear stats button if no stats - document.getElementById('clear-stats').disabled = (tuh === 0); - } + constructor(room, userId, socket, aiBot) { + console.log("SoloTossupClient constructor called"); + super(room, userId, socket); + this.aiBot = aiBot; + } + + onmessage(message) { + const data = JSON.parse(message); + switch (data.type) { + case "clear-stats": + return this.clearStats(data); + case "toggle-ai-mode": + return this.toggleAiMode(data); + case "toggle-correct": + return this.toggleCorrect(data); + case "toggle-type-to-answer": + return this.toggleTypeToAnswer(data); + default: + return super.onmessage(message); + } + } + + buzz({ timer, userId, username }) { + if (userId !== this.USER_ID) { + return; + } + + super.buzz({ userId }); + + if (this.room.settings.typeToAnswer) { + document.getElementById("answer-input-group").classList.remove("d-none"); + document.getElementById("answer-input").focus(); + } else { + document.getElementById("buzz").disabled = false; + document.getElementById("buzz").textContent = "Reveal"; + } + } + + clearStats({ userId }) { + this.updateStatDisplay(this.room.players[userId]); + } + + endCurrentTossup({ isSkip, starred, tossup }) { + super.endCurrentTossup({ starred, tossup }); + if ( + !isSkip && + this.room.previousTossup.userId === this.USER_ID && + this.room.mode !== MODE_ENUM.LOCAL + ) { + const previous = this.room.previousTossup; + const pointValue = previous.isCorrect + ? previous.inPower + ? previous.powerValue + : 10 + : previous.endOfQuestion + ? 0 + : previous.negValue; + questionStats.recordTossup({ + _id: previous.tossup._id, + celerity: previous.celerity, + isCorrect: previous.isCorrect, + multiplayer: false, + pointValue, + }); + } + } + + async giveTossupAnswer({ + directive, + directedPrompt, + perQuestionCelerity, + score, + tossup, + userId, + }) { + super.giveTossupAnswer({ directive, directedPrompt, score, userId }); + + if (directive === "prompt") { + return; + } + + if (userId === this.USER_ID) { + this.updateStatDisplay(this.room.players[this.USER_ID]); + } else if (this.aiBot.active) { + upsertPlayerItem(this.aiBot.player); + } + + if (this.room.settings.rebuzz && directive === "reject") { + document.getElementById("buzz").disabled = false; + document.getElementById("buzz").textContent = "Buzz"; + document.getElementById("pause").disabled = false; + } + } + + async startNextTossup({ packetLength, tossup }) { + super.startNextTossup({ tossup, packetLength }); + document.getElementById("next").disabled = false; + document.getElementById("toggle-correct").textContent = "I was wrong"; + document.getElementById("toggle-correct").classList.add("d-none"); + document.getElementById("next").textContent = "Skip"; + } + + revealTossupAnswer({ answer, question }) { + super.revealTossupAnswer({ answer, question }); + + document.getElementById("buzz").disabled = true; + document.getElementById("buzz").textContent = "Buzz"; + document.getElementById("next").disabled = false; + document.getElementById("next").textContent = "Next"; + + document.getElementById("toggle-correct").classList.remove("d-none"); + document.getElementById("toggle-correct").textContent = this.room + .previousTossup.isCorrect + ? "I was wrong" + : "I was right"; + } + + setCategories({ + alternateSubcategories, + categories, + subcategories, + percentView, + categoryPercents, + }) { + super.setCategories(); + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + setDifficulties({ difficulties }) { + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + setMaxYear({ maxYear }) { + super.setMaxYear({ maxYear }); + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + setMinYear({ minYear }) { + super.setMinYear({ minYear }); + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + setPacketNumbers({ packetNumbers }) { + super.setPacketNumbers({ packetNumbers }); + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + setReadingSpeed({ readingSpeed }) { + super.setReadingSpeed({ readingSpeed }); + window.localStorage.setItem( + "singleplayer-tossup-settings", + JSON.stringify({ ...this.room.settings, version: settingsVersion }), + ); + } + + async setSetName({ setName, setLength }) { + super.setSetName({ setName, setLength }); + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + setStrictness({ strictness }) { + super.setStrictness({ strictness }); + window.localStorage.setItem( + "singleplayer-tossup-settings", + JSON.stringify({ ...this.room.settings, version: settingsVersion }), + ); + } + + toggleAiMode({ aiMode }) { + if (aiMode) { + upsertPlayerItem(this.aiBot.player); + } + + this.aiBot.active = aiMode; + document.getElementById("ai-settings").disabled = !aiMode; + document.getElementById("toggle-ai-mode").checked = aiMode; + document + .getElementById("player-list-group") + .classList.toggle("d-none", !aiMode); + document + .getElementById("player-list-group-hr") + .classList.toggle("d-none", !aiMode); + window.localStorage.setItem( + "singleplayer-tossup-settings", + JSON.stringify({ ...this.room.settings, version: settingsVersion }), + ); + } + + toggleCorrect({ correct, userId }) { + this.updateStatDisplay(this.room.players[this.USER_ID]); + document.getElementById("toggle-correct").textContent = correct + ? "I was wrong" + : "I was right"; + } + + togglePowermarkOnly({ powermarkOnly }) { + super.togglePowermarkOnly({ powermarkOnly }); + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + toggleRebuzz({ rebuzz }) { + super.toggleRebuzz({ rebuzz }); + window.localStorage.setItem( + "singleplayer-tossup-settings", + JSON.stringify({ ...this.room.settings, version: settingsVersion }), + ); + } + + toggleStopOnPower({ stopOnPower }) { + console.log("SoloTossupClient.toggleStopOnPower called"); + super.toggleStopOnPower({ stopOnPower }); + window.localStorage.setItem( + "singleplayer-tossup-settings", + JSON.stringify({ ...this.room.settings, version: settingsVersion }), + ); + } + + setMode({ mode }) { + switch (mode) { + case MODE_ENUM.SET_NAME: + document + .getElementById("local-packet-settings") + .classList.add("d-none"); + break; + case MODE_ENUM.RANDOM: + document + .getElementById("local-packet-settings") + .classList.add("d-none"); + break; + case MODE_ENUM.STARRED: + document.getElementById("difficulty-settings").classList.add("d-none"); + document + .getElementById("local-packet-settings") + .classList.add("d-none"); + document.getElementById("set-settings").classList.add("d-none"); + document.getElementById("toggle-powermark-only").disabled = true; + document.getElementById("toggle-standard-only").disabled = true; + break; + case MODE_ENUM.LOCAL: + document.getElementById("difficulty-settings").classList.add("d-none"); + document + .getElementById("local-packet-settings") + .classList.remove("d-none"); + document.getElementById("set-settings").classList.add("d-none"); + document.getElementById("toggle-powermark-only").disabled = true; + document.getElementById("toggle-standard-only").disabled = true; + break; + } + super.setMode({ mode }); + window.localStorage.setItem( + "singleplayer-tossup-mode", + JSON.stringify({ mode, version: modeVersion }), + ); + } + + toggleStandardOnly({ standardOnly }) { + super.toggleStandardOnly({ standardOnly }); + window.localStorage.setItem( + "singleplayer-tossup-query", + JSON.stringify({ ...this.room.query, version: queryVersion }), + ); + } + + toggleTimer({ timer }) { + super.toggleTimer({ timer }); + window.localStorage.setItem( + "singleplayer-tossup-settings", + JSON.stringify({ ...this.room.settings, version: settingsVersion }), + ); + } + + toggleTypeToAnswer({ typeToAnswer }) { + document.getElementById("type-to-answer").checked = typeToAnswer; + window.localStorage.setItem( + "singleplayer-tossup-settings", + JSON.stringify({ ...this.room.settings, version: settingsVersion }), + ); + } + + /** + * Updates the displayed stat line. + */ + updateStatDisplay({ powers, tens, negs, tuh, points, celerity }) { + const averageCelerity = celerity.correct.average.toFixed(3); + const plural = tuh === 1 ? "" : "s"; + document.getElementById("statline").innerHTML = + `${powers}/${tens}/${negs} with ${tuh} tossup${plural} seen (${points} pts, celerity: ${averageCelerity})`; + + // disable clear stats button if no stats + document.getElementById("clear-stats").disabled = tuh === 0; + } } diff --git a/quizbowl/TossupRoom.js b/quizbowl/TossupRoom.js index 526cac019..0dd8e9665 100644 --- a/quizbowl/TossupRoom.js +++ b/quizbowl/TossupRoom.js @@ -1,313 +1,440 @@ -import { ANSWER_TIME_LIMIT, DEAD_TIME_LIMIT, MODE_ENUM, TOSSUP_PROGRESS_ENUM } from './constants.js'; -import insertTokensIntoHTML from './insert-tokens-into-html.js'; -import QuestionRoom from './QuestionRoom.js'; - -export const TossupRoomMixin = (QuestionRoomClass) => class extends QuestionRoomClass { - constructor(name, categoryManager, supportedQuestionTypes = ['tossup']) { - super(name, categoryManager, supportedQuestionTypes); - - this.timeoutID = null; - /** - * @type {string | null} - * The userId of the player who buzzed in. - * We should ensure that buzzedIn is null before calling any readQuestion. - */ - this.buzzedIn = null; - this.buzzes = []; - this.buzzpointIndices = []; - this.liveAnswer = ''; - this.paused = false; - this.questionSplit = []; - this.tossup = {}; - this.tossupProgress = TOSSUP_PROGRESS_ENUM.NOT_STARTED; - this.wordIndex = 0; - - this.query = { - ...this.query, - powermarkOnly: false - }; - - this.settings = { - ...this.settings, - rebuzz: false, - stopOnPower: false, - readingSpeed: 50 - }; - - this.previousTossup = { - celerity: 0, - endOfQuestion: false, - isCorrect: true, - inPower: false, - negValue: -5, - powerValue: 15, - tossup: {}, - userId: null - }; - } - - async message(userId, message) { - switch (message.type) { - case 'buzz': return this.buzz(userId, message); - case 'give-answer': return this.giveTossupAnswer(userId, message); - case 'next': return this.next(userId, message); - case 'pause': return this.pause(userId, message); - case 'set-reading-speed': return this.setReadingSpeed(userId, message); - case 'toggle-powermark-only': return this.togglePowermarkOnly(userId, message); - case 'toggle-rebuzz': return this.toggleRebuzz(userId, message); - case 'toggle-stop-on-power': return this.toggleStopOnPower(userId, message); - default: return super.message(userId, message); - } - } - - buzz(userId) { - if (!this.settings.rebuzz && this.buzzes.includes(userId)) { return; } - if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.READING) { return; } - - const username = this.players[userId].username; - if (this.buzzedIn) { - return this.emitMessage({ type: 'lost-buzzer-race', userId, username }); - } - - clearTimeout(this.timeoutID); - this.buzzedIn = userId; - this.buzzes.push(userId); - this.buzzpointIndices.push(this.questionSplit.slice(0, this.wordIndex).join(' ').length); - this.paused = false; - - this.emitMessage({ type: 'buzz', userId, username }); - this.emitMessage({ type: 'update-question', word: '(#)' }); - - this.startServerTimer( - ANSWER_TIME_LIMIT * 10, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }) - ); - } - - endCurrentTossup(userId) { - if (this.buzzedIn) { return false; } // prevents skipping when someone has buzzed in - if (this.queryingQuestion) { return false; } - const isSkip = this.tossupProgress === TOSSUP_PROGRESS_ENUM.READING; - if (isSkip && !this.settings.skip) { return false; } - - clearInterval(this.timer.interval); - clearTimeout(this.timeoutID); - this.emitMessage({ type: 'timer-update', timeRemaining: 0 }); - - this.buzzedIn = null; - this.buzzes = []; - this.buzzpointIndices = []; - this.paused = false; - - if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { this.revealTossupAnswer(); } - - const starred = this.mode === MODE_ENUM.STARRED ? true : (this.mode === MODE_ENUM.LOCAL ? false : null); - this.emitMessage({ type: 'end-current-tossup', isSkip, starred, tossup: this.tossup }); - return true; - } - - giveTossupAnswer(userId, { givenAnswer }) { - if (typeof givenAnswer !== 'string') { return false; } - if (this.buzzedIn !== userId) { return false; } - - this.liveAnswer = ''; - clearInterval(this.timer.interval); - this.emitMessage({ type: 'timer-update', timeRemaining: ANSWER_TIME_LIMIT * 10 }); - - if (Object.keys(this.tossup || {}).length === 0) { return; } - - const { celerity, directive, directedPrompt, points } = this.scoreTossup({ givenAnswer }); - - switch (directive) { - case 'accept': - this.buzzedIn = null; - this.revealTossupAnswer(); - this.players[userId].updateStats(points, celerity); - Object.values(this.players).forEach(player => { player.tuh++; }); - break; - case 'reject': - this.buzzedIn = null; - this.players[userId].updateStats(points, celerity); - if (!this.settings.rebuzz && this.buzzes.length === Object.keys(this.sockets).length) { - this.revealTossupAnswer(); - Object.values(this.players).forEach(player => { player.tuh++; }); - } else { - this.readQuestion(Date.now()); - } - break; - case 'prompt': - this.startServerTimer( - ANSWER_TIME_LIMIT * 10, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }) - ); - } - - this.emitMessage({ - type: 'give-tossup-answer', - userId, - username: this.players[userId].username, - givenAnswer, - directive, - directedPrompt, - score: points, - celerity: this.players[userId].celerity.correct.average, - // the below fields are used to record buzzpoint data - tossup: this.tossup, - perQuestionCelerity: celerity - }); - } - - async next(userId) { - if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.NOT_STARTED) { - return await this.startNextTossup(userId); - } - const allowed = this.endCurrentTossup(userId); - if (allowed) { await this.startNextTossup(userId); } - } - - pause(userId) { - if (this.buzzedIn) { return false; } - if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { return false; } - - this.paused = !this.paused; - if (this.paused) { - clearTimeout(this.timeoutID); - clearInterval(this.timer.interval); - } else if (this.wordIndex >= this.questionSplit.length) { - this.startServerTimer( - this.timer.timeRemaining, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.revealTossupAnswer() - ); - } else { - this.readQuestion(Date.now()); - } - const username = this.players[userId].username; - this.emitMessage({ type: 'pause', paused: this.paused, username }); - } - - async readQuestion(expectedReadTime) { - - if (Object.keys(this.tossup || {}).length === 0) { return; } - if (this.wordIndex >= this.questionSplit.length) { - this.startServerTimer( - DEAD_TIME_LIMIT * 10, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.revealTossupAnswer() - ); - return; - } - - const word = this.questionSplit[this.wordIndex]; - - // stop reading and start timer if power and stopOnPower is enabled - if ((word === '(*)' || word === '[*]') && this.stopOnPower) { - this.startServerTimer( - DEAD_TIME_LIMIT * 10, - (time) => this.emitMessage({ type: 'timer-update', timeRemaining: time }), - () => this.revealTossupAnswer() - ); - return; - } - - this.wordIndex++; - this.emitMessage({ type: 'update-question', word }); - - // calculate time needed before reading next word - let time = Math.log(word.length) + 1; - if ((word.endsWith('.') && word.charCodeAt(word.length - 2) > 96 && word.charCodeAt(word.length - 2) < 123) || - word.slice(-2) === '.\u201d' || word.slice(-2) === '!\u201d' || word.slice(-2) === '?\u201d') { - time += 2.5; - } else if (word.endsWith(',') || word.slice(-2) === ',\u201d') { - time += 1.5; - } else if (word === '(*)' || word === '[*]') { - time = 0; - } - - time = time * 0.9 * (140 - this.settings.readingSpeed); - const delay = time - Date.now() + expectedReadTime; - - this.timeoutID = setTimeout(() => { - this.readQuestion(time + expectedReadTime); - }, delay); - } - - revealTossupAnswer() { - if (Object.keys(this.tossup || {}).length === 0) return; - this.tossupProgress = TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED; - this.tossup.markedQuestion = insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, { ' (#) ': this.buzzpointIndices }); - this.emitMessage({ - type: 'reveal-tossup-answer', - question: insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, { ' (#) ': this.buzzpointIndices }), - answer: this.tossup.answer - }); - } - - scoreTossup({ givenAnswer }) { - const celerity = this.questionSplit.slice(this.wordIndex).join(' ').length / this.tossup.question.length; - const endOfQuestion = (this.wordIndex === this.questionSplit.length); - const inPower = Math.max(this.questionSplit.indexOf('(*)'), this.questionSplit.indexOf('[*]')) >= this.wordIndex; - const { directive, directedPrompt } = this.checkAnswer(this.tossup.answer, givenAnswer, this.settings.strictness); - const isCorrect = directive === 'accept'; - const points = isCorrect ? (inPower ? this.previousTossup.powerValue : 10) : (endOfQuestion ? 0 : this.previousTossup.negValue); - - this.previousTossup = { - ...this.previousTossup, - celerity, - endOfQuestion, - inPower, - isCorrect, - tossup: this.tossup, - userId: this.buzzedIn - }; - - return { celerity, directive, directedPrompt, endOfQuestion, inPower, points }; - } - - setReadingSpeed(userId, { readingSpeed }) { - if (isNaN(readingSpeed)) { return false; } - if (readingSpeed > 100) { readingSpeed = 100; } - if (readingSpeed < 0) { readingSpeed = 0; } - - this.settings.readingSpeed = readingSpeed; - const username = this.players[userId].username; - this.emitMessage({ type: 'set-reading-speed', username, readingSpeed }); - } - - async startNextTossup(userId) { - const username = this.players[userId].username; - this.tossup = await this.getNextQuestion('tossups'); - this.queryingQuestion = false; - if (!this.tossup) { return; } - this.emitMessage({ type: 'start-next-tossup', packetLength: this.packet.tossups.length, tossup: this.tossup, userId, username }); - this.questionSplit = this.tossup.question_sanitized.split(' ').filter(word => word !== ''); - this.wordIndex = 0; - this.tossupProgress = TOSSUP_PROGRESS_ENUM.READING; - clearTimeout(this.timeoutID); - this.readQuestion(Date.now()); - } - - togglePowermarkOnly(userId, { powermarkOnly }) { - this.query.powermarkOnly = powermarkOnly; - const username = this.players[userId].username; - this.adjustQuery(['powermarkOnly'], [powermarkOnly]); - this.emitMessage({ type: 'toggle-powermark-only', powermarkOnly, username }); - } - - toggleRebuzz(userId, { rebuzz }) { - this.settings.rebuzz = rebuzz; - const username = this.players[userId].username; - this.emitMessage({ type: 'toggle-rebuzz', rebuzz, username }); - } - - toggleStopOnPower(userId, { stopOnPower }) { - this.settings.stopOnPower = stopOnPower; - const username = this.players[userId].username; - this.emitMessage({ type: 'toggle-stop-on-power', stopOnPower, username }); - } -}; +import { + ANSWER_TIME_LIMIT, + DEAD_TIME_LIMIT, + MODE_ENUM, + TOSSUP_PROGRESS_ENUM, +} from "./constants.js"; +import insertTokensIntoHTML from "./insert-tokens-into-html.js"; +import QuestionRoom from "./QuestionRoom.js"; + +export const TossupRoomMixin = (QuestionRoomClass) => + class extends QuestionRoomClass { + constructor(name, categoryManager, supportedQuestionTypes = ["tossup"]) { + super(name, categoryManager, supportedQuestionTypes); + + this.timeoutID = null; + /** + * @type {string | null} + * The userId of the player who buzzed in. + * We should ensure that buzzedIn is null before calling any readQuestion. + */ + this.buzzedIn = null; + this.buzzes = []; + this.buzzpointIndices = []; + this.liveAnswer = ""; + this.paused = false; + this.questionSplit = []; + this.tossup = {}; + this.tossupProgress = TOSSUP_PROGRESS_ENUM.NOT_STARTED; + this.wordIndex = 0; + + this.query = { + ...this.query, + powermarkOnly: false, + }; + + this.settings = { + ...this.settings, + rebuzz: false, + stopOnPower: false, + readingSpeed: 50, + }; + + this.previousTossup = { + celerity: 0, + endOfQuestion: false, + isCorrect: true, + inPower: false, + negValue: -5, + powerValue: 15, + tossup: {}, + userId: null, + }; + } + + async message(userId, message) { + switch (message.type) { + case "buzz": + return this.buzz(userId, message); + case "give-answer": + return this.giveTossupAnswer(userId, message); + case "next": + return this.next(userId, message); + case "pause": + return this.pause(userId, message); + case "set-reading-speed": + return this.setReadingSpeed(userId, message); + case "toggle-powermark-only": + return this.togglePowermarkOnly(userId, message); + case "toggle-rebuzz": + return this.toggleRebuzz(userId, message); + case "toggle-stop-on-power": + return this.toggleStopOnPower(userId, message); + default: + return super.message(userId, message); + } + } + + buzz(userId) { + if (!this.settings.rebuzz && this.buzzes.includes(userId)) { + return; + } + if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.READING) { + return; + } + + const username = this.players[userId].username; + if (this.buzzedIn) { + return this.emitMessage({ type: "lost-buzzer-race", userId, username }); + } + + clearTimeout(this.timeoutID); + this.buzzedIn = userId; + this.buzzes.push(userId); + this.buzzpointIndices.push( + this.questionSplit.slice(0, this.wordIndex).join(" ").length, + ); + this.paused = false; + + this.emitMessage({ type: "buzz", userId, username }); + this.emitMessage({ type: "update-question", word: "(#)" }); + + this.startServerTimer( + ANSWER_TIME_LIMIT * 10, + (time) => + this.emitMessage({ type: "timer-update", timeRemaining: time }), + () => this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }), + ); + } + + endCurrentTossup(userId) { + if (this.buzzedIn) { + return false; + } // prevents skipping when someone has buzzed in + if (this.queryingQuestion) { + return false; + } + const isSkip = this.tossupProgress === TOSSUP_PROGRESS_ENUM.READING; + if (isSkip && !this.settings.skip) { + return false; + } + + clearInterval(this.timer.interval); + clearTimeout(this.timeoutID); + this.emitMessage({ type: "timer-update", timeRemaining: 0 }); + + this.buzzedIn = null; + this.buzzes = []; + this.buzzpointIndices = []; + this.paused = false; + + if (this.tossupProgress !== TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { + this.revealTossupAnswer(); + } + + const starred = + this.mode === MODE_ENUM.STARRED + ? true + : this.mode === MODE_ENUM.LOCAL + ? false + : null; + this.emitMessage({ + type: "end-current-tossup", + isSkip, + starred, + tossup: this.tossup, + }); + return true; + } + + giveTossupAnswer(userId, { givenAnswer }) { + if (typeof givenAnswer !== "string") { + return false; + } + if (this.buzzedIn !== userId) { + return false; + } + + this.liveAnswer = ""; + clearInterval(this.timer.interval); + this.emitMessage({ + type: "timer-update", + timeRemaining: ANSWER_TIME_LIMIT * 10, + }); + + if (Object.keys(this.tossup || {}).length === 0) { + return; + } + + const { celerity, directive, directedPrompt, points } = this.scoreTossup({ + givenAnswer, + }); + + switch (directive) { + case "accept": + this.buzzedIn = null; + this.revealTossupAnswer(); + this.players[userId].updateStats(points, celerity); + Object.values(this.players).forEach((player) => { + player.tuh++; + }); + break; + case "reject": + this.buzzedIn = null; + this.players[userId].updateStats(points, celerity); + if ( + !this.settings.rebuzz && + this.buzzes.length === Object.keys(this.sockets).length + ) { + this.revealTossupAnswer(); + Object.values(this.players).forEach((player) => { + player.tuh++; + }); + } else { + this.readQuestion(Date.now()); + } + break; + case "prompt": + this.startServerTimer( + ANSWER_TIME_LIMIT * 10, + (time) => + this.emitMessage({ type: "timer-update", timeRemaining: time }), + () => + this.giveTossupAnswer(userId, { givenAnswer: this.liveAnswer }), + ); + } + + this.emitMessage({ + type: "give-tossup-answer", + userId, + username: this.players[userId].username, + givenAnswer, + directive, + directedPrompt, + score: points, + celerity: this.players[userId].celerity.correct.average, + // the below fields are used to record buzzpoint data + tossup: this.tossup, + perQuestionCelerity: celerity, + }); + } + + async next(userId) { + if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.NOT_STARTED) { + return await this.startNextTossup(userId); + } + const allowed = this.endCurrentTossup(userId); + if (allowed) { + await this.startNextTossup(userId); + } + } + + pause(userId) { + if (this.buzzedIn) { + return false; + } + if (this.tossupProgress === TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED) { + return false; + } + + this.paused = !this.paused; + if (this.paused) { + clearTimeout(this.timeoutID); + clearInterval(this.timer.interval); + } else if (this.wordIndex >= this.questionSplit.length) { + this.startServerTimer( + this.timer.timeRemaining, + (time) => + this.emitMessage({ type: "timer-update", timeRemaining: time }), + () => this.revealTossupAnswer(), + ); + } else { + this.readQuestion(Date.now()); + } + const username = this.players[userId].username; + this.emitMessage({ type: "pause", paused: this.paused, username }); + } + + async readQuestion(expectedReadTime) { + if (Object.keys(this.tossup || {}).length === 0) { + return; + } + if (this.wordIndex >= this.questionSplit.length) { + this.startServerTimer( + DEAD_TIME_LIMIT * 10, + (time) => + this.emitMessage({ type: "timer-update", timeRemaining: time }), + () => this.revealTossupAnswer(), + ); + return; + } + + const word = this.questionSplit[this.wordIndex]; + + // stop reading and start timer if power and stopOnPower is enabled + if ((word === "(*)" || word === "[*]") && this.settings.stopOnPower) { + this.startServerTimer( + DEAD_TIME_LIMIT * 10, + (time) => + this.emitMessage({ type: "timer-update", timeRemaining: time }), + () => this.revealTossupAnswer(), + ); + return; + } + + this.wordIndex++; + this.emitMessage({ type: "update-question", word }); + + // calculate time needed before reading next word + let time = Math.log(word.length) + 1; + if ( + (word.endsWith(".") && + word.charCodeAt(word.length - 2) > 96 && + word.charCodeAt(word.length - 2) < 123) || + word.slice(-2) === ".\u201d" || + word.slice(-2) === "!\u201d" || + word.slice(-2) === "?\u201d" + ) { + time += 2.5; + } else if (word.endsWith(",") || word.slice(-2) === ",\u201d") { + time += 1.5; + } else if (word === "(*)" || word === "[*]") { + time = 0; + } + + time = time * 0.9 * (140 - this.settings.readingSpeed); + const delay = time - Date.now() + expectedReadTime; + + this.timeoutID = setTimeout(() => { + this.readQuestion(time + expectedReadTime); + }, delay); + } + + revealTossupAnswer() { + if (Object.keys(this.tossup || {}).length === 0) return; + this.tossupProgress = TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED; + this.tossup.markedQuestion = insertTokensIntoHTML( + this.tossup.question, + this.tossup.question_sanitized, + { " (#) ": this.buzzpointIndices }, + ); + this.emitMessage({ + type: "reveal-tossup-answer", + question: insertTokensIntoHTML( + this.tossup.question, + this.tossup.question_sanitized, + { " (#) ": this.buzzpointIndices }, + ), + answer: this.tossup.answer, + }); + } + + scoreTossup({ givenAnswer }) { + const celerity = + this.questionSplit.slice(this.wordIndex).join(" ").length / + this.tossup.question.length; + const endOfQuestion = this.wordIndex === this.questionSplit.length; + const inPower = + Math.max( + this.questionSplit.indexOf("(*)"), + this.questionSplit.indexOf("[*]"), + ) >= this.wordIndex; + const { directive, directedPrompt } = this.checkAnswer( + this.tossup.answer, + givenAnswer, + this.settings.strictness, + ); + const isCorrect = directive === "accept"; + const points = isCorrect + ? inPower + ? this.previousTossup.powerValue + : 10 + : endOfQuestion + ? 0 + : this.previousTossup.negValue; + + this.previousTossup = { + ...this.previousTossup, + celerity, + endOfQuestion, + inPower, + isCorrect, + tossup: this.tossup, + userId: this.buzzedIn, + }; + + return { + celerity, + directive, + directedPrompt, + endOfQuestion, + inPower, + points, + }; + } + + setReadingSpeed(userId, { readingSpeed }) { + if (isNaN(readingSpeed)) { + return false; + } + if (readingSpeed > 100) { + readingSpeed = 100; + } + if (readingSpeed < 0) { + readingSpeed = 0; + } + + this.settings.readingSpeed = readingSpeed; + const username = this.players[userId].username; + this.emitMessage({ type: "set-reading-speed", username, readingSpeed }); + } + + async startNextTossup(userId) { + const username = this.players[userId].username; + this.tossup = await this.getNextQuestion("tossups"); + this.queryingQuestion = false; + if (!this.tossup) { + return; + } + this.emitMessage({ + type: "start-next-tossup", + packetLength: this.packet.tossups.length, + tossup: this.tossup, + userId, + username, + }); + this.questionSplit = this.tossup.question_sanitized + .split(" ") + .filter((word) => word !== ""); + this.wordIndex = 0; + this.tossupProgress = TOSSUP_PROGRESS_ENUM.READING; + clearTimeout(this.timeoutID); + this.readQuestion(Date.now()); + } + + togglePowermarkOnly(userId, { powermarkOnly }) { + this.query.powermarkOnly = powermarkOnly; + const username = this.players[userId].username; + this.adjustQuery(["powermarkOnly"], [powermarkOnly]); + this.emitMessage({ + type: "toggle-powermark-only", + powermarkOnly, + username, + }); + } + + toggleRebuzz(userId, { rebuzz }) { + this.settings.rebuzz = rebuzz; + const username = this.players[userId].username; + this.emitMessage({ type: "toggle-rebuzz", rebuzz, username }); + } + + toggleStopOnPower(userId, { stopOnPower }) { + this.settings.stopOnPower = stopOnPower; + const username = this.players[userId].username; + this.emitMessage({ type: "toggle-stop-on-power", stopOnPower, username }); + } + }; const TossupRoom = TossupRoomMixin(QuestionRoom); export default TossupRoom; From 2ac3e679434b599cd28c386d5795b38e6076f8a1 Mon Sep 17 00:00:00 2001 From: Theodore Chen Date: Mon, 16 Feb 2026 07:01:53 -0500 Subject: [PATCH 3/7] updating scoring in stopOnPower to 10 for correct, -5 incorrect interruption, and 0 incorrect end of question --- quizbowl/TossupRoom.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/quizbowl/TossupRoom.js b/quizbowl/TossupRoom.js index 0dd8e9665..56107485b 100644 --- a/quizbowl/TossupRoom.js +++ b/quizbowl/TossupRoom.js @@ -25,6 +25,7 @@ export const TossupRoomMixin = (QuestionRoomClass) => this.paused = false; this.questionSplit = []; this.tossup = {}; + this.stopOnPowerEnded = false; this.tossupProgress = TOSSUP_PROGRESS_ENUM.NOT_STARTED; this.wordIndex = 0; @@ -273,6 +274,7 @@ export const TossupRoomMixin = (QuestionRoomClass) => // stop reading and start timer if power and stopOnPower is enabled if ((word === "(*)" || word === "[*]") && this.settings.stopOnPower) { + this.stopOnPowerEnded = true; this.startServerTimer( DEAD_TIME_LIMIT * 10, (time) => @@ -333,7 +335,9 @@ export const TossupRoomMixin = (QuestionRoomClass) => const celerity = this.questionSplit.slice(this.wordIndex).join(" ").length / this.tossup.question.length; - const endOfQuestion = this.wordIndex === this.questionSplit.length; + const endOfQuestion = this.settings.stopOnPower + ? this.stopOnPowerEnded + : this.wordIndex === this.questionSplit.length; const inPower = Math.max( this.questionSplit.indexOf("(*)"), @@ -345,13 +349,19 @@ export const TossupRoomMixin = (QuestionRoomClass) => this.settings.strictness, ); const isCorrect = directive === "accept"; - const points = isCorrect - ? inPower - ? this.previousTossup.powerValue - : 10 - : endOfQuestion - ? 0 - : this.previousTossup.negValue; + const points = this.settings.stopOnPower + ? isCorrect + ? 10 + : this.stopOnPowerEnded + ? 0 + : this.previousTossup.negValue + : isCorrect + ? inPower + ? this.previousTossup.powerValue + : 10 + : endOfQuestion + ? 0 + : this.previousTossup.negValue; this.previousTossup = { ...this.previousTossup, From 79b63c395671ff7ba79c993994cf18e42eac9016 Mon Sep 17 00:00:00 2001 From: Theodore Chen Date: Mon, 16 Feb 2026 10:33:17 -0500 Subject: [PATCH 4/7] added stop on power to mp --- .../play/mp/MultiplayerTossupBonusClient.js | 1700 ++++++++++------- client/play/mp/room.html | 1047 +++++++--- client/play/mp/room.jsx | 267 ++- 3 files changed, 1935 insertions(+), 1079 deletions(-) diff --git a/client/play/mp/MultiplayerTossupBonusClient.js b/client/play/mp/MultiplayerTossupBonusClient.js index c6de9bf5c..894156d5a 100644 --- a/client/play/mp/MultiplayerTossupBonusClient.js +++ b/client/play/mp/MultiplayerTossupBonusClient.js @@ -1,732 +1,1050 @@ +import { + MODE_ENUM, + QUESTION_TYPE_ENUM, + TOSSUP_PROGRESS_ENUM, +} from "../../../quizbowl/constants.js"; +import questionStats from "../../scripts/auth/question-stats.js"; +import TossupBonusClient from "../TossupBonusClient.js"; +import { arrayToRange } from "../ranges.js"; +import upsertPlayerItem from "../upsert-player-item.js"; +import { setYear } from "../year-slider.js"; + +export const MultiplayerClientMixin = (ClientClass) => + class extends ClientClass { + constructor(room, userId, socket) { + console.log("MultiplayerClinetMixin is active"); + super(room, userId, socket); + this.socket = socket; + } + + onmessage(event) { + const data = JSON.parse(event.data); + switch (data.type) { + case "chat": + return this.chat(data, false); + case "chat-live-update": + return this.chat(data, true); + case "clear-stats": + return this.clearStats(data); + case "confirm-ban": + return this.confirmBan(data); + case "connection-acknowledged": + return this.connectionAcknowledged(data); + case "connection-acknowledged-query": + return this.connectionAcknowledgedQuery(data); + case "connection-acknowledged-question": + return this.connectionAcknowledgedQuestion(data); + case "enforcing-removal": + return this.ackRemovedFromRoom(data); + case "error": + return this.handleError(data); + case "force-username": + return this.forceUsername(data); + case "give-answer-live-update": + return this.logGiveAnswer(data); + case "initiated-vk": + return this.vkInit(data); + case "join": + return this.join(data); + case "leave": + return this.leave(data); + case "lost-buzzer-race": + return this.lostBuzzerRace(data); + case "mute-player": + return this.mutePlayer(data); + case "no-points-votekick-attempt": + return this.failedVotekickPoints(data); + case "owner-change": + return this.ownerChange(data); + case "set-username": + return this.setUsername(data); + case "successful-vk": + return this.vkHandle(data); + case "toggle-controlled": + return this.toggleControlled(data); + case "toggle-lock": + return this.toggleLock(data); + case "toggle-login-required": + return this.toggleLoginRequired(data); + case "toggle-public": + return this.togglePublic(data); + case "toggle-stop-on-power": + return this.toggleStopOnPower(data); + default: + return super.onmessage(event.data); + } + } -import { MODE_ENUM, QUESTION_TYPE_ENUM, TOSSUP_PROGRESS_ENUM } from '../../../quizbowl/constants.js'; -import questionStats from '../../scripts/auth/question-stats.js'; -import TossupBonusClient from '../TossupBonusClient.js'; -import { arrayToRange } from '../ranges.js'; -import upsertPlayerItem from '../upsert-player-item.js'; -import { setYear } from '../year-slider.js'; - -export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass { - constructor (room, userId, socket) { - super(room, userId, socket); - this.socket = socket; - } - - onmessage (event) { - const data = JSON.parse(event.data); - switch (data.type) { - case 'chat': return this.chat(data, false); - case 'chat-live-update': return this.chat(data, true); - case 'clear-stats': return this.clearStats(data); - case 'confirm-ban': return this.confirmBan(data); - case 'connection-acknowledged': return this.connectionAcknowledged(data); - case 'connection-acknowledged-query': return this.connectionAcknowledgedQuery(data); - case 'connection-acknowledged-question': return this.connectionAcknowledgedQuestion(data); - case 'enforcing-removal': return this.ackRemovedFromRoom(data); - case 'error': return this.handleError(data); - case 'force-username': return this.forceUsername(data); - case 'give-answer-live-update': return this.logGiveAnswer(data); - case 'initiated-vk': return this.vkInit(data); - case 'join': return this.join(data); - case 'leave': return this.leave(data); - case 'lost-buzzer-race': return this.lostBuzzerRace(data); - case 'mute-player': return this.mutePlayer(data); - case 'no-points-votekick-attempt': return this.failedVotekickPoints(data); - case 'owner-change': return this.ownerChange(data); - case 'set-username': return this.setUsername(data); - case 'successful-vk': return this.vkHandle(data); - case 'toggle-controlled': return this.toggleControlled(data); - case 'toggle-lock': return this.toggleLock(data); - case 'toggle-login-required': return this.toggleLoginRequired(data); - case 'toggle-public': return this.togglePublic(data); - default: return super.onmessage(event.data); - } - } - - // if a banned/kicked user tries to join a this.room they were removed from this is the response - ackRemovedFromRoom ({ removalType }) { - if (removalType === 'kick') { - window.alert('You were kicked from this room by room players, and cannot rejoin it.'); - } else { - window.alert('You were banned from this room by the room owner, and cannot rejoin it.'); - } - setTimeout(() => { - window.location.replace('../'); - }, 100); - } - - buzz ({ userId, username }) { - this.logEventConditionally(username, 'buzzed'); - if (userId === this.USER_ID) { - document.getElementById('answer-input-group').classList.remove('d-none'); - document.getElementById('answer-input').focus(); - } - super.buzz({ userId }); - } - - chat ({ message, userId, username }, live = false) { - if (this.room.muteList.includes(userId)) { - return; - } - if (!live && message === '') { - document.getElementById('live-chat-' + userId).parentElement.remove(); - return; - } - - if (!live && message) { - document.getElementById('live-chat-' + userId).className = ''; - document.getElementById('live-chat-' + userId).id = ''; - return; - } - - if (document.getElementById('live-chat-' + userId)) { - document.getElementById('live-chat-' + userId).textContent = message; - return; - } - - const b = document.createElement('b'); - b.textContent = username; - - const span = document.createElement('span'); - span.classList.add('text-muted'); - span.id = 'live-chat-' + userId; - span.textContent = message; - - const li = document.createElement('li'); - li.appendChild(b); - li.appendChild(document.createTextNode(' ')); - li.appendChild(span); - document.getElementById('room-history').prepend(li); - } - - clearStats ({ userId }) { - for (const field of ['celerity', 'negs', 'points', 'powers', 'tens', 'tuh', 'zeroes']) { - this.room.players[userId][field] = 0; - } - upsertPlayerItem(this.room.players[userId], this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[this.room.players[userId].teamId]); - this.sortPlayerListGroup(); - } - - confirmBan ({ targetId, targetUsername }) { - if (targetId === this.USER_ID) { - window.alert('You were banned from this room by the room owner.'); + // if a banned/kicked user tries to join a this.room they were removed from this is the response + ackRemovedFromRoom({ removalType }) { + if (removalType === "kick") { + window.alert( + "You were kicked from this room by room players, and cannot rejoin it.", + ); + } else { + window.alert( + "You were banned from this room by the room owner, and cannot rejoin it.", + ); + } setTimeout(() => { - window.location.replace('../'); + window.location.replace("../"); }, 100); - } else { - this.logEventConditionally(targetUsername + ' has been banned from this room.'); - } - } - - connectionAcknowledged ({ - bonusEligibleTeamId, - bonusProgress, - buzzedIn, - canBuzz, - currentQuestionType, - isPermanent, - ownerId, - mode, - packetLength, - players, - settings, - packetCount, - teams, - tossupProgress, - userId - }) { - this.room.bonusEligibleTeamId = bonusEligibleTeamId; - this.room.public = settings.public; - this.room.ownerId = ownerId; - this.room.setLength = packetCount; - this.USER_ID = userId; - window.localStorage.setItem('USER_ID', this.USER_ID); - - document.getElementById('buzz').disabled = !canBuzz; - document.getElementById('reveal').disabled = currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP || userId !== bonusEligibleTeamId; - - if (isPermanent) { - document.getElementById('category-select-button').disabled = true; - document.getElementById('toggle-enable-bonuses').disabled = true; - document.getElementById('permanent-room-warning').classList.remove('d-none'); - document.getElementById('reading-speed').disabled = true; - document.getElementById('set-strictness').disabled = true; - document.getElementById('set-mode').disabled = true; - document.getElementById('toggle-public').disabled = true; - } - - for (const userId of Object.keys(players)) { - const teamId = players[userId].teamId; - players[userId].celerity = players[userId].celerity.correct.average; - this.room.players[userId] = players[userId]; - this.room.teams[teamId] = teams[teamId]; - upsertPlayerItem(this.room.players[userId], this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[teamId]); - } - this.sortPlayerListGroup(); - - document.getElementById('packet-length-info').textContent = mode === MODE_ENUM.SET_NAME ? packetLength : '-'; - - if (currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP) { - switch (tossupProgress) { - case TOSSUP_PROGRESS_ENUM.NOT_STARTED: - document.getElementById('buzz').disabled = true; - document.getElementById('next').textContent = 'Start'; - document.getElementById('next').classList.remove('btn-primary'); - document.getElementById('next').classList.add('btn-success'); + } + + buzz({ userId, username }) { + this.logEventConditionally(username, "buzzed"); + if (userId === this.USER_ID) { + document + .getElementById("answer-input-group") + .classList.remove("d-none"); + document.getElementById("answer-input").focus(); + } + super.buzz({ userId }); + } + + chat({ message, userId, username }, live = false) { + if (this.room.muteList.includes(userId)) { + return; + } + if (!live && message === "") { + document.getElementById("live-chat-" + userId).parentElement.remove(); + return; + } + + if (!live && message) { + document.getElementById("live-chat-" + userId).className = ""; + document.getElementById("live-chat-" + userId).id = ""; + return; + } + + if (document.getElementById("live-chat-" + userId)) { + document.getElementById("live-chat-" + userId).textContent = message; + return; + } + + const b = document.createElement("b"); + b.textContent = username; + + const span = document.createElement("span"); + span.classList.add("text-muted"); + span.id = "live-chat-" + userId; + span.textContent = message; + + const li = document.createElement("li"); + li.appendChild(b); + li.appendChild(document.createTextNode(" ")); + li.appendChild(span); + document.getElementById("room-history").prepend(li); + } + + clearStats({ userId }) { + for (const field of [ + "celerity", + "negs", + "points", + "powers", + "tens", + "tuh", + "zeroes", + ]) { + this.room.players[userId][field] = 0; + } + upsertPlayerItem( + this.room.players[userId], + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[this.room.players[userId].teamId], + ); + this.sortPlayerListGroup(); + } + + confirmBan({ targetId, targetUsername }) { + if (targetId === this.USER_ID) { + window.alert("You were banned from this room by the room owner."); + setTimeout(() => { + window.location.replace("../"); + }, 100); + } else { + this.logEventConditionally( + targetUsername + " has been banned from this room.", + ); + } + } + + connectionAcknowledged({ + bonusEligibleTeamId, + bonusProgress, + buzzedIn, + canBuzz, + currentQuestionType, + isPermanent, + ownerId, + mode, + packetLength, + players, + settings, + packetCount, + teams, + tossupProgress, + userId, + }) { + this.room.bonusEligibleTeamId = bonusEligibleTeamId; + this.room.public = settings.public; + this.room.ownerId = ownerId; + this.room.setLength = packetCount; + this.USER_ID = userId; + window.localStorage.setItem("USER_ID", this.USER_ID); + + document.getElementById("buzz").disabled = !canBuzz; + document.getElementById("reveal").disabled = + currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP || + userId !== bonusEligibleTeamId; + + if (isPermanent) { + document.getElementById("category-select-button").disabled = true; + document.getElementById("toggle-enable-bonuses").disabled = true; + document + .getElementById("permanent-room-warning") + .classList.remove("d-none"); + document.getElementById("reading-speed").disabled = true; + document.getElementById("set-strictness").disabled = true; + document.getElementById("set-mode").disabled = true; + document.getElementById("toggle-public").disabled = true; + } + + for (const userId of Object.keys(players)) { + const teamId = players[userId].teamId; + players[userId].celerity = players[userId].celerity.correct.average; + this.room.players[userId] = players[userId]; + this.room.teams[teamId] = teams[teamId]; + upsertPlayerItem( + this.room.players[userId], + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[teamId], + ); + } + this.sortPlayerListGroup(); + + document.getElementById("packet-length-info").textContent = + mode === MODE_ENUM.SET_NAME ? packetLength : "-"; + + if (currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP) { + switch (tossupProgress) { + case TOSSUP_PROGRESS_ENUM.NOT_STARTED: + document.getElementById("buzz").disabled = true; + document.getElementById("next").textContent = "Start"; + document.getElementById("next").classList.remove("btn-primary"); + document.getElementById("next").classList.add("btn-success"); + break; + case TOSSUP_PROGRESS_ENUM.READING: + document.getElementById("next").textContent = "Skip"; + document.getElementById("settings").classList.add("d-none"); + if (buzzedIn) { + document.getElementById("buzz").disabled = true; + document.getElementById("next").disabled = true; + document.getElementById("pause").disabled = true; + } else { + document.getElementById("buzz").disabled = false; + document.getElementById("pause").disabled = false; + } + break; + case TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED: + document.getElementById("buzz").disabled = true; + document.getElementById("next").textContent = "Next"; + document.getElementById("settings").classList.add("d-none"); + break; + } + } else if (currentQuestionType === QUESTION_TYPE_ENUM.BONUS) { + document.getElementById("buzz").disabled = true; + } + + this.toggleEnableBonuses({ enableBonuses: settings.enableBonuses }); + this.toggleLock({ lock: settings.lock }); + this.toggleLoginRequired({ loginRequired: settings.loginRequired }); + this.toggleRebuzz({ rebuzz: settings.rebuzz }); + this.toggleSkip({ skip: settings.skip }); + this.toggleTimer({ timer: settings.timer }); + this.setMode({ mode }); + this.setReadingSpeed({ readingSpeed: settings.readingSpeed }); + this.setStrictness({ strictness: settings.strictness }); + + if (settings.controlled) { + this.toggleControlled({ controlled: settings.controlled }); + } + if (settings.public) { + this.togglePublic({ public: settings.public }); + } + } + + async connectionAcknowledgedQuery({ + difficulties = [], + minYear, + maxYear, + packetNumbers = [], + powermarkOnly, + setName = "", + standardOnly, + alternateSubcategories, + categories, + subcategories, + percentView, + categoryPercents, + }) { + this.setDifficulties({ difficulties }); + + // need to set min year first to avoid conflicts between saved max year and default min year + setYear(minYear, "min-year"); + setYear(maxYear, "max-year"); + + document.getElementById("packet-number").value = + arrayToRange(packetNumbers); + document.getElementById("set-name").value = setName; + document.getElementById("toggle-powermark-only").checked = powermarkOnly; + + if (setName !== "" && this.room.setLength === 0) { + document.getElementById("set-name").classList.add("is-invalid"); + } + + document.getElementById("toggle-standard-only").checked = standardOnly; + + this.setCategories({ + categories, + subcategories, + alternateSubcategories, + percentView, + categoryPercents, + }); + } + + connectionAcknowledgedQuestion({ currentQuestionType, question }) { + document.getElementById("set-name-info").textContent = + this.question?.set?.name ?? ""; + document.getElementById("packet-number-info").textContent = + this.question?.packet?.number ?? "-"; + document.getElementById("question-number-info").textContent = + this.question?.number ?? "-"; + + if (currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP) { + this.room.tossup = question; + } else if (currentQuestionType === QUESTION_TYPE_ENUM.BONUS) { + this.room.bonus = question; + } + } + + endCurrentBonus({ + bonus, + lastPartRevealed, + pointsPerPart, + starred, + teamId, + }) { + super.endCurrentBonus({ bonus, starred }); + if (lastPartRevealed) { + const points = pointsPerPart.reduce((a, b) => a + b, 0); + this.room.teams[teamId].bonusStats[points]++; + upsertPlayerItem( + this.room.players[teamId], + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[teamId], + ); + this.sortPlayerListGroup(); + } + } + + failedVotekickPoints({ userId }) { + if (userId === this.USER_ID) { + window.alert( + "You can only votekick once you have answered a question correctly!", + ); + } + } + + forceUsername({ message, username }) { + window.alert(message); + window.localStorage.setItem("multiplayer-username", username); + document.querySelector("#username").value = username; + } + + async giveBonusAnswer({ + currentPartNumber, + directive, + directedPrompt, + givenAnswer, + score, + userId, + username, + }) { + this.logGiveAnswer({ + directive, + givenAnswer, + questionType: QUESTION_TYPE_ENUM.BONUS, + username, + }); + if (directive === "prompt" && directedPrompt) { + this.logEventConditionally( + username, + `was prompted with "${directedPrompt}"`, + ); + } else if (directive === "prompt") { + this.logEventConditionally(username, "was prompted"); + } + super.giveBonusAnswer({ + currentPartNumber, + directive, + directedPrompt, + userId, + }); + } + + async giveTossupAnswer({ + celerity, + tossup, + perQuestionCelerity, + directive, + directedPrompt, + givenAnswer, + score, + userId, + username, + }) { + this.logGiveAnswer({ + directive, + givenAnswer, + questionType: QUESTION_TYPE_ENUM.TOSSUP, + username, + }); + if (directive === "prompt" && directedPrompt) { + this.logEventConditionally( + username, + `was prompted with "${directedPrompt}"`, + ); + } else if (directive === "prompt") { + this.logEventConditionally(username, "was prompted"); + } else { + this.logEventConditionally( + username, + `${score > 0 ? "" : "in"}correctly answered for ${score} points`, + ); + } + super.giveTossupAnswer({ + directive, + directedPrompt, + givenAnswer, + score, + userId, + username, + }); + + if (directive === "prompt") { + return; + } + + document.getElementById("pause").disabled = false; + + if (directive === "accept") { + document.getElementById("buzz").disabled = true; + Array.from(document.getElementsByClassName("tuh")).forEach( + (element) => { + element.textContent = parseInt(element.innerHTML) + 1; + }, + ); + this.room.bonusEligibleTeamId = this.room.players[userId].teamId; + } + + if (directive === "reject") { + document.getElementById("buzz").disabled = + !document.getElementById("toggle-rebuzz").checked && + userId === this.USER_ID; + } + + if (score > 10) { + this.room.players[userId].powers++; + } else if (score === 10) { + this.room.players[userId].tens++; + } else if (score < 0) { + this.room.players[userId].negs++; + } + + this.room.players[userId].points += score; + this.room.players[userId].tuh++; + this.room.players[userId].celerity = celerity; + + upsertPlayerItem( + this.room.players[userId], + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[this.room.players[userId].teamId], + ); + this.sortPlayerListGroup(); + + if (userId === this.USER_ID) { + questionStats.recordTossup({ + _id: tossup._id, + celerity: perQuestionCelerity, + isCorrect: score > 0, + multiplayer: true, + pointValue: score, + }); + } + } + + handleError({ message }) { + this.socket.close(3000); + window.alert(message); + window.location.href = "/multiplayer"; + } + + join({ isNew, team, user, userId, username }) { + this.logEventConditionally(username, "joined the game"); + if (userId === this.USER_ID) { + return; + } + this.room.players[userId] = user; + this.room.teams[user.teamId] = team; + + if (isNew) { + user.celerity = user.celerity.correct.average; + upsertPlayerItem( + user, + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[user.teamId], + ); + this.sortPlayerListGroup(); + } else { + document + .getElementById(`list-group-${userId}`) + .classList.remove("offline"); + document.getElementById("points-" + userId).classList.add("bg-success"); + document + .getElementById("points-" + userId) + .classList.remove("bg-secondary"); + document.getElementById("username-" + userId).textContent = username; + } + } + + leave({ userId, username }) { + this.logEventConditionally(username, "left the game"); + this.room.players[userId].online = false; + document.getElementById(`list-group-${userId}`).classList.add("offline"); + document + .getElementById(`points-${userId}`) + .classList.remove("bg-success"); + document.getElementById(`points-${userId}`).classList.add("bg-secondary"); + } + + /** + * Log the event, but only if `username !== undefined`. + * If username is undefined, do nothing, regardless of the value of message. + * @param {string | undefined} username + * @param {string | undefined} message + */ + logEventConditionally(username, message) { + if (username === undefined) { + return; + } + + const span1 = document.createElement("span"); + span1.textContent = username; + + const span2 = document.createElement("span"); + span2.textContent = message; + + const i = document.createElement("i"); + i.appendChild(span1); + i.appendChild(document.createTextNode(" ")); + i.appendChild(span2); + + const li = document.createElement("li"); + li.appendChild(i); + + document.getElementById("room-history").prepend(li); + } + + logGiveAnswer({ directive = null, givenAnswer, questionType, username }) { + const badge = document.createElement("span"); + badge.textContent = + questionType === QUESTION_TYPE_ENUM.TOSSUP ? "Buzz" : "Answer"; + switch (directive) { + case "accept": + badge.className = "badge text-dark bg-success"; break; - case TOSSUP_PROGRESS_ENUM.READING: - document.getElementById('next').textContent = 'Skip'; - document.getElementById('settings').classList.add('d-none'); - if (buzzedIn) { - document.getElementById('buzz').disabled = true; - document.getElementById('next').disabled = true; - document.getElementById('pause').disabled = true; - } else { - document.getElementById('buzz').disabled = false; - document.getElementById('pause').disabled = false; - } + case "reject": + badge.className = "badge text-light bg-danger"; + break; + case "prompt": + badge.className = "badge text-dark bg-warning"; break; - case TOSSUP_PROGRESS_ENUM.ANSWER_REVEALED: - document.getElementById('buzz').disabled = true; - document.getElementById('next').textContent = 'Next'; - document.getElementById('settings').classList.add('d-none'); + default: + badge.className = "badge text-light bg-primary"; break; } - } else if (currentQuestionType === QUESTION_TYPE_ENUM.BONUS) { - document.getElementById('buzz').disabled = true; - } - - this.toggleEnableBonuses({ enableBonuses: settings.enableBonuses }); - this.toggleLock({ lock: settings.lock }); - this.toggleLoginRequired({ loginRequired: settings.loginRequired }); - this.toggleRebuzz({ rebuzz: settings.rebuzz }); - this.toggleSkip({ skip: settings.skip }); - this.toggleTimer({ timer: settings.timer }); - this.setMode({ mode }); - this.setReadingSpeed({ readingSpeed: settings.readingSpeed }); - this.setStrictness({ strictness: settings.strictness }); - - if (settings.controlled) { - this.toggleControlled({ controlled: settings.controlled }); - } - if (settings.public) { - this.togglePublic({ public: settings.public }); - } - } - - async connectionAcknowledgedQuery ({ - difficulties = [], - minYear, - maxYear, - packetNumbers = [], - powermarkOnly, - setName = '', - standardOnly, - alternateSubcategories, - categories, - subcategories, - percentView, - categoryPercents - }) { - this.setDifficulties({ difficulties }); - - // need to set min year first to avoid conflicts between saved max year and default min year - setYear(minYear, 'min-year'); - setYear(maxYear, 'max-year'); - - document.getElementById('packet-number').value = arrayToRange(packetNumbers); - document.getElementById('set-name').value = setName; - document.getElementById('toggle-powermark-only').checked = powermarkOnly; - - if (setName !== '' && this.room.setLength === 0) { - document.getElementById('set-name').classList.add('is-invalid'); - } - - document.getElementById('toggle-standard-only').checked = standardOnly; - - this.setCategories({ categories, subcategories, alternateSubcategories, percentView, categoryPercents }); - } - - connectionAcknowledgedQuestion ({ currentQuestionType, question }) { - document.getElementById('set-name-info').textContent = this.question?.set?.name ?? ''; - document.getElementById('packet-number-info').textContent = this.question?.packet?.number ?? '-'; - document.getElementById('question-number-info').textContent = this.question?.number ?? '-'; - - if (currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP) { - this.room.tossup = question; - } else if (currentQuestionType === QUESTION_TYPE_ENUM.BONUS) { - this.room.bonus = question; - } - } - - endCurrentBonus ({ bonus, lastPartRevealed, pointsPerPart, starred, teamId }) { - super.endCurrentBonus({ bonus, starred }); - if (lastPartRevealed) { - const points = pointsPerPart.reduce((a, b) => a + b, 0); - this.room.teams[teamId].bonusStats[points]++; - upsertPlayerItem(this.room.players[teamId], this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[teamId]); - this.sortPlayerListGroup(); + + const b = document.createElement("b"); + b.textContent = username; + + const span = document.createElement("span"); + span.textContent = givenAnswer; + + let li; + if (document.getElementById("live-buzz")) { + li = document.getElementById("live-buzz"); + li.textContent = ""; + } else { + li = document.createElement("li"); + li.id = "live-buzz"; + document.getElementById("room-history").prepend(li); + } + + li.appendChild(badge); + li.appendChild(document.createTextNode(" ")); + li.appendChild(b); + li.appendChild(document.createTextNode(" ")); + li.appendChild(span); + + if (directive === "accept" || directive === "reject") { + const secondBadge = document.createElement("span"); + secondBadge.className = badge.className; + + if (directive === "accept") { + secondBadge.textContent = "Correct"; + } else if (directive === "reject") { + secondBadge.textContent = "Incorrect"; + } + + li.appendChild(document.createTextNode(" ")); + li.appendChild(secondBadge); + } + + if (directive) { + li.id = ""; + } } - } - failedVotekickPoints ({ userId }) { - if (userId === this.USER_ID) { - window.alert('You can only votekick once you have answered a question correctly!'); + lostBuzzerRace({ username, userId }) { + this.logEventConditionally(username, "lost the buzzer race"); + if (userId === this.USER_ID) { + document.getElementById("answer-input-group").classList.add("d-none"); + } } - } - forceUsername ({ message, username }) { - window.alert(message); - window.localStorage.setItem('multiplayer-username', username); - document.querySelector('#username').value = username; - } + mutePlayer({ targetId, targetUsername, muteStatus }) { + if (muteStatus === "Mute") { + if (!this.room.muteList.includes(targetId)) { + this.room.muteList.push(targetId); + this.logEventConditionally(targetUsername, "was muted"); + } + } else { + if (this.room.muteList.includes(targetId)) { + this.room.muteList = this.room.muteList.filter( + (Id) => Id !== targetId, + ); + this.logEventConditionally(targetUsername, "was unmuted"); + } + } + } + + ownerChange({ newOwner }) { + if (this.room.players[newOwner]) { + this.room.ownerId = newOwner; + this.logEventConditionally( + this.room.players[newOwner].username, + "became the room owner", + ); + } else this.logEventConditionally(newOwner, "became the room owner"); + + Object.keys(this.room.players).forEach((player) => { + upsertPlayerItem( + this.room.players[player], + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[this.room.players[player].teamId], + ); + }); - async giveBonusAnswer ({ currentPartNumber, directive, directedPrompt, givenAnswer, score, userId, username }) { - this.logGiveAnswer({ directive, givenAnswer, questionType: QUESTION_TYPE_ENUM.BONUS, username }); - if (directive === 'prompt' && directedPrompt) { - this.logEventConditionally(username, `was prompted with "${directedPrompt}"`); - } else if (directive === 'prompt') { - this.logEventConditionally(username, 'was prompted'); + document.getElementById("toggle-controlled").disabled = + this.room.public || this.room.ownerId !== this.USER_ID; } - super.giveBonusAnswer({ currentPartNumber, directive, directedPrompt, userId }); - } - async giveTossupAnswer ({ celerity, tossup, perQuestionCelerity, directive, directedPrompt, givenAnswer, score, userId, username }) { - this.logGiveAnswer({ directive, givenAnswer, questionType: QUESTION_TYPE_ENUM.TOSSUP, username }); - if (directive === 'prompt' && directedPrompt) { - this.logEventConditionally(username, `was prompted with "${directedPrompt}"`); - } else if (directive === 'prompt') { - this.logEventConditionally(username, 'was prompted'); - } else { - this.logEventConditionally(username, `${score > 0 ? '' : 'in'}correctly answered for ${score} points`); + pause({ paused, username }) { + this.logEventConditionally( + username, + `${paused ? "" : "un"}paused the game`, + ); + super.pause({ paused }); } - super.giveTossupAnswer({ directive, directedPrompt, givenAnswer, score, userId, username }); - if (directive === 'prompt') { return; } + revealAnswer({ answer, question }) { + super.revealAnswer({ answer, question }); + document.getElementById("next").textContent = "Next"; + document.getElementById("next").disabled = false; + } - document.getElementById('pause').disabled = false; + revealNextAnswer({ answer, currentPartNumber, lastPartRevealed }) { + super.revealNextAnswer({ answer, currentPartNumber, lastPartRevealed }); + if (lastPartRevealed) { + document.getElementById("next").textContent = "Next"; + document.getElementById("next").disabled = false; + } + } - if (directive === 'accept') { - document.getElementById('buzz').disabled = true; - Array.from(document.getElementsByClassName('tuh')).forEach(element => { - element.textContent = parseInt(element.innerHTML) + 1; + setCategories({ + alternateSubcategories, + categories, + subcategories, + percentView, + categoryPercents, + username, + }) { + this.logEventConditionally(username, "updated the categories"); + this.room.categoryManager.import({ + categories, + subcategories, + alternateSubcategories, + percentView, + categoryPercents, }); - this.room.bonusEligibleTeamId = this.room.players[userId].teamId; + if (!document.getElementById("category-modal")) { + return; + } + super.setCategories(); } - if (directive === 'reject') { - document.getElementById('buzz').disabled = !document.getElementById('toggle-rebuzz').checked && userId === this.USER_ID; + setDifficulties({ difficulties, username = undefined }) { + this.logEventConditionally( + username, + difficulties.length > 0 + ? `set the difficulties to ${difficulties}` + : "cleared the difficulties", + ); + + if (!document.getElementById("difficulties")) { + this.room.difficulties = difficulties; + return; + } + + Array.from(document.getElementById("difficulties").children).forEach( + (li) => { + const input = li.querySelector("input"); + if (difficulties.includes(parseInt(input.value))) { + input.checked = true; + li.classList.add("active"); + } else { + input.checked = false; + li.classList.remove("active"); + } + }, + ); } - if (score > 10) { - this.room.players[userId].powers++; - } else if (score === 10) { - this.room.players[userId].tens++; - } else if (score < 0) { - this.room.players[userId].negs++; + setMinYear({ minYear, username }) { + const maxYear = parseInt( + document.getElementById("max-year-label").textContent, + ); + this.logEventConditionally( + username, + `changed the year range to ${minYear}-${maxYear}`, + ); + super.setMinYear({ minYear }); } - this.room.players[userId].points += score; - this.room.players[userId].tuh++; - this.room.players[userId].celerity = celerity; + setMaxYear({ maxYear, username }) { + const minYear = parseInt( + document.getElementById("min-year-label").textContent, + ); + this.logEventConditionally( + username, + `changed the year range to ${minYear}-${maxYear}`, + ); + super.setMaxYear({ maxYear }); + } - upsertPlayerItem(this.room.players[userId], this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[this.room.players[userId].teamId]); - this.sortPlayerListGroup(); + setMode({ mode, username }) { + this.logEventConditionally(username, "changed the mode to " + mode); + this.room.mode = mode; + super.setMode({ mode }); + } - if (userId === this.USER_ID) { - questionStats.recordTossup({ - _id: tossup._id, - celerity: perQuestionCelerity, - isCorrect: score > 0, - multiplayer: true, - pointValue: score - }); + setPacketNumbers({ username, packetNumbers }) { + super.setPacketNumbers({ packetNumbers }); + this.logEventConditionally( + username, + packetNumbers.length > 0 + ? `changed packet numbers to ${arrayToRange(packetNumbers)}` + : "cleared packet numbers", + ); } - } - handleError ({ message }) { - this.socket.close(3000); - window.alert(message); - window.location.href = '/multiplayer'; - } + setReadingSpeed({ username, readingSpeed }) { + super.setReadingSpeed({ readingSpeed }); + this.logEventConditionally( + username, + `changed the reading speed to ${readingSpeed}`, + ); + } + + setStrictness({ strictness, username }) { + this.logEventConditionally( + username, + `changed the strictness to ${strictness}`, + ); + super.setStrictness({ strictness }); + } - join ({ isNew, team, user, userId, username }) { - this.logEventConditionally(username, 'joined the game'); - if (userId === this.USER_ID) { return; } - this.room.players[userId] = user; - this.room.teams[user.teamId] = team; + setSetName({ username, setName, setLength }) { + this.logEventConditionally( + username, + setName.length > 0 + ? `changed set name to ${setName}` + : "cleared set name", + ); + this.room.setLength = setLength; + super.setSetName({ setName, setLength }); + } - if (isNew) { - user.celerity = user.celerity.correct.average; - upsertPlayerItem(user, this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[user.teamId]); + setUsername({ oldUsername, newUsername, userId }) { + this.logEventConditionally( + oldUsername, + `changed their username to ${newUsername}`, + ); + document.getElementById("username-" + userId).textContent = newUsername; + this.room.players[userId].username = newUsername; this.sortPlayerListGroup(); - } else { - document.getElementById(`list-group-${userId}`).classList.remove('offline'); - document.getElementById('points-' + userId).classList.add('bg-success'); - document.getElementById('points-' + userId).classList.remove('bg-secondary'); - document.getElementById('username-' + userId).textContent = username; - } - } - - leave ({ userId, username }) { - this.logEventConditionally(username, 'left the game'); - this.room.players[userId].online = false; - document.getElementById(`list-group-${userId}`).classList.add('offline'); - document.getElementById(`points-${userId}`).classList.remove('bg-success'); - document.getElementById(`points-${userId}`).classList.add('bg-secondary'); - } - - /** - * Log the event, but only if `username !== undefined`. - * If username is undefined, do nothing, regardless of the value of message. - * @param {string | undefined} username - * @param {string | undefined} message - */ - logEventConditionally (username, message) { - if (username === undefined) { return; } - - const span1 = document.createElement('span'); - span1.textContent = username; - - const span2 = document.createElement('span'); - span2.textContent = message; - - const i = document.createElement('i'); - i.appendChild(span1); - i.appendChild(document.createTextNode(' ')); - i.appendChild(span2); - - const li = document.createElement('li'); - li.appendChild(i); - - document.getElementById('room-history').prepend(li); - } - - logGiveAnswer ({ directive = null, givenAnswer, questionType, username }) { - const badge = document.createElement('span'); - badge.textContent = questionType === QUESTION_TYPE_ENUM.TOSSUP ? 'Buzz' : 'Answer'; - switch (directive) { - case 'accept': - badge.className = 'badge text-dark bg-success'; - break; - case 'reject': - badge.className = 'badge text-light bg-danger'; - break; - case 'prompt': - badge.className = 'badge text-dark bg-warning'; - break; - default: - badge.className = 'badge text-light bg-primary'; - break; - } - - const b = document.createElement('b'); - b.textContent = username; - - const span = document.createElement('span'); - span.textContent = givenAnswer; - - let li; - if (document.getElementById('live-buzz')) { - li = document.getElementById('live-buzz'); - li.textContent = ''; - } else { - li = document.createElement('li'); - li.id = 'live-buzz'; - document.getElementById('room-history').prepend(li); - } - - li.appendChild(badge); - li.appendChild(document.createTextNode(' ')); - li.appendChild(b); - li.appendChild(document.createTextNode(' ')); - li.appendChild(span); - - if (directive === 'accept' || directive === 'reject') { - const secondBadge = document.createElement('span'); - secondBadge.className = badge.className; - - if (directive === 'accept') { - secondBadge.textContent = 'Correct'; - } else if (directive === 'reject') { - secondBadge.textContent = 'Incorrect'; - } - - li.appendChild(document.createTextNode(' ')); - li.appendChild(secondBadge); - } - - if (directive) { li.id = ''; } - } - - lostBuzzerRace ({ username, userId }) { - this.logEventConditionally(username, 'lost the buzzer race'); - if (userId === this.USER_ID) { document.getElementById('answer-input-group').classList.add('d-none'); } - } - - mutePlayer ({ targetId, targetUsername, muteStatus }) { - if (muteStatus === 'Mute') { - if (!this.room.muteList.includes(targetId)) { - this.room.muteList.push(targetId); - this.logEventConditionally(targetUsername, 'was muted'); - } - } else { - if (this.room.muteList.includes(targetId)) { - this.room.muteList = this.room.muteList.filter(Id => Id !== targetId); - this.logEventConditionally(targetUsername, 'was unmuted'); - } - } - } - - ownerChange ({ newOwner }) { - if (this.room.players[newOwner]) { - this.room.ownerId = newOwner; - this.logEventConditionally(this.room.players[newOwner].username, 'became the room owner'); - } else this.logEventConditionally(newOwner, 'became the room owner'); - - Object.keys(this.room.players).forEach((player) => { - upsertPlayerItem(this.room.players[player], this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[this.room.players[player].teamId]); - }); - - document.getElementById('toggle-controlled').disabled = this.room.public || (this.room.ownerId !== this.USER_ID); - } - - pause ({ paused, username }) { - this.logEventConditionally(username, `${paused ? '' : 'un'}paused the game`); - super.pause({ paused }); - } - - revealAnswer ({ answer, question }) { - super.revealAnswer({ answer, question }); - document.getElementById('next').textContent = 'Next'; - document.getElementById('next').disabled = false; - } - - revealNextAnswer ({ answer, currentPartNumber, lastPartRevealed }) { - super.revealNextAnswer({ answer, currentPartNumber, lastPartRevealed }); - if (lastPartRevealed) { - document.getElementById('next').textContent = 'Next'; - document.getElementById('next').disabled = false; - } - } - - setCategories ({ alternateSubcategories, categories, subcategories, percentView, categoryPercents, username }) { - this.logEventConditionally(username, 'updated the categories'); - this.room.categoryManager.import({ categories, subcategories, alternateSubcategories, percentView, categoryPercents }); - if (!document.getElementById('category-modal')) { return; } - super.setCategories(); - } - setDifficulties ({ difficulties, username = undefined }) { - this.logEventConditionally(username, difficulties.length > 0 ? `set the difficulties to ${difficulties}` : 'cleared the difficulties'); + if (userId === this.USER_ID) { + this.room.username = newUsername; + window.localStorage.setItem("multiplayer-username", this.room.username); + document.getElementById("username").value = this.room.username; + } + upsertPlayerItem( + this.room.players[userId], + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[this.room.players[userId].teamId], + ); + } + + sortPlayerListGroup(descending = true) { + const listGroup = document.getElementById("player-list-group"); + const items = Array.from(listGroup.children); + const offset = "list-group-".length; + items + .sort((a, b) => { + const aPoints = parseInt( + document.getElementById("points-" + a.id.substring(offset)) + .textContent, + ); + const bPoints = parseInt( + document.getElementById("points-" + b.id.substring(offset)) + .textContent, + ); + // if points are equal, sort alphabetically by username + if (aPoints === bPoints) { + const aUsername = document.getElementById( + "username-" + a.id.substring(offset), + ).textContent; + const bUsername = document.getElementById( + "username-" + b.id.substring(offset), + ).textContent; + return descending + ? aUsername.localeCompare(bUsername) + : bUsername.localeCompare(aUsername); + } + return descending ? bPoints - aPoints : aPoints - bPoints; + }) + .forEach((item) => { + listGroup.appendChild(item); + }); + } + + startNextQuestion({ packetLength, question }) { + document.getElementById("next").classList.add("btn-primary"); + document.getElementById("next").classList.remove("btn-success"); + document.getElementById("next").textContent = "Skip"; + super.startNextQuestion({ packetLength, question }); + } + + startNextBonus({ bonus, packetLength, username }) { + this.logEventConditionally(username, "started the next bonus"); + super.startNextBonus({ bonus, packetLength }); + } + + startNextTossup({ tossup, packetLength, username }) { + this.logEventConditionally(username, "started the next tossup"); + super.startNextTossup({ tossup, packetLength }); + } + + toggleControlled({ controlled, username }) { + this.logEventConditionally( + username, + `${controlled ? "enabled" : "disabled"} controlled mode`, + ); + + document.getElementById("toggle-controlled").checked = controlled; + document + .getElementById("controlled-room-warning") + .classList.toggle("d-none", !controlled); + document.getElementById("toggle-public").disabled = controlled; + + controlled = controlled && this.USER_ID !== this.room.ownerId; + document.getElementById("toggle-enable-bonuses").disabled = controlled; + document.getElementById("toggle-lock").disabled = controlled; + document.getElementById("toggle-login-required").disabled = controlled; + document.getElementById("toggle-timer").disabled = controlled; + document.getElementById("toggle-powermark-only").disabled = controlled; + document.getElementById("toggle-rebuzz").disabled = controlled; + document.getElementById("toggle-skip").disabled = controlled; + document.getElementById("toggle-standard-only").disabled = controlled; + document.getElementById("category-select-button").disabled = controlled; + document.getElementById("reading-speed").disabled = controlled; + document.getElementById("set-mode").disabled = controlled; + document.getElementById("set-strictness").disabled = controlled; + } + + toggleEnableBonuses({ enableBonuses, username }) { + this.logEventConditionally( + username, + `${enableBonuses ? "enabled" : "disabled"} bonuses`, + ); + super.toggleEnableBonuses({ enableBonuses }); + } + + toggleLock({ lock, username }) { + this.logEventConditionally( + username, + `${lock ? "locked" : "unlocked"} the room`, + ); + document.getElementById("toggle-lock").checked = lock; + } + + toggleLoginRequired({ loginRequired, username }) { + this.logEventConditionally( + username, + `${loginRequired ? "enabled" : "disabled"} requiring players to be logged in`, + ); + document.getElementById("toggle-login-required").checked = loginRequired; + } + + togglePowermarkOnly({ powermarkOnly, username }) { + this.logEventConditionally( + username, + `${powermarkOnly ? "enabled" : "disabled"} powermark only`, + ); + super.togglePowermarkOnly({ powermarkOnly }); + } + + toggleRebuzz({ rebuzz, username }) { + this.logEventConditionally( + username, + `${rebuzz ? "enabled" : "disabled"} multiple buzzes (effective next question)`, + ); + super.toggleRebuzz({ rebuzz }); + } + + toggleSkip({ skip, username }) { + this.logEventConditionally( + username, + `${skip ? "enabled" : "disabled"} skipping`, + ); + super.toggleSkip({ skip }); + } + + toggleStandardOnly({ standardOnly, username }) { + this.logEventConditionally( + username, + `${standardOnly ? "enabled" : "disabled"} standard format only`, + ); + super.toggleStandardOnly({ standardOnly }); + } + + toggleTimer({ timer, username }) { + this.logEventConditionally( + username, + `${timer ? "enabled" : "disabled"} the timer`, + ); + super.toggleTimer({ timer }); + } - if (!document.getElementById('difficulties')) { - this.room.difficulties = difficulties; - return; + toggleThreePartBonuses({ threePartBonuses, username }) { + this.logEventConditionally( + username, + `${threePartBonuses ? "enabled" : "disabled"} three-part bonuses only`, + ); + super.toggleThreePartBonuses({ threePartBonuses }); } - Array.from(document.getElementById('difficulties').children).forEach(li => { - const input = li.querySelector('input'); - if (difficulties.includes(parseInt(input.value))) { - input.checked = true; - li.classList.add('active'); + togglePublic({ public: isPublic, username }) { + this.logEventConditionally( + username, + `made the room ${isPublic ? "public" : "private"}`, + ); + document.getElementById("chat").disabled = isPublic; + document.getElementById("toggle-controlled").disabled = + isPublic || this.room.ownerId !== this.USER_ID; + document.getElementById("toggle-lock").disabled = isPublic; + document.getElementById("toggle-login-required").disabled = isPublic; + document.getElementById("toggle-public").checked = isPublic; + document.getElementById("toggle-timer").disabled = isPublic; + this.room.public = isPublic; + if (isPublic) { + document.getElementById("toggle-lock").checked = false; + document.getElementById("toggle-login-required").checked = false; + this.toggleTimer({ timer: true }); + } + Object.keys(this.room.players).forEach((player) => { + upsertPlayerItem( + this.room.players[player], + this.USER_ID, + this.room.ownerId, + this.socket, + this.room.public, + this.room.teams[this.room.players[player].teamId], + ); + }); + } + + stopOnPower({ stopOnPower: stopOnPower, username }) { + this.logEventConditionally( + username, + `${stopOnPower ? "enabled" : "disabled"} stop on power`, + ); + } + + vkInit({ targetUsername, threshold }) { + this.logEventConditionally( + `A votekick has been started against user ${targetUsername} and needs ${threshold} votes to succeed.`, + ); + } + + vkHandle({ targetUsername, targetId }) { + if (this.USER_ID === targetId) { + window.alert("You were vote kicked from this room by others."); + setTimeout(() => { + window.location.replace("../"); + }, 100); } else { - input.checked = false; - li.classList.remove('active'); - } - }); - } - - setMinYear ({ minYear, username }) { - const maxYear = parseInt(document.getElementById('max-year-label').textContent); - this.logEventConditionally(username, `changed the year range to ${minYear}-${maxYear}`); - super.setMinYear({ minYear }); - } - - setMaxYear ({ maxYear, username }) { - const minYear = parseInt(document.getElementById('min-year-label').textContent); - this.logEventConditionally(username, `changed the year range to ${minYear}-${maxYear}`); - super.setMaxYear({ maxYear }); - } - - setMode ({ mode, username }) { - this.logEventConditionally(username, 'changed the mode to ' + mode); - this.room.mode = mode; - super.setMode({ mode }); - } - - setPacketNumbers ({ username, packetNumbers }) { - super.setPacketNumbers({ packetNumbers }); - this.logEventConditionally(username, packetNumbers.length > 0 ? `changed packet numbers to ${arrayToRange(packetNumbers)}` : 'cleared packet numbers'); - } - - setReadingSpeed ({ username, readingSpeed }) { - super.setReadingSpeed({ readingSpeed }); - this.logEventConditionally(username, `changed the reading speed to ${readingSpeed}`); - } - - setStrictness ({ strictness, username }) { - this.logEventConditionally(username, `changed the strictness to ${strictness}`); - super.setStrictness({ strictness }); - } - - setSetName ({ username, setName, setLength }) { - this.logEventConditionally(username, setName.length > 0 ? `changed set name to ${setName}` : 'cleared set name'); - this.room.setLength = setLength; - super.setSetName({ setName, setLength }); - } - - setUsername ({ oldUsername, newUsername, userId }) { - this.logEventConditionally(oldUsername, `changed their username to ${newUsername}`); - document.getElementById('username-' + userId).textContent = newUsername; - this.room.players[userId].username = newUsername; - this.sortPlayerListGroup(); - - if (userId === this.USER_ID) { - this.room.username = newUsername; - window.localStorage.setItem('multiplayer-username', this.room.username); - document.getElementById('username').value = this.room.username; - } - upsertPlayerItem(this.room.players[userId], this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[this.room.players[userId].teamId]); - } - - sortPlayerListGroup (descending = true) { - const listGroup = document.getElementById('player-list-group'); - const items = Array.from(listGroup.children); - const offset = 'list-group-'.length; - items.sort((a, b) => { - const aPoints = parseInt(document.getElementById('points-' + a.id.substring(offset)).textContent); - const bPoints = parseInt(document.getElementById('points-' + b.id.substring(offset)).textContent); - // if points are equal, sort alphabetically by username - if (aPoints === bPoints) { - const aUsername = document.getElementById('username-' + a.id.substring(offset)).textContent; - const bUsername = document.getElementById('username-' + b.id.substring(offset)).textContent; - return descending ? aUsername.localeCompare(bUsername) : bUsername.localeCompare(aUsername); - } - return descending ? bPoints - aPoints : aPoints - bPoints; - }).forEach(item => { - listGroup.appendChild(item); - }); - } - - startNextQuestion ({ packetLength, question }) { - document.getElementById('next').classList.add('btn-primary'); - document.getElementById('next').classList.remove('btn-success'); - document.getElementById('next').textContent = 'Skip'; - super.startNextQuestion({ packetLength, question }); - } - - startNextBonus ({ bonus, packetLength, username }) { - this.logEventConditionally(username, 'started the next bonus'); - super.startNextBonus({ bonus, packetLength }); - } - - startNextTossup ({ tossup, packetLength, username }) { - this.logEventConditionally(username, 'started the next tossup'); - super.startNextTossup({ tossup, packetLength }); - } - - toggleControlled ({ controlled, username }) { - this.logEventConditionally(username, `${controlled ? 'enabled' : 'disabled'} controlled mode`); - - document.getElementById('toggle-controlled').checked = controlled; - document.getElementById('controlled-room-warning').classList.toggle('d-none', !controlled); - document.getElementById('toggle-public').disabled = controlled; - - controlled = controlled && (this.USER_ID !== this.room.ownerId); - document.getElementById('toggle-enable-bonuses').disabled = controlled; - document.getElementById('toggle-lock').disabled = controlled; - document.getElementById('toggle-login-required').disabled = controlled; - document.getElementById('toggle-timer').disabled = controlled; - document.getElementById('toggle-powermark-only').disabled = controlled; - document.getElementById('toggle-rebuzz').disabled = controlled; - document.getElementById('toggle-skip').disabled = controlled; - document.getElementById('toggle-standard-only').disabled = controlled; - document.getElementById('category-select-button').disabled = controlled; - document.getElementById('reading-speed').disabled = controlled; - document.getElementById('set-mode').disabled = controlled; - document.getElementById('set-strictness').disabled = controlled; - } - - toggleEnableBonuses ({ enableBonuses, username }) { - this.logEventConditionally(username, `${enableBonuses ? 'enabled' : 'disabled'} bonuses`); - super.toggleEnableBonuses({ enableBonuses }); - } - - toggleLock ({ lock, username }) { - this.logEventConditionally(username, `${lock ? 'locked' : 'unlocked'} the room`); - document.getElementById('toggle-lock').checked = lock; - } - - toggleLoginRequired ({ loginRequired, username }) { - this.logEventConditionally(username, `${loginRequired ? 'enabled' : 'disabled'} requiring players to be logged in`); - document.getElementById('toggle-login-required').checked = loginRequired; - } - - togglePowermarkOnly ({ powermarkOnly, username }) { - this.logEventConditionally(username, `${powermarkOnly ? 'enabled' : 'disabled'} powermark only`); - super.togglePowermarkOnly({ powermarkOnly }); - } - - toggleRebuzz ({ rebuzz, username }) { - this.logEventConditionally(username, `${rebuzz ? 'enabled' : 'disabled'} multiple buzzes (effective next question)`); - super.toggleRebuzz({ rebuzz }); - } - - toggleSkip ({ skip, username }) { - this.logEventConditionally(username, `${skip ? 'enabled' : 'disabled'} skipping`); - super.toggleSkip({ skip }); - } - - toggleStandardOnly ({ standardOnly, username }) { - this.logEventConditionally(username, `${standardOnly ? 'enabled' : 'disabled'} standard format only`); - super.toggleStandardOnly({ standardOnly }); - } - - toggleTimer ({ timer, username }) { - this.logEventConditionally(username, `${timer ? 'enabled' : 'disabled'} the timer`); - super.toggleTimer({ timer }); - } - - toggleThreePartBonuses ({ threePartBonuses, username }) { - this.logEventConditionally(username, `${threePartBonuses ? 'enabled' : 'disabled'} three-part bonuses only`); - super.toggleThreePartBonuses({ threePartBonuses }); - } - - togglePublic ({ public: isPublic, username }) { - this.logEventConditionally(username, `made the room ${isPublic ? 'public' : 'private'}`); - document.getElementById('chat').disabled = isPublic; - document.getElementById('toggle-controlled').disabled = isPublic || (this.room.ownerId !== this.USER_ID); - document.getElementById('toggle-lock').disabled = isPublic; - document.getElementById('toggle-login-required').disabled = isPublic; - document.getElementById('toggle-public').checked = isPublic; - document.getElementById('toggle-timer').disabled = isPublic; - this.room.public = isPublic; - if (isPublic) { - document.getElementById('toggle-lock').checked = false; - document.getElementById('toggle-login-required').checked = false; - this.toggleTimer({ timer: true }); - } - Object.keys(this.room.players).forEach((player) => { - upsertPlayerItem(this.room.players[player], this.USER_ID, this.room.ownerId, this.socket, this.room.public, this.room.teams[this.room.players[player].teamId]); - }); - } - - vkInit ({ targetUsername, threshold }) { - this.logEventConditionally(`A votekick has been started against user ${targetUsername} and needs ${threshold} votes to succeed.`); - } - - vkHandle ({ targetUsername, targetId }) { - if (this.USER_ID === targetId) { - window.alert('You were vote kicked from this room by others.'); - setTimeout(() => { - window.location.replace('../'); - }, 100); - } else { - this.logEventConditionally(targetUsername + ' has been vote kicked from this room.'); + this.logEventConditionally( + targetUsername + " has been vote kicked from this room.", + ); + } } - } -}; + }; const MultiplayerTossupBonusClient = MultiplayerClientMixin(TossupBonusClient); export default MultiplayerTossupBonusClient; diff --git a/client/play/mp/room.html b/client/play/mp/room.html index 2a8ecca22..91dd4f9f2 100644 --- a/client/play/mp/room.html +++ b/client/play/mp/room.html @@ -1,327 +1,794 @@ + + QB Reader + + - - QB Reader - - + + + - - - + + + + - - - - - - -
- + -
-
- -
-
- -
-
-
-
- 10.0 -
-
-
- - -
-
    -
    -
    - - - - -
    - - -
    -
    - Set Name - - -
    -
    - Packet # - -
    -
    +
    +
    + -
    -
    - Year range: 2010 - 2025 -
    - - +
    +
    + -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - +
    +
    +
    +
    + 10.0
    -
    - - +
    +
    + +
    -
    - - +
      +
      +
      + + + +
      -
      - - + + +
      +
      + Set Name + + +
      +
      + Packet # + +
      +
      -
      - - +
      +
      + Year range: 2010 - + 2025 +
      + + +
      -
      - - - - -
      - View more settings here. +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + + + +
      + View more settings here. +
      +

      + Chat is disabled in public rooms. + This is a permanent room. Some settings have + been restricted. + + This is a controlled room. Some settings can + only be edited by the creator. + +

      -

      - Chat is disabled in public rooms. - This is a permanent room. Some settings have been restricted. - - This is a controlled room. Some settings can only be edited by the creator. - -

      -
      -
      -
      - -
      - - -
      -
      -
      -
      - - +
      + + +
      + + +
      + +
      +
      + + +
      +
      +
      + + + + Packet + - Question + - of + - + +
      - -
      - - - - Packet - - Question - - of - - - +
      +

      +
        -
        -

        -
          -
          -
          -
          -
          - - - - - - +
          +
          +
          + + + + + + +
          -
          -
          +
          -