diff --git a/client/multiplayer/room.jsx b/client/multiplayer/room.jsx
index a53a1c07..a0ff4908 100644
--- a/client/multiplayer/room.jsx
+++ b/client/multiplayer/room.jsx
@@ -5,9 +5,9 @@ import audio from '../audio/index.js';
import CategoryManager from '../../quizbowl/category-manager.js';
import { getDropdownValues } from '../scripts/utilities/dropdown-checklist.js';
import { arrayToRange, createTossupCard, rangeToArray } from '../scripts/utilities/index.js';
-import { escapeHTML } from '../scripts/utilities/strings.js';
import CategoryModal from '../scripts/components/CategoryModal.min.js';
import DifficultyDropdown from '../scripts/components/DifficultyDropdown.min.js';
+import upsertPlayerItem from '../scripts/upsertPlayerItem.js';
const categoryManager = new CategoryManager();
let oldCategories = JSON.stringify(categoryManager.export());
@@ -142,7 +142,7 @@ function clearStats ({ userId }) {
for (const field of ['celerity', 'negs', 'points', 'powers', 'tens', 'tuh', 'zeroes']) {
players[userId][field] = 0;
}
- upsertPlayerItem(players[userId]);
+ upsertPlayerItem(players[userId], USER_ID);
sortPlayerListGroup();
}
@@ -168,7 +168,7 @@ function connectionAcknowledged ({
Object.keys(messagePlayers).forEach(userId => {
messagePlayers[userId].celerity = messagePlayers[userId].celerity.correct.average;
players[userId] = messagePlayers[userId];
- upsertPlayerItem(players[userId]);
+ upsertPlayerItem(players[userId], USER_ID);
});
sortPlayerListGroup();
@@ -328,7 +328,7 @@ async function giveAnswer ({ celerity, directive, directedPrompt, givenAnswer, p
players[userId].tuh++;
players[userId].celerity = celerity;
- upsertPlayerItem(players[userId]);
+ upsertPlayerItem(players[userId], USER_ID);
sortPlayerListGroup();
}
@@ -359,7 +359,7 @@ function join ({ isNew, user, userId, username }) {
if (isNew) {
user.celerity = user.celerity.correct.average;
- upsertPlayerItem(user);
+ upsertPlayerItem(user, USER_ID);
sortPlayerListGroup();
players[userId] = user;
} else {
@@ -688,63 +688,6 @@ function updateTimerDisplay (time) {
document.querySelector('.timer .fraction').innerText = '.' + tenths;
}
-function upsertPlayerItem (player) {
- const { userId, username, powers = 0, tens = 0, negs = 0, tuh = 0, points = 0, celerity = 0, online } = player;
-
- if (document.getElementById('list-group-' + userId)) {
- document.getElementById('list-group-' + userId).remove();
- }
-
- const playerItem = document.createElement('a');
- playerItem.className = `list-group-item ${userId === USER_ID ? 'user-score' : ''} clickable`;
- playerItem.id = `list-group-${userId}`;
- playerItem.innerHTML = `
-
- ${escapeHTML(username)}
- ${points}
-
- `;
-
- playerItem.setAttribute('data-bs-container', 'body');
- playerItem.setAttribute('data-bs-custom-class', 'custom-popover');
- playerItem.setAttribute('data-bs-html', 'true');
- playerItem.setAttribute('data-bs-placement', 'left');
- playerItem.setAttribute('data-bs-toggle', 'popover');
- playerItem.setAttribute('data-bs-trigger', 'focus');
- playerItem.setAttribute('tabindex', '0');
-
- playerItem.setAttribute('data-bs-title', username);
- playerItem.setAttribute('data-bs-content', `
-
- -
- Powers
- ${powers}
-
- -
- Tens
- ${tens}
-
- -
- Negs
- ${negs}
-
- -
- TUH
- ${tuh}
-
- -
- Celerity
- ${celerity.toFixed(3)}
-
-
- `);
-
- document.getElementById('player-list-group').appendChild(playerItem);
- // bootstrap requires "new" to be called on each popover
- // eslint-disable-next-line no-new
- new bootstrap.Popover(playerItem);
-}
-
function setYearRange ({ minYear, maxYear, username }) {
if (username) { logEvent(username, `changed the year range to ${minYear}-${maxYear}`); }
diff --git a/client/multiplayer/room.min.js b/client/multiplayer/room.min.js
index b9474497..2c7e626b 100644
--- a/client/multiplayer/room.min.js
+++ b/client/multiplayer/room.min.js
@@ -1,35 +1,5 @@
-import account from"../scripts/accounts.js";import questionStats from"../scripts/auth/question-stats.js";import api from"../scripts/api/index.js";import audio from"../audio/index.js";import CategoryManager from"../../quizbowl/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import{arrayToRange,createTossupCard,rangeToArray}from"../scripts/utilities/index.js";import{escapeHTML}from"../scripts/utilities/strings.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";const categoryManager=new CategoryManager;let oldCategories=JSON.stringify(categoryManager.export()),startingDifficulties=[],maxPacketNumber=24;/**
+import account from"../scripts/accounts.js";import questionStats from"../scripts/auth/question-stats.js";import api from"../scripts/api/index.js";import audio from"../audio/index.js";import CategoryManager from"../../quizbowl/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import{arrayToRange,createTossupCard,rangeToArray}from"../scripts/utilities/index.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";import upsertPlayerItem from"../scripts/upsertPlayerItem.js";const categoryManager=new CategoryManager;let oldCategories=JSON.stringify(categoryManager.export()),startingDifficulties=[],maxPacketNumber=24;/**
* userId to player object
*/const players={},ROOM_NAME=decodeURIComponent(window.location.pathname.substring(13));let tossup={},USER_ID=window.localStorage.getItem("USER_ID")||"unknown",username=window.localStorage.getItem("multiplayer-username")||api.getRandomName();const socket=new window.WebSocket(window.location.href.replace("http","ws")+(window.location.href.endsWith("?private=true")?"&":"?")+new URLSearchParams({roomName:ROOM_NAME,userId:USER_ID,username}).toString()),PING_INTERVAL_ID=setInterval(()=>socket.send(JSON.stringify({type:"ping"})),45e3);// Ping server every 45 seconds to prevent socket disconnection
-socket.onclose=function(a){const{code:b}=a;3e3!==b&&window.alert("Disconnected from server"),clearInterval(PING_INTERVAL_ID)},socket.onmessage=function(a){const b=JSON.parse(a.data);switch(b.type){case"buzz":return buzz(b);case"force-username":return forceUsername(b);case"chat":return chat(b,!1);case"chat-live-update":return chat(b,!0);case"clear-stats":return clearStats(b);case"connection-acknowledged":return connectionAcknowledged(b);case"connection-acknowledged-query":return connectionAcknowledgedQuery(b);case"connection-acknowledged-tossup":return connectionAcknowledgedTossup(b);case"end-of-set":return endOfSet(b);case"error":return handleError(b);case"give-answer":return giveAnswer(b);case"give-answer-live-update":return logGiveAnswer(b,!0);case"join":return join(b);case"leave":return leave(b);case"lost-buzzer-race":return lostBuzzerRace(b);case"next":return next(b);case"no-questions-found":return noQuestionsFound(b);case"pause":return pause(b);case"reveal-answer":return revealAnswer(b);case"set-categories":return setCategories(b);case"set-difficulties":return setDifficulties(b);case"set-reading-speed":return setReadingSpeed(b);case"set-packet-numbers":return setPacketNumbers(b);case"set-strictness":return setStrictness(b);case"set-set-name":return setSetName(b);case"set-username":return setUsername(b);case"set-year-range":return setYearRange(b);case"skip":return next(b);case"start":return next(b);case"timer-update":return updateTimerDisplay(b.timeRemaining);case"toggle-lock":return toggleLock(b);case"toggle-login-required":return toggleLoginRequired(b);case"toggle-powermark-only":return togglePowermarkOnly(b);case"toggle-public":return togglePublic(b);case"toggle-rebuzz":return toggleRebuzz(b);case"toggle-select-by-set-name":return toggleSelectBySetName(b);case"toggle-skip":return toggleSkip(b);case"toggle-standard-only":return toggleStandardOnly(b);case"toggle-timer":return toggleTimer(b);case"update-question":return updateQuestion(b)}};function buzz({userId:a,username:b}){logEvent(b,"buzzed"),document.getElementById("buzz").disabled=!0,document.getElementById("pause").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("skip").disabled=!0,a===USER_ID&&(document.getElementById("answer-input-group").classList.remove("d-none"),document.getElementById("answer-input").focus())}function chat({message:a,userId:c,username:d},e=!1){if(!e&&""===a)return void document.getElementById("live-chat-"+c).parentElement.remove();if(!e&&a)return document.getElementById("live-chat-"+c).className="",void(document.getElementById("live-chat-"+c).id="");if(document.getElementById("live-chat-"+c))return void(document.getElementById("live-chat-"+c).textContent=a);const f=document.createElement("b");f.textContent=d;const b=document.createElement("span");b.classList.add("text-muted"),b.id="live-chat-"+c,b.textContent=a;const g=document.createElement("li");g.appendChild(f),g.appendChild(document.createTextNode(" ")),g.appendChild(b),document.getElementById("room-history").prepend(g)}function clearStats({userId:a}){for(const b of["celerity","negs","points","powers","tens","tuh","zeroes"])players[a][b]=0;upsertPlayerItem(players[a]),sortPlayerListGroup()}function connectionAcknowledged({buzzedIn:a,canBuzz:b,isPermanent:c,players:d,questionProgress:e,settings:f,userId:g}){document.getElementById("buzz").disabled=!b,c&&(document.getElementById("category-select-button").disabled=!0,document.getElementById("strictness").disabled=!0,document.getElementById("toggle-public").disabled=!0,document.getElementById("toggle-select-by-set-name").disabled=!0,document.getElementById("private-chat-warning").innerHTML="This is a permanent room. Some settings have been restricted."),Object.keys(d).forEach(a=>{d[a].celerity=d[a].celerity.correct.average,players[a]=d[a],upsertPlayerItem(players[a])}),sortPlayerListGroup();0===e?(document.getElementById("next").textContent="Start",document.getElementById("next").classList.remove("btn-primary"),document.getElementById("next").classList.add("btn-success")):1===e?(showSkipButton(),document.getElementById("settings").classList.add("d-none"),a?(document.getElementById("buzz").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("pause").disabled=!0):(document.getElementById("buzz").disabled=!1,document.getElementById("pause").disabled=!1)):2===e?(showNextButton(),document.getElementById("settings").classList.add("d-none")):void 0;document.getElementById("toggle-lock").checked=f.lock,document.getElementById("toggle-login-required").checked=f.loginRequired,document.getElementById("chat").disabled=f.public,document.getElementById("toggle-lock").disabled=f.public,document.getElementById("toggle-login-required").disabled=f.public,document.getElementById("toggle-timer").disabled=f.public,document.getElementById("toggle-public").checked=f.public,document.getElementById("reading-speed").value=f.readingSpeed,document.getElementById("reading-speed-display").textContent=f.readingSpeed,document.getElementById("strictness").value=f.strictness,document.getElementById("strictness-display").textContent=f.strictness,document.getElementById("toggle-rebuzz").checked=f.rebuzz,document.getElementById("toggle-skip").checked=f.skip,document.getElementById("timer").classList.toggle("d-none",!f.timer),document.getElementById("toggle-timer").checked=f.timer,USER_ID=g,window.localStorage.setItem("USER_ID",USER_ID)}async function connectionAcknowledgedQuery({difficulties:k=[],minYear:a,maxYear:b,packetNumbers:l=[],powermarkOnly:c,selectBySetName:d,setName:m="",standardOnly:e,alternateSubcategories:f,categories:g,subcategories:h,percentView:i,categoryPercents:j}){setDifficulties({difficulties:k}),$("#slider").slider("values",0,a),$("#slider").slider("values",1,b),document.getElementById("year-range-a").textContent=a,document.getElementById("year-range-b").textContent=b,document.getElementById("packet-number").value=arrayToRange(l),document.getElementById("toggle-powermark-only").checked=c,document.getElementById("difficulty-settings").classList.toggle("d-none",d),document.getElementById("set-settings").classList.toggle("d-none",!d),document.getElementById("toggle-select-by-set-name").checked=d,document.getElementById("toggle-powermark-only").disabled=d,document.getElementById("toggle-standard-only").disabled=d,document.getElementById("set-name").value=m,maxPacketNumber=await api.getNumPackets(m),""!==m&&0===maxPacketNumber&&document.getElementById("set-name").classList.add("is-invalid"),document.getElementById("toggle-standard-only").checked=e,categoryManager.import({categories:g,subcategories:h,alternateSubcategories:f,percentView:i,categoryPercents:j}),categoryManager.loadCategoryModal()}function connectionAcknowledgedTossup({tossup:a}){tossup=a,document.getElementById("set-name-info").textContent=tossup?.set?.name??"",document.getElementById("packet-number-info").textContent=tossup?.packet?.number??"-",document.getElementById("question-number-info").textContent=tossup?.number??"-"}function endOfSet(){window.alert("You have reached the end of the set")}function forceUsername({message:a,username:b}){window.alert(a),window.localStorage.setItem("multiplayer-username",b),document.querySelector("#username").value=b}async function giveAnswer({celerity:a,directive:b,directedPrompt:c,givenAnswer:d,perQuestionCelerity:e,score:f,tossup:g,userId:h,username:i}){document.getElementById("answer-input").value="",document.getElementById("answer-input-group").classList.add("d-none"),document.getElementById("answer-input").blur(),logGiveAnswer({directive:b,message:d,username:i}),"prompt"===b&&c?logEvent(i,`was prompted with "${c}"`):"prompt"===b?logEvent(i,"was prompted"):logEvent(i,`${0{a.textContent=parseInt(a.innerHTML)+1})),"reject"===b&&(document.getElementById("buzz").disabled=!document.getElementById("toggle-rebuzz").checked&&h===USER_ID),10f&&players[h].negs++,players[h].points+=f,players[h].tuh++,players[h].celerity=a,upsertPlayerItem(players[h]),sortPlayerListGroup()),"prompt"!==b&&h===USER_ID&&(await account.getUsername())&&questionStats.recordTossup(g,0{const b=parseInt(document.getElementById("points-"+d.id.substring(f)).innerHTML),e=parseInt(document.getElementById("points-"+a.id.substring(f)).innerHTML);// if points are equal, sort alphabetically by username
-if(b===e){const b=document.getElementById("username-"+d.id.substring(f)).innerHTML,e=document.getElementById("username-"+a.id.substring(f)).innerHTML;return c?b.localeCompare(e):e.localeCompare(b)}return c?e-b:b-e}).forEach(a=>{d.appendChild(a)})}function setCategories({alternateSubcategories:a,categories:b,subcategories:c,percentView:d,categoryPercents:e,username:f}){logEvent(f,"updated the categories"),categoryManager.import({categories:b,subcategories:c,alternateSubcategories:a,percentView:d,categoryPercents:e}),categoryManager.loadCategoryModal()}function setDifficulties({difficulties:a,username:b=void 0}){return b&&logEvent(b,0{const c=b.querySelector("input");a.includes(parseInt(c.value))?(c.checked=!0,b.classList.add("active")):(c.checked=!1,b.classList.remove("active"))}):void(startingDifficulties=a)}function setPacketNumbers({username:a,packetNumbers:b}){b=arrayToRange(b),logEvent(a,0
- ${escapeHTML(c)}
- ${i}
-
- `,k.setAttribute("data-bs-container","body"),k.setAttribute("data-bs-custom-class","custom-popover"),k.setAttribute("data-bs-html","true"),k.setAttribute("data-bs-placement","left"),k.setAttribute("data-bs-toggle","popover"),k.setAttribute("data-bs-trigger","focus"),k.setAttribute("tabindex","0"),k.setAttribute("data-bs-title",c),k.setAttribute("data-bs-content",`
-
- -
- Powers
- ${e}
-
- -
- Tens
- ${f}
-
- -
- Negs
- ${g}
-
- -
- TUH
- ${h}
-
- -
- Celerity
- ${j.toFixed(3)}
-
-
- `),document.getElementById("player-list-group").appendChild(k),new bootstrap.Popover(k)}function setYearRange({minYear:a,maxYear:b,username:c}){c&&logEvent(c,`changed the year range to ${a}-${b}`),$("#slider").slider("values",0,a),$("#slider").slider("values",1,b),document.getElementById("year-range-a").textContent=a,document.getElementById("year-range-b").textContent=b}document.getElementById("answer-form").addEventListener("submit",function(a){a.preventDefault(),a.stopPropagation();const b=document.getElementById("answer-input").value;socket.send(JSON.stringify({type:"give-answer",givenAnswer:b}))}),document.getElementById("answer-input").addEventListener("input",function(){socket.send(JSON.stringify({type:"give-answer-live-update",message:this.value}))}),document.getElementById("buzz").addEventListener("click",function(){this.blur(),audio.soundEffects&&audio.buzz.play(),socket.send(JSON.stringify({type:"buzz"})),socket.send(JSON.stringify({type:"give-answer-live-update",message:""}))}),document.getElementById("chat").addEventListener("click",function(){this.blur(),document.getElementById("chat-input-group").classList.remove("d-none"),document.getElementById("chat-input").focus(),socket.send(JSON.stringify({type:"chat-live-update",message:""}))}),document.getElementById("chat-form").addEventListener("submit",function(a){a.preventDefault(),a.stopPropagation();const b=document.getElementById("chat-input").value;document.getElementById("chat-input").value="",document.getElementById("chat-input-group").classList.add("d-none"),document.getElementById("chat-input").blur(),socket.send(JSON.stringify({type:"chat",message:b}))}),document.getElementById("chat-input").addEventListener("input",function(){socket.send(JSON.stringify({type:"chat-live-update",message:this.value}))}),document.getElementById("clear-stats").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"clear-stats"}))}),document.getElementById("next").addEventListener("click",function(){switch(this.blur(),this.innerHTML){case"Start":socket.send(JSON.stringify({type:"start"}));break;case"Next":socket.send(JSON.stringify({type:"next"}))}}),document.getElementById("skip").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"skip"}))}),document.getElementById("packet-number").addEventListener("change",function(){const a=rangeToArray(this.value,maxPacketNumber);return a.some(a=>1>a||a>maxPacketNumber)?void document.getElementById("packet-number").classList.add("is-invalid"):void(document.getElementById("packet-number").classList.remove("is-invalid"),socket.send(JSON.stringify({type:"set-packet-numbers",packetNumbers:a})))}),document.getElementById("pause").addEventListener("click",function(){this.blur();const a=parseFloat(document.querySelector(".timer .face").innerText),b=parseFloat(document.querySelector(".timer .fraction").innerText);socket.send(JSON.stringify({type:"pause",pausedTime:10*(a+b)}))}),document.getElementById("reading-speed").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-reading-speed",readingSpeed:this.value}))}),document.getElementById("reading-speed").addEventListener("input",function(){document.getElementById("reading-speed-display").textContent=this.value}),document.getElementById("report-question-submit").addEventListener("click",function(){api.reportQuestion(document.getElementById("report-question-id").value,document.getElementById("report-question-reason").value,document.getElementById("report-question-description").value)}),document.getElementById("set-name").addEventListener("change",async function(){api.getSetList().includes(this.value)||0===this.value.length?this.classList.remove("is-invalid"):this.classList.add("is-invalid"),maxPacketNumber=await api.getNumPackets(this.value),document.getElementById("packet-number").value=""===this.value||0===maxPacketNumber?"":`1-${maxPacketNumber}`,socket.send(JSON.stringify({type:"set-set-name",setName:this.value,packetNumbers:rangeToArray(document.getElementById("packet-number").value)}))}),document.getElementById("strictness").addEventListener("change",function(){this.blur(),socket.send(JSON.stringify({type:"set-strictness",strictness:this.value}))}),document.getElementById("strictness").addEventListener("input",function(){document.getElementById("strictness-display").textContent=this.value}),document.getElementById("toggle-lock").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-lock",lock:this.checked}))}),document.getElementById("toggle-login-required").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-login-required",loginRequired:this.checked}))}),document.getElementById("toggle-powermark-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-powermark-only",powermarkOnly:this.checked}))}),document.getElementById("toggle-rebuzz").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-rebuzz",rebuzz:this.checked}))}),document.getElementById("toggle-skip").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-skip",skip:this.checked}))}),document.getElementById("toggle-select-by-set-name").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-select-by-set-name",setName:document.getElementById("set-name").value,selectBySetName:this.checked}))}),document.getElementById("toggle-settings").addEventListener("click",function(){this.blur(),document.getElementById("buttons").classList.toggle("col-lg-9"),document.getElementById("buttons").classList.toggle("col-lg-12"),document.getElementById("content").classList.toggle("col-lg-9"),document.getElementById("content").classList.toggle("col-lg-12"),document.getElementById("settings").classList.toggle("d-none"),document.getElementById("settings").classList.toggle("d-lg-none")}),document.getElementById("toggle-standard-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-standard-only",standardOnly:this.checked}))}),document.getElementById("toggle-timer").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-timer",timer:this.checked}))}),document.getElementById("toggle-public").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-public",public:this.checked}))}),document.getElementById("username").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-username",userId:USER_ID,username:this.value})),username=this.value,window.localStorage.setItem("multiplayer-username",username)}),document.getElementById("year-range-a").onchange=function(){const[a,b]=$("#slider").slider("values");if(b{if("Escape"===a.key&&"chat-input"===document.activeElement.id&&(document.getElementById("chat-input").value="",document.getElementById("chat-input-group").classList.add("d-none"),document.getElementById("chat-input").blur(),socket.send(JSON.stringify({type:"chat",message:""}))),!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName))switch(a.key?.toLowerCase()){case" ":document.getElementById("buzz").click(),a.target===document.body&&a.preventDefault();break;case"e":return document.getElementById("toggle-settings").click();case"k":return document.getElementsByClassName("card-header-clickable")[0].click();case"p":return document.getElementById("pause").click();case"t":return document.getElementsByClassName("star-tossup")[0].click();case"y":return navigator.clipboard.writeText(tossup._id??"");case"n":case"s":document.getElementById("next").click(),document.getElementById("skip").click()}}),document.addEventListener("keypress",function(a){"Enter"===a.key&&a.target===document.body&&document.getElementById("chat").click()}),document.getElementById("username").value=username,ReactDOM.createRoot(document.getElementById("category-modal-root")).render(/*#__PURE__*/React.createElement(CategoryModal,{categoryManager:categoryManager,onClose:()=>{oldCategories!==JSON.stringify(categoryManager.export())&&socket.send(JSON.stringify({type:"set-categories",...categoryManager.export()})),oldCategories=JSON.stringify(categoryManager.export())}})),ReactDOM.createRoot(document.getElementById("difficulty-dropdown-root")).render(/*#__PURE__*/React.createElement(DifficultyDropdown,{startingDifficulties:startingDifficulties,onChange:()=>socket.send(JSON.stringify({type:"set-difficulties",difficulties:getDropdownValues("difficulties")}))}));
\ No newline at end of file
+socket.onclose=function(a){const{code:b}=a;3e3!==b&&window.alert("Disconnected from server"),clearInterval(PING_INTERVAL_ID)},socket.onmessage=function(a){const b=JSON.parse(a.data);switch(b.type){case"buzz":return buzz(b);case"force-username":return forceUsername(b);case"chat":return chat(b,!1);case"chat-live-update":return chat(b,!0);case"clear-stats":return clearStats(b);case"connection-acknowledged":return connectionAcknowledged(b);case"connection-acknowledged-query":return connectionAcknowledgedQuery(b);case"connection-acknowledged-tossup":return connectionAcknowledgedTossup(b);case"end-of-set":return endOfSet(b);case"error":return handleError(b);case"give-answer":return giveAnswer(b);case"give-answer-live-update":return logGiveAnswer(b,!0);case"join":return join(b);case"leave":return leave(b);case"lost-buzzer-race":return lostBuzzerRace(b);case"next":return next(b);case"no-questions-found":return noQuestionsFound(b);case"pause":return pause(b);case"reveal-answer":return revealAnswer(b);case"set-categories":return setCategories(b);case"set-difficulties":return setDifficulties(b);case"set-reading-speed":return setReadingSpeed(b);case"set-packet-numbers":return setPacketNumbers(b);case"set-strictness":return setStrictness(b);case"set-set-name":return setSetName(b);case"set-username":return setUsername(b);case"set-year-range":return setYearRange(b);case"skip":return next(b);case"start":return next(b);case"timer-update":return updateTimerDisplay(b.timeRemaining);case"toggle-lock":return toggleLock(b);case"toggle-login-required":return toggleLoginRequired(b);case"toggle-powermark-only":return togglePowermarkOnly(b);case"toggle-public":return togglePublic(b);case"toggle-rebuzz":return toggleRebuzz(b);case"toggle-select-by-set-name":return toggleSelectBySetName(b);case"toggle-skip":return toggleSkip(b);case"toggle-standard-only":return toggleStandardOnly(b);case"toggle-timer":return toggleTimer(b);case"update-question":return updateQuestion(b)}};function buzz({userId:a,username:b}){logEvent(b,"buzzed"),document.getElementById("buzz").disabled=!0,document.getElementById("pause").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("skip").disabled=!0,a===USER_ID&&(document.getElementById("answer-input-group").classList.remove("d-none"),document.getElementById("answer-input").focus())}function chat({message:a,userId:c,username:d},e=!1){if(!e&&""===a)return void document.getElementById("live-chat-"+c).parentElement.remove();if(!e&&a)return document.getElementById("live-chat-"+c).className="",void(document.getElementById("live-chat-"+c).id="");if(document.getElementById("live-chat-"+c))return void(document.getElementById("live-chat-"+c).textContent=a);const f=document.createElement("b");f.textContent=d;const b=document.createElement("span");b.classList.add("text-muted"),b.id="live-chat-"+c,b.textContent=a;const g=document.createElement("li");g.appendChild(f),g.appendChild(document.createTextNode(" ")),g.appendChild(b),document.getElementById("room-history").prepend(g)}function clearStats({userId:a}){for(const b of["celerity","negs","points","powers","tens","tuh","zeroes"])players[a][b]=0;upsertPlayerItem(players[a],USER_ID),sortPlayerListGroup()}function connectionAcknowledged({buzzedIn:a,canBuzz:b,isPermanent:c,players:d,questionProgress:e,settings:f,userId:g}){document.getElementById("buzz").disabled=!b,c&&(document.getElementById("category-select-button").disabled=!0,document.getElementById("strictness").disabled=!0,document.getElementById("toggle-public").disabled=!0,document.getElementById("toggle-select-by-set-name").disabled=!0,document.getElementById("private-chat-warning").innerHTML="This is a permanent room. Some settings have been restricted."),Object.keys(d).forEach(a=>{d[a].celerity=d[a].celerity.correct.average,players[a]=d[a],upsertPlayerItem(players[a],USER_ID)}),sortPlayerListGroup();0===e?(document.getElementById("next").textContent="Start",document.getElementById("next").classList.remove("btn-primary"),document.getElementById("next").classList.add("btn-success")):1===e?(showSkipButton(),document.getElementById("settings").classList.add("d-none"),a?(document.getElementById("buzz").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("pause").disabled=!0):(document.getElementById("buzz").disabled=!1,document.getElementById("pause").disabled=!1)):2===e?(showNextButton(),document.getElementById("settings").classList.add("d-none")):void 0;document.getElementById("toggle-lock").checked=f.lock,document.getElementById("toggle-login-required").checked=f.loginRequired,document.getElementById("chat").disabled=f.public,document.getElementById("toggle-lock").disabled=f.public,document.getElementById("toggle-login-required").disabled=f.public,document.getElementById("toggle-timer").disabled=f.public,document.getElementById("toggle-public").checked=f.public,document.getElementById("reading-speed").value=f.readingSpeed,document.getElementById("reading-speed-display").textContent=f.readingSpeed,document.getElementById("strictness").value=f.strictness,document.getElementById("strictness-display").textContent=f.strictness,document.getElementById("toggle-rebuzz").checked=f.rebuzz,document.getElementById("toggle-skip").checked=f.skip,document.getElementById("timer").classList.toggle("d-none",!f.timer),document.getElementById("toggle-timer").checked=f.timer,USER_ID=g,window.localStorage.setItem("USER_ID",USER_ID)}async function connectionAcknowledgedQuery({difficulties:k=[],minYear:a,maxYear:b,packetNumbers:l=[],powermarkOnly:c,selectBySetName:d,setName:m="",standardOnly:e,alternateSubcategories:f,categories:g,subcategories:h,percentView:i,categoryPercents:j}){setDifficulties({difficulties:k}),$("#slider").slider("values",0,a),$("#slider").slider("values",1,b),document.getElementById("year-range-a").textContent=a,document.getElementById("year-range-b").textContent=b,document.getElementById("packet-number").value=arrayToRange(l),document.getElementById("toggle-powermark-only").checked=c,document.getElementById("difficulty-settings").classList.toggle("d-none",d),document.getElementById("set-settings").classList.toggle("d-none",!d),document.getElementById("toggle-select-by-set-name").checked=d,document.getElementById("toggle-powermark-only").disabled=d,document.getElementById("toggle-standard-only").disabled=d,document.getElementById("set-name").value=m,maxPacketNumber=await api.getNumPackets(m),""!==m&&0===maxPacketNumber&&document.getElementById("set-name").classList.add("is-invalid"),document.getElementById("toggle-standard-only").checked=e,categoryManager.import({categories:g,subcategories:h,alternateSubcategories:f,percentView:i,categoryPercents:j}),categoryManager.loadCategoryModal()}function connectionAcknowledgedTossup({tossup:a}){tossup=a,document.getElementById("set-name-info").textContent=tossup?.set?.name??"",document.getElementById("packet-number-info").textContent=tossup?.packet?.number??"-",document.getElementById("question-number-info").textContent=tossup?.number??"-"}function endOfSet(){window.alert("You have reached the end of the set")}function forceUsername({message:a,username:b}){window.alert(a),window.localStorage.setItem("multiplayer-username",b),document.querySelector("#username").value=b}async function giveAnswer({celerity:a,directive:b,directedPrompt:c,givenAnswer:d,perQuestionCelerity:e,score:f,tossup:g,userId:h,username:i}){document.getElementById("answer-input").value="",document.getElementById("answer-input-group").classList.add("d-none"),document.getElementById("answer-input").blur(),logGiveAnswer({directive:b,message:d,username:i}),"prompt"===b&&c?logEvent(i,`was prompted with "${c}"`):"prompt"===b?logEvent(i,"was prompted"):logEvent(i,`${0{a.textContent=parseInt(a.innerHTML)+1})),"reject"===b&&(document.getElementById("buzz").disabled=!document.getElementById("toggle-rebuzz").checked&&h===USER_ID),10f&&players[h].negs++,players[h].points+=f,players[h].tuh++,players[h].celerity=a,upsertPlayerItem(players[h],USER_ID),sortPlayerListGroup()),"prompt"!==b&&h===USER_ID&&(await account.getUsername())&&questionStats.recordTossup(g,0{const b=parseInt(document.getElementById("points-"+d.id.substring(f)).innerHTML),e=parseInt(document.getElementById("points-"+a.id.substring(f)).innerHTML);// if points are equal, sort alphabetically by username
+if(b===e){const b=document.getElementById("username-"+d.id.substring(f)).innerHTML,e=document.getElementById("username-"+a.id.substring(f)).innerHTML;return c?b.localeCompare(e):e.localeCompare(b)}return c?e-b:b-e}).forEach(a=>{d.appendChild(a)})}function setCategories({alternateSubcategories:a,categories:b,subcategories:c,percentView:d,categoryPercents:e,username:f}){logEvent(f,"updated the categories"),categoryManager.import({categories:b,subcategories:c,alternateSubcategories:a,percentView:d,categoryPercents:e}),categoryManager.loadCategoryModal()}function setDifficulties({difficulties:a,username:b=void 0}){return b&&logEvent(b,0{const c=b.querySelector("input");a.includes(parseInt(c.value))?(c.checked=!0,b.classList.add("active")):(c.checked=!1,b.classList.remove("active"))}):void(startingDifficulties=a)}function setPacketNumbers({username:a,packetNumbers:b}){b=arrayToRange(b),logEvent(a,01>a||a>maxPacketNumber)?void document.getElementById("packet-number").classList.add("is-invalid"):void(document.getElementById("packet-number").classList.remove("is-invalid"),socket.send(JSON.stringify({type:"set-packet-numbers",packetNumbers:a})))}),document.getElementById("pause").addEventListener("click",function(){this.blur();const a=parseFloat(document.querySelector(".timer .face").innerText),b=parseFloat(document.querySelector(".timer .fraction").innerText);socket.send(JSON.stringify({type:"pause",pausedTime:10*(a+b)}))}),document.getElementById("reading-speed").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-reading-speed",readingSpeed:this.value}))}),document.getElementById("reading-speed").addEventListener("input",function(){document.getElementById("reading-speed-display").textContent=this.value}),document.getElementById("report-question-submit").addEventListener("click",function(){api.reportQuestion(document.getElementById("report-question-id").value,document.getElementById("report-question-reason").value,document.getElementById("report-question-description").value)}),document.getElementById("set-name").addEventListener("change",async function(){api.getSetList().includes(this.value)||0===this.value.length?this.classList.remove("is-invalid"):this.classList.add("is-invalid"),maxPacketNumber=await api.getNumPackets(this.value),document.getElementById("packet-number").value=""===this.value||0===maxPacketNumber?"":`1-${maxPacketNumber}`,socket.send(JSON.stringify({type:"set-set-name",setName:this.value,packetNumbers:rangeToArray(document.getElementById("packet-number").value)}))}),document.getElementById("strictness").addEventListener("change",function(){this.blur(),socket.send(JSON.stringify({type:"set-strictness",strictness:this.value}))}),document.getElementById("strictness").addEventListener("input",function(){document.getElementById("strictness-display").textContent=this.value}),document.getElementById("toggle-lock").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-lock",lock:this.checked}))}),document.getElementById("toggle-login-required").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-login-required",loginRequired:this.checked}))}),document.getElementById("toggle-powermark-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-powermark-only",powermarkOnly:this.checked}))}),document.getElementById("toggle-rebuzz").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-rebuzz",rebuzz:this.checked}))}),document.getElementById("toggle-skip").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-skip",skip:this.checked}))}),document.getElementById("toggle-select-by-set-name").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-select-by-set-name",setName:document.getElementById("set-name").value,selectBySetName:this.checked}))}),document.getElementById("toggle-settings").addEventListener("click",function(){this.blur(),document.getElementById("buttons").classList.toggle("col-lg-9"),document.getElementById("buttons").classList.toggle("col-lg-12"),document.getElementById("content").classList.toggle("col-lg-9"),document.getElementById("content").classList.toggle("col-lg-12"),document.getElementById("settings").classList.toggle("d-none"),document.getElementById("settings").classList.toggle("d-lg-none")}),document.getElementById("toggle-standard-only").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-standard-only",standardOnly:this.checked}))}),document.getElementById("toggle-timer").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-timer",timer:this.checked}))}),document.getElementById("toggle-public").addEventListener("click",function(){this.blur(),socket.send(JSON.stringify({type:"toggle-public",public:this.checked}))}),document.getElementById("username").addEventListener("change",function(){socket.send(JSON.stringify({type:"set-username",userId:USER_ID,username:this.value})),username=this.value,window.localStorage.setItem("multiplayer-username",username)}),document.getElementById("year-range-a").onchange=function(){const[a,b]=$("#slider").slider("values");if(b{if("Escape"===a.key&&"chat-input"===document.activeElement.id&&(document.getElementById("chat-input").value="",document.getElementById("chat-input-group").classList.add("d-none"),document.getElementById("chat-input").blur(),socket.send(JSON.stringify({type:"chat",message:""}))),!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName))switch(a.key?.toLowerCase()){case" ":document.getElementById("buzz").click(),a.target===document.body&&a.preventDefault();break;case"e":return document.getElementById("toggle-settings").click();case"k":return document.getElementsByClassName("card-header-clickable")[0].click();case"p":return document.getElementById("pause").click();case"t":return document.getElementsByClassName("star-tossup")[0].click();case"y":return navigator.clipboard.writeText(tossup._id??"");case"n":case"s":document.getElementById("next").click(),document.getElementById("skip").click()}}),document.addEventListener("keypress",function(a){"Enter"===a.key&&a.target===document.body&&document.getElementById("chat").click()}),document.getElementById("username").value=username,ReactDOM.createRoot(document.getElementById("category-modal-root")).render(/*#__PURE__*/React.createElement(CategoryModal,{categoryManager:categoryManager,onClose:()=>{oldCategories!==JSON.stringify(categoryManager.export())&&socket.send(JSON.stringify({type:"set-categories",...categoryManager.export()})),oldCategories=JSON.stringify(categoryManager.export())}})),ReactDOM.createRoot(document.getElementById("difficulty-dropdown-root")).render(/*#__PURE__*/React.createElement(DifficultyDropdown,{startingDifficulties:startingDifficulties,onChange:()=>socket.send(JSON.stringify({type:"set-difficulties",difficulties:getDropdownValues("difficulties")}))}));
\ No newline at end of file
diff --git a/client/scripts/upsertPlayerItem.js b/client/scripts/upsertPlayerItem.js
new file mode 100644
index 00000000..f9579406
--- /dev/null
+++ b/client/scripts/upsertPlayerItem.js
@@ -0,0 +1,64 @@
+import { escapeHTML } from './utilities/strings.js';
+
+/**
+ * Upserts a player item to the DOM element with the id `player-list-group`.
+ * @param {Player} player
+ * @param {string} USER_ID - The item is highlighted blue if `USER_ID === player.userId`.
+ */
+export default function upsertPlayerItem (player, USER_ID) {
+ const { userId, username, powers = 0, tens = 0, negs = 0, tuh = 0, points = 0, online } = player;
+ const celerity = player?.celerity?.correct?.average ?? player?.celerity ?? 0;
+
+ if (document.getElementById('list-group-' + userId)) {
+ document.getElementById('list-group-' + userId).remove();
+ }
+
+ const playerItem = document.createElement('a');
+ playerItem.className = `list-group-item ${userId === USER_ID ? 'user-score' : ''} clickable`;
+ playerItem.id = `list-group-${userId}`;
+ playerItem.innerHTML = `
+
+ ${escapeHTML(username)}
+ ${points}
+
+ `;
+
+ playerItem.setAttribute('data-bs-container', 'body');
+ playerItem.setAttribute('data-bs-custom-class', 'custom-popover');
+ playerItem.setAttribute('data-bs-html', 'true');
+ playerItem.setAttribute('data-bs-placement', 'left');
+ playerItem.setAttribute('data-bs-toggle', 'popover');
+ playerItem.setAttribute('data-bs-trigger', 'focus');
+ playerItem.setAttribute('tabindex', '0');
+
+ playerItem.setAttribute('data-bs-title', username);
+ playerItem.setAttribute('data-bs-content', `
+
+ -
+ Powers
+ ${powers}
+
+ -
+ Tens
+ ${tens}
+
+ -
+ Negs
+ ${negs}
+
+ -
+ TUH
+ ${tuh}
+
+ -
+ Celerity
+ ${celerity.toFixed(3)}
+
+
+ `);
+
+ document.getElementById('player-list-group').appendChild(playerItem);
+ // bootstrap requires "new" to be called on each popover
+ // eslint-disable-next-line no-new
+ new bootstrap.Popover(playerItem);
+}
diff --git a/client/singleplayer/ClientTossupRoom.js b/client/singleplayer/ClientTossupRoom.js
index b7ade161..589fef1b 100644
--- a/client/singleplayer/ClientTossupRoom.js
+++ b/client/singleplayer/ClientTossupRoom.js
@@ -5,17 +5,9 @@ export default class ClientTossupRoom extends TossupRoom {
constructor (name, categories = [], subcategories = [], alternateSubcategories = []) {
super(name, categories, subcategories, alternateSubcategories);
- this.previous = {
- celerity: 0,
- endOfQuestion: false,
- isCorrect: true,
- inPower: false,
- negValue: -5,
- powerValue: 15,
- tossup: {}
- };
this.settings = {
...this.settings,
+ aiMode: false,
skip: true,
showHistory: true,
typeToAnswer: true
@@ -32,6 +24,7 @@ export default class ClientTossupRoom extends TossupRoom {
async message (userId, message) {
switch (message.type) {
+ case 'toggle-ai-mode': return this.toggleAiMode(userId, message);
case 'toggle-correct': return this.toggleCorrect(userId, message);
case 'toggle-show-history': return this.toggleShowHistory(userId, message);
case 'toggle-type-to-answer': return this.toggleTypeToAnswer(userId, message);
@@ -56,17 +49,14 @@ export default class ClientTossupRoom extends TossupRoom {
document.getElementById('answer-input').value = value;
}
- async scoreTossup ({ givenAnswer }) {
- const { celerity, directive, directedPrompt, endOfQuestion, inPower, points } = await super.scoreTossup({ givenAnswer });
- this.previous.celerity = celerity;
- this.previous.endOfQuestion = endOfQuestion;
- this.previous.isCorrect = points > 0;
- this.previous.inPower = inPower;
- this.previous.tossup = this.tossup;
- return { celerity, directive, directedPrompt, points };
+ toggleAiMode (userId, { aiMode }) {
+ this.settings.aiMode = aiMode;
+ this.emitMessage({ type: 'toggle-ai-mode', aiMode, userId });
}
toggleCorrect (userId, { correct }) {
+ if (userId !== this.previous.userId) { return; }
+
const multiplier = correct ? 1 : -1;
if (this.previous.inPower) {
diff --git a/client/singleplayer/ai-mode/AIBot.js b/client/singleplayer/ai-mode/AIBot.js
new file mode 100644
index 00000000..be6e207b
--- /dev/null
+++ b/client/singleplayer/ai-mode/AIBot.js
@@ -0,0 +1,86 @@
+import Player from '../../../quizbowl/Player.js';
+
+export default class AIBot {
+ constructor (room, name = 'ai-bot') {
+ this.room = room;
+ this.player = new Player(name);
+ this.player.username = name;
+ this.socket = {
+ send: this.onmessage.bind(this),
+ sendToServer: (message) => room.message(name, message)
+ };
+ this.active = true;
+
+ this.tossup = {};
+ this.wordIndex = 0;
+ }
+
+ onmessage (message) {
+ const data = JSON.parse(message);
+ switch (data.type) {
+ case 'start':
+ case 'skip':
+ case 'next': return this.next(data);
+
+ case 'update-question': return this.updateQuestion(data);
+ }
+ }
+
+ get active () {
+ return this._active;
+ }
+
+ set active (value) {
+ this._active = value;
+ if (this._active) {
+ this.room.players[this.player.userId] = this.player;
+ this.room.sockets[this.player.userId] = this.socket;
+ } else {
+ this.room.leave(this.player.userId);
+ }
+ }
+
+ sendBuzz ({ correct }) {
+ if (!this.active) { return; }
+ // need to wait 50ms before each action
+ // otherwise the server will not process things correctly
+ setTimeout(
+ () => {
+ this.socket.sendToServer({ type: 'buzz' });
+ setTimeout(
+ () => this.socket.sendToServer({ type: 'give-answer', givenAnswer: correct ? this.tossup.answer_sanitized : '' }),
+ 1000
+ );
+ }, 50
+ );
+ }
+
+ /**
+ * Calculate when to buzz
+ * @returns {{buzzpoint: number, correctBuzz: boolean}}
+ */
+ calculateBuzzpoint ({ packetLength, oldTossup, tossup }) {
+ throw new Error('calculateBuzzpoint not implemented');
+ }
+
+ next ({ packetLength, oldTossup, tossup }) {
+ this.tossup = tossup;
+ this.wordIndex = 0;
+ ({ buzzpoint: this.buzzpoint, correctBuzz: this.correctBuzz } = this.calculateBuzzpoint({ packetLength, oldTossup, tossup }));
+ }
+
+ /**
+ *
+ * @param {({ packetLength, oldTossup, tossup }) => {buzzpoint: number, correctBuzz: boolean}} calculateBuzzpointFunction
+ */
+ setAIBot (calculateBuzzpointFunction) {
+ this.calculateBuzzpoint = calculateBuzzpointFunction;
+ }
+
+ updateQuestion ({ word }) {
+ if (word !== '(#)') { this.wordIndex++; }
+ if (this.wordIndex === this.buzzpoint) {
+ return this.sendBuzz({ correct: this.correctBuzz });
+ }
+ }
+}
diff --git a/client/singleplayer/ai-mode/README.md b/client/singleplayer/ai-mode/README.md
new file mode 100644
index 00000000..6aebd178
--- /dev/null
+++ b/client/singleplayer/ai-mode/README.md
@@ -0,0 +1,14 @@
+# How to add a new AI bot
+
+Following the pattern in `ai-bots.js`, create a function with the following signature:
+
+```javascript
+function ({ packetLength, oldTossup, tossup }) {
+ const buzzpoint: number;
+ const correctBuzz: boolean;
+ return { buzzpoint, correctBuzz };
+}
+```
+
+and add it (along with a description) to the `aiBots` dictionary.
+The rest of the code will handle loading the bot into the game, etc.
diff --git a/client/singleplayer/ai-mode/ai-bots.js b/client/singleplayer/ai-mode/ai-bots.js
new file mode 100644
index 00000000..eb45e96b
--- /dev/null
+++ b/client/singleplayer/ai-mode/ai-bots.js
@@ -0,0 +1,28 @@
+import loadAiBots from './load-ai-bots.js';
+
+const rightAfterPower = ({ packetLength, oldTossup, tossup }) => {
+ let buzzpoint = tossup.question_sanitized.split(' ').indexOf('(*)') + 1;
+ if (buzzpoint === 0) {
+ buzzpoint = tossup.question_sanitized.split(' ').length / 2;
+ buzzpoint = Math.floor(buzzpoint);
+ }
+ return { buzzpoint, correctBuzz: true };
+};
+
+const buzzRandomly = ({ packetLength, oldTossup, tossup }) => {
+ const buzzpoint = Math.floor(Math.random() * tossup.question_sanitized.split(' ').length);
+ const correctBuzz = Math.random() < 0.5;
+ return { buzzpoint, correctBuzz };
+};
+
+/**
+ * Should be in the format of:
+ * `[name: string]: [calculateBuzzpoint: function, description: string]`
+ */
+const aiBots = {
+ 'right-after-power': [rightAfterPower, 'Buzz right after the power mark'],
+ 'buzz-randomly': [buzzRandomly, 'Buzz at a random point in the question (50% chance of being correct)']
+};
+
+loadAiBots(aiBots);
+export default aiBots;
diff --git a/client/singleplayer/ai-mode/load-ai-bots.js b/client/singleplayer/ai-mode/load-ai-bots.js
new file mode 100644
index 00000000..9ef40166
--- /dev/null
+++ b/client/singleplayer/ai-mode/load-ai-bots.js
@@ -0,0 +1,16 @@
+export default function loadAiBots (aiBots) {
+ // eslint-disable-next-line no-unused-vars
+ for (const [key, [calculateBuzzpoint, description]] of Object.entries(aiBots)) {
+ document.getElementById('choose-ai').innerHTML += `
+
+
+
+
+ `;
+ }
+
+ const firstBot = Object.keys(aiBots)[0];
+ document.getElementById(`ai-choice-${firstBot}`).checked = true;
+}
diff --git a/client/singleplayer/tossups/index.html b/client/singleplayer/tossups/index.html
index 85aceaa0..df774baf 100644
--- a/client/singleplayer/tossups/index.html
+++ b/client/singleplayer/tossups/index.html
@@ -19,6 +19,11 @@
+
@@ -92,6 +97,9 @@
0.0
+
+
@@ -107,10 +115,16 @@
+
+
+
+
+