diff --git a/client/database/index.jsx b/client/database/index.jsx index c77e75c1..1f9a89fe 100644 --- a/client/database/index.jsx +++ b/client/database/index.jsx @@ -7,9 +7,9 @@ import BonusCard from '../scripts/components/BonusCard.min.js'; import CategoryModal from '../scripts/components/CategoryModal.min.js'; import DifficultyDropdown from '../scripts/components/DifficultyDropdown.min.js'; import Star from '../scripts/components/Star.min.js'; -import CategoryManager from '../scripts/utilities/category-manager.js'; +import CategoryManager from '../../quizbowl/category-manager.js'; import { getDropdownValues } from '../scripts/utilities/dropdown-checklist.js'; -import { insertTokensIntoHTML } from '../scripts/utilities/insert-tokens-into-html.js'; +import insertTokensIntoHTML from '../../quizbowl/insert-tokens-into-html.js'; const starredTossupIds = new Set(await star.getStarredTossupIds()); const starredBonusIds = new Set(await star.getStarredBonusIds()); diff --git a/client/database/index.min.js b/client/database/index.min.js index 4201b405..ee82c922 100644 --- a/client/database/index.min.js +++ b/client/database/index.min.js @@ -1,4 +1,4 @@ -import{downloadQuestionsAsText,downloadBonusesAsCSV,downloadTossupsAsCSV,downloadQuestionsAsJSON}from"./download.js";import api from"../scripts/api/index.js";import star from"../scripts/auth/star.js";import TossupCard from"../scripts/components/TossupCard.min.js";import BonusCard from"../scripts/components/BonusCard.min.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";import Star from"../scripts/components/Star.min.js";import CategoryManager from"../scripts/utilities/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import{insertTokensIntoHTML}from"../scripts/utilities/insert-tokens-into-html.js";const starredTossupIds=new Set(await star.getStarredTossupIds()),starredBonusIds=new Set(await star.getStarredBonusIds()),paginationShiftLength=992""!==a).map(a=>new RegExp(a,"ig")):[b];for(const g of f){if("question"===e||"all"===e){const{starts:b,ends:c}=getMatchIndices(a.question_sanitized,g);a.question=insertTokensIntoHTML(a.question,a.question_sanitized,[b,c])}if("answer"===e||"all"===e){const{starts:b,ends:c}=getMatchIndices(a.answer_sanitized,g);a.answer=insertTokensIntoHTML(a.answer,a.answer_sanitized,[b,c])}}return a}function highlightBonusQuery({bonus:a,regExp:b,searchType:e="all",ignoreWordOrder:c,queryString:d}){const f=c?d.split(" ").filter(a=>""!==a).map(a=>new RegExp(a,"ig")):[b];for(const g of f){if("question"===e||"all"===e){{const{starts:b,ends:c}=getMatchIndices(a.leadin_sanitized,g);a.leadin=insertTokensIntoHTML(a.leadin,a.leadin_sanitized,[b,c])}for(let b=0;ba+c)}function b(){return Math.floor(1e4/(r||25))}function c(a,c){switch(a.preventDefault(),c){case"first":R=1;break;case"previous":R=Math.max(1,R-1);break;case"next":R=Math.min(V,R+1,b());break;case"last":R=Math.min(V,b());break;default:R=c}S(R),$(paginationShiftLength*Math.floor((R-1)/paginationShiftLength)),e(a,!1,!0),window.scrollTo({top:document.getElementById("tossups").offsetTop-100,behavior:"smooth"})}function d(a,c){switch(a.preventDefault(),c){case"first":T=1;break;case"previous":T=Math.max(1,T-1);break;case"next":T=Math.min(X,T+1,b());break;case"last":T=Math.min(X,b());break;default:T=c}U(T),aa(paginationShiftLength*Math.floor((T-1)/paginationShiftLength)),e(a,!1,!0),window.scrollTo({top:document.getElementById("bonuses").offsetTop-100,behavior:"smooth"})}function e(a,b=!1,c=!1){const d=performance.now();a.preventDefault(),Q(!0),(b||!c)&&(R=1,T=1,S(R),U(T));const e=new URLSearchParams({queryString:t,...categoryManager.export(),difficulties:getDropdownValues("difficulties"),maxReturnLength:r,questionType:v,randomize:b,exactPhrase:H,powermarkOnly:J,regex:D,ignoreWordOrder:F,searchType:x,setName:document.getElementById("set-name").value,tossupPagination:R,bonusPagination:T,minYear:z,maxYear:B}).toString();fetch(`/api/query?${e}`).then(a=>{if(400===a.status)throw new Error("Invalid query");return a}).then(a=>a.json()).then(a=>{const{tossups:c,bonuses:f,queryString:h}=a,j=RegExp(h,"ig"),l=Math.max(1,r||25),{count:n,questionArray:p}=c,{count:s,questionArray:u}=f,v=JSON.parse(JSON.stringify(p)),w=JSON.parse(JSON.stringify(u));// create deep copy to highlight +import{downloadQuestionsAsText,downloadBonusesAsCSV,downloadTossupsAsCSV,downloadQuestionsAsJSON}from"./download.js";import api from"../scripts/api/index.js";import star from"../scripts/auth/star.js";import TossupCard from"../scripts/components/TossupCard.min.js";import BonusCard from"../scripts/components/BonusCard.min.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";import Star from"../scripts/components/Star.min.js";import CategoryManager from"../../quizbowl/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import insertTokensIntoHTML from"../../quizbowl/insert-tokens-into-html.js";const starredTossupIds=new Set(await star.getStarredTossupIds()),starredBonusIds=new Set(await star.getStarredBonusIds()),paginationShiftLength=992""!==a).map(a=>new RegExp(a,"ig")):[b];for(const g of f){if("question"===e||"all"===e){const{starts:b,ends:c}=getMatchIndices(a.question_sanitized,g);a.question=insertTokensIntoHTML(a.question,a.question_sanitized,[b,c])}if("answer"===e||"all"===e){const{starts:b,ends:c}=getMatchIndices(a.answer_sanitized,g);a.answer=insertTokensIntoHTML(a.answer,a.answer_sanitized,[b,c])}}return a}function highlightBonusQuery({bonus:a,regExp:b,searchType:e="all",ignoreWordOrder:c,queryString:d}){const f=c?d.split(" ").filter(a=>""!==a).map(a=>new RegExp(a,"ig")):[b];for(const g of f){if("question"===e||"all"===e){{const{starts:b,ends:c}=getMatchIndices(a.leadin_sanitized,g);a.leadin=insertTokensIntoHTML(a.leadin,a.leadin_sanitized,[b,c])}for(let b=0;ba+c)}function b(){return Math.floor(1e4/(r||25))}function c(a,c){switch(a.preventDefault(),c){case"first":R=1;break;case"previous":R=Math.max(1,R-1);break;case"next":R=Math.min(V,R+1,b());break;case"last":R=Math.min(V,b());break;default:R=c}S(R),$(paginationShiftLength*Math.floor((R-1)/paginationShiftLength)),e(a,!1,!0),window.scrollTo({top:document.getElementById("tossups").offsetTop-100,behavior:"smooth"})}function d(a,c){switch(a.preventDefault(),c){case"first":T=1;break;case"previous":T=Math.max(1,T-1);break;case"next":T=Math.min(X,T+1,b());break;case"last":T=Math.min(X,b());break;default:T=c}U(T),aa(paginationShiftLength*Math.floor((T-1)/paginationShiftLength)),e(a,!1,!0),window.scrollTo({top:document.getElementById("bonuses").offsetTop-100,behavior:"smooth"})}function e(a,b=!1,c=!1){const d=performance.now();a.preventDefault(),Q(!0),(b||!c)&&(R=1,T=1,S(R),U(T));const e=new URLSearchParams({queryString:t,...categoryManager.export(),difficulties:getDropdownValues("difficulties"),maxReturnLength:r,questionType:v,randomize:b,exactPhrase:H,powermarkOnly:J,regex:D,ignoreWordOrder:F,searchType:x,setName:document.getElementById("set-name").value,tossupPagination:R,bonusPagination:T,minYear:z,maxYear:B}).toString();fetch(`/api/query?${e}`).then(a=>{if(400===a.status)throw new Error("Invalid query");return a}).then(a=>a.json()).then(a=>{const{tossups:c,bonuses:f,queryString:h}=a,j=RegExp(h,"ig"),l=Math.max(1,r||25),{count:n,questionArray:p}=c,{count:s,questionArray:u}=f,v=JSON.parse(JSON.stringify(p)),w=JSON.parse(JSON.stringify(u));// create deep copy to highlight if(""!==t){for(let a=0;a{console.error("Error:",a),window.alert("Invalid query. Please check your search parameters and try again.")}).finally(()=>{document.querySelectorAll("b.collapsed[data-bs-toggle=\"collapse\"]").forEach(a=>a.classList.remove("collapsed")),document.querySelectorAll("div.card-container.collapse:not(.show)").forEach(a=>a.classList.add("show")),Q(!1)})}const[f,g]=React.useState([]),[h,i]=React.useState([]),[j,k]=React.useState([]),[l,m]=React.useState([]),[n,o]=React.useState(0),[p,q]=React.useState(0),[r,s]=React.useState(""),[t,u]=React.useState(""),[v,w]=React.useState("all"),[x,y]=React.useState("all"),[z,A]=React.useState(""),[B,C]=React.useState(""),[D,E]=React.useState(!1),[F,G]=React.useState(!1),[H,I]=React.useState(!1),[J,K]=React.useState(!1),[L,M]=React.useState(!1),[N,O]=React.useState(!0),[P,Q]=React.useState(!1);let[R,S]=React.useState(1),[T,U]=React.useState(1);const[V,W]=React.useState(1),[X,Y]=React.useState(1),[Z,$]=React.useState(0),[_,aa]=React.useState(0),[ba,ca]=React.useState(0),da="true"===window.localStorage.getItem("database-font-size")?window.localStorage.getItem("font-size")??16:16,ea=[];for(let a=0;a{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)}),window.addEventListener("popstate",a=>{if(null===a.state)return o(0),g([]),k([]),q(0),i([]),m([]),W(1),Y(1),$(0),void aa(0);const{tossups:b,highlightedTossupArray:c,bonuses:d,highlightedBonusArray:e,timeElapsed:f,workingMaxReturnLength:h,randomize:j}=a.state,{count:l,questionArray:n}=b,{count:p,questionArray:r}=d;o(l),g(n),k(c),q(p),i(r),m(e),j?(W(1),Y(1)):(W(Math.ceil(l/h)),Y(Math.ceil(p/h))),$(paginationShiftLength*Math.floor((R-1)/paginationShiftLength)),aa(paginationShiftLength*Math.floor((T-1)/paginationShiftLength)),ca(f)}),document.getElementById("set-list").innerHTML=api.getSetList().map(a=>``).join("")},[]),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement(CategoryModal,{categoryManager:categoryManager,disablePercentView:!0}),/*#__PURE__*/React.createElement("form",{className:"mt-3",onSubmit:a=>{e(a)}},/*#__PURE__*/React.createElement("div",{className:"input-group mb-2"},/*#__PURE__*/React.createElement("input",{type:"text",className:"form-control",id:"query",placeholder:"Query",value:t,onChange:a=>{u(a.target.value)}}),/*#__PURE__*/React.createElement("button",{type:"submit",className:"btn btn-info"},"Search"),/*#__PURE__*/React.createElement("button",{id:"randomize",className:"btn btn-success",onClick:a=>{e(a,!0)}},"Random")),/*#__PURE__*/React.createElement("div",{className:"row"},/*#__PURE__*/React.createElement("div",{className:"col-6 col-xl-3 mb-2"},/*#__PURE__*/React.createElement(DifficultyDropdown,null)),/*#__PURE__*/React.createElement("div",{className:"col-6 col-xl-3 mb-2"},/*#__PURE__*/React.createElement("input",{type:"number",className:"form-control",id:"max-return-length",placeholder:"# to Display",value:r,onChange:a=>{s(a.target.value)}})),/*#__PURE__*/React.createElement("div",{className:"input-group col-12 col-xl-6 mb-2"},/*#__PURE__*/React.createElement("input",{type:"text",className:"form-control",id:"set-name",placeholder:"Set Name",list:"set-list"}),/*#__PURE__*/React.createElement("datalist",{id:"set-list"}),/*#__PURE__*/React.createElement("button",{type:"button",className:"btn btn-danger",id:"category-select-button","data-bs-toggle":"modal","data-bs-target":"#category-modal"},"Categories"))),/*#__PURE__*/React.createElement("div",{className:"row"},/*#__PURE__*/React.createElement("div",{className:"col-6 col-md-3 mb-2"},/*#__PURE__*/React.createElement("select",{className:"form-select",id:"search-type",value:x,onChange:a=>{y(a.target.value)}},/*#__PURE__*/React.createElement("option",{value:"all"},"All text"),/*#__PURE__*/React.createElement("option",{value:"question"},"Question"),/*#__PURE__*/React.createElement("option",{value:"answer"},"Answer"))),/*#__PURE__*/React.createElement("div",{className:"col-6 col-md-3 mb-2"},/*#__PURE__*/React.createElement("select",{className:"form-select",id:"question-type",value:v,onChange:a=>{w(a.target.value)}},/*#__PURE__*/React.createElement("option",{value:"all"},"All questions"),/*#__PURE__*/React.createElement("option",{value:"tossup"},"Tossups"),/*#__PURE__*/React.createElement("option",{value:"bonus"},"Bonuses"))),/*#__PURE__*/React.createElement("div",{className:"col-6 col-md-3 mb-2"},/*#__PURE__*/React.createElement("input",{type:"number",className:"form-control",id:"min-year",placeholder:"Min Year",value:z,onChange:a=>{A(a.target.value)}})),/*#__PURE__*/React.createElement("div",{className:"col-6 col-md-3 mb-2"},/*#__PURE__*/React.createElement("input",{type:"number",className:"form-control",id:"max-year",placeholder:"Max Year",value:B,onChange:a=>{C(a.target.value)}}))),/*#__PURE__*/React.createElement("div",{className:"row"},/*#__PURE__*/React.createElement("div",{className:"col-12"},/*#__PURE__*/React.createElement("div",{className:"form-check form-switch"},/*#__PURE__*/React.createElement("input",{className:"form-check-input",type:"checkbox",role:"switch",id:"toggle-regex",checked:D,onChange:()=>{E(!D)}}),/*#__PURE__*/React.createElement("label",{className:"form-check-label",htmlFor:"toggle-regex"},"Search using regular expression"),/*#__PURE__*/React.createElement("a",{href:"https://www.sitepoint.com/learn-regex/"}," What's this?")),/*#__PURE__*/React.createElement("div",{className:"form-check form-switch"},/*#__PURE__*/React.createElement("input",{className:"form-check-input",type:"checkbox",role:"switch",id:"toggle-ignore-word-order",checked:!D&&F,disabled:D,onChange:()=>{G(!F)}}),/*#__PURE__*/React.createElement("label",{className:"form-check-label",htmlFor:"toggle-ignore-word-order"},"Ignore word order")),/*#__PURE__*/React.createElement("div",{className:"form-check form-switch"},/*#__PURE__*/React.createElement("input",{className:"form-check-input",type:"checkbox",role:"switch",id:"toggle-exact-phrase",checked:!D&&H,disabled:D,onChange:()=>{I(!H)}}),/*#__PURE__*/React.createElement("label",{className:"form-check-label",htmlFor:"toggle-exact-phrase"},"Search for exact phrase")),/*#__PURE__*/React.createElement("div",{className:"form-check form-switch"},/*#__PURE__*/React.createElement("input",{className:"form-check-input",type:"checkbox",role:"switch",id:"toggle-powermark-only",checked:J,onChange:()=>{K(!J)}}),/*#__PURE__*/React.createElement("label",{className:"form-check-label",htmlFor:"toggle-powermark-only"},"Powermarked tossups only")),/*#__PURE__*/React.createElement("div",{className:"form-check form-switch"},/*#__PURE__*/React.createElement("input",{className:"form-check-input",type:"checkbox",role:"switch",id:"toggle-hide-answerlines",checked:L,onChange:()=>{M(!L)}}),/*#__PURE__*/React.createElement("label",{className:"form-check-label",htmlFor:"toggle-hide-answerlines"},"Hide answerlines")),/*#__PURE__*/React.createElement("div",{className:"form-check form-switch"},/*#__PURE__*/React.createElement("input",{className:"form-check-input",type:"checkbox",role:"switch",id:"toggle-show-card-footers",checked:N,onChange:()=>{O(!N)}}),/*#__PURE__*/React.createElement("label",{className:"form-check-label",htmlFor:"toggle-show-card-footers"},"Show card footers")),/*#__PURE__*/React.createElement("div",{className:"float-end"},/*#__PURE__*/React.createElement("b",null,"Download this page:"),/*#__PURE__*/React.createElement("a",{className:"ms-2 clickable",onClick:()=>{downloadQuestionsAsText(f,h)}},"TXT"),/*#__PURE__*/React.createElement("a",{className:"ms-2 clickable",onClick:()=>{downloadTossupsAsCSV(f),downloadBonusesAsCSV(h)}},"CSV"),/*#__PURE__*/React.createElement("a",{className:"ms-2 clickable",onClick:()=>{downloadQuestionsAsJSON(f,h)}},"JSON"))))),P&&/*#__PURE__*/React.createElement("div",{className:"d-block mx-auto mt-3 spinner-border",role:"status"},/*#__PURE__*/React.createElement("span",{className:"d-none"},"Loading...")),/*#__PURE__*/React.createElement("div",{className:"row text-center mt-2 mt-sm-0"},/*#__PURE__*/React.createElement("h3",{id:"tossups"},"Tossups")),0window.scrollTo({top:document.getElementById("bonuses").offsetTop,behavior:"smooth"})},"Jump to bonuses")))// eslint-disable-line :/*#__PURE__*/React.createElement("div",{className:"text-muted"},"No tossups found"),/*#__PURE__*/React.createElement("div",null,ea),1{c(a,"first")}},"\xAB")),/*#__PURE__*/React.createElement("li",{className:"page-item"},/*#__PURE__*/React.createElement("a",{className:"page-link",href:"#","aria-label":"Previous",onClick:a=>{c(a,"previous")}},"\u2039")),a(Math.min(Z),Math.min(Z+paginationShiftLength,V)).map(a=>{const b=R===a+1;return/*#__PURE__*/React.createElement("li",{key:`tossup-pagination-${a+1}`,className:"page-item"},/*#__PURE__*/React.createElement("a",{className:`page-link ${b&&"active"}`,href:"#",onClick:b=>{c(b,a+1)}},a+1))}),/*#__PURE__*/React.createElement("li",{className:"page-item"},/*#__PURE__*/React.createElement("a",{className:"page-link",href:"#","aria-label":"Next",onClick:a=>{c(a,"next")}},"\u203A")),/*#__PURE__*/React.createElement("li",{className:"page-item"},/*#__PURE__*/React.createElement("a",{className:"page-link",href:"#","aria-label":"Last",onClick:a=>{c(a,"last")}},/*#__PURE__*/React.createElement("span",{"aria-hidden":"true"},"\xBB"))))),/*#__PURE__*/React.createElement("div",{className:"mb-5"}),/*#__PURE__*/React.createElement("div",{className:"row text-center"},/*#__PURE__*/React.createElement("h3",{id:"bonuses"},"Bonuses")),0window.scrollTo({top:document.getElementById("tossups").offsetTop,behavior:"smooth"})},"Jump to tossups")))// eslint-disable-line :/*#__PURE__*/React.createElement("div",{className:"text-muted"},"No bonuses found"),/*#__PURE__*/React.createElement("div",null,fa),1{d(a,"first")}},"\xAB")),/*#__PURE__*/React.createElement("li",{className:"page-item"},/*#__PURE__*/React.createElement("a",{className:"page-link",href:"#","aria-label":"Previous",onClick:a=>{d(a,"previous")}},"\u2039")),a(Math.min(_),Math.min(_+paginationShiftLength,X)).map(a=>{const b=T===a+1;return/*#__PURE__*/React.createElement("li",{key:`bonus-pagination-${a+1}`,className:"page-item"},/*#__PURE__*/React.createElement("a",{className:`page-link ${b&&"active"}`,href:"#",onClick:b=>{d(b,a+1)}},a+1))}),/*#__PURE__*/React.createElement("li",{className:"page-item"},/*#__PURE__*/React.createElement("a",{className:"page-link",href:"#","aria-label":"Next",onClick:a=>{d(a,"next")}},"\u203A")),/*#__PURE__*/React.createElement("li",{className:"page-item"},/*#__PURE__*/React.createElement("a",{className:"page-link",href:"#","aria-label":"Last",onClick:a=>{d(a,"last")}},/*#__PURE__*/React.createElement("span",{"aria-hidden":"true"},"\xBB"))))),/*#__PURE__*/React.createElement("div",{className:"mb-5"}))}const root=ReactDOM.createRoot(document.getElementById("root"));root.render(/*#__PURE__*/React.createElement(QueryForm,null)); \ No newline at end of file diff --git a/client/multiplayer/room.jsx b/client/multiplayer/room.jsx index 812359b8..0b306070 100644 --- a/client/multiplayer/room.jsx +++ b/client/multiplayer/room.jsx @@ -4,7 +4,7 @@ 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 '../scripts/utilities/category-manager.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'; @@ -452,7 +452,7 @@ function lostBuzzerRace ({ username, userId }) { if (userId === USER_ID) { document.getElementById('answer-input-group').classList.add('d-none'); } } -function next ({ tossup: nextTossup, type, username }) { +function next ({ oldTossup, tossup: nextTossup, type, username }) { switch (type) { case 'next': logEvent(username, 'went to the next question'); @@ -468,9 +468,7 @@ function next ({ tossup: nextTossup, type, username }) { } if (type === 'next' || type === 'skip') { - tossup.question = document.getElementById('question').innerHTML; - tossup.answer = document.getElementById('answer').innerHTML.replace('ANSWER: ', ''); - createTossupCard(tossup); + createTossupCard(oldTossup); } else if (type === 'start') { document.getElementById('next').classList.add('btn-primary'); document.getElementById('next').classList.remove('btn-success'); @@ -564,21 +562,21 @@ function setDifficulties ({ difficulties, username = undefined }) { }); } -function setPacketNumbers ({ username, value }) { - value = arrayToRange(value); - logEvent(username, value.length > 0 ? `changed packet numbers to ${value}` : 'cleared packet numbers'); - document.getElementById('packet-number').value = value; +function setPacketNumbers ({ username, packetNumbers }) { + packetNumbers = arrayToRange(packetNumbers); + logEvent(username, packetNumbers.length > 0 ? `changed packet numbers to ${packetNumbers}` : 'cleared packet numbers'); + document.getElementById('packet-number').value = packetNumbers; } -function setReadingSpeed ({ username, value }) { - logEvent(username, `changed the reading speed to ${value}`); - document.getElementById('reading-speed').value = value; - document.getElementById('reading-speed-display').textContent = value; +function setReadingSpeed ({ username, readingSpeed }) { + logEvent(username, `changed the reading speed to ${readingSpeed}`); + document.getElementById('reading-speed').value = readingSpeed; + document.getElementById('reading-speed-display').textContent = readingSpeed; } -function setSetName ({ username, value }) { - logEvent(username, value.length > 0 ? `changed set name to ${value}` : 'cleared set name'); - document.getElementById('set-name').value = value; +function setSetName ({ username, setName }) { + logEvent(username, setName.length > 0 ? `changed set name to ${setName}` : 'cleared set name'); + document.getElementById('set-name').value = setName; } function setUsername ({ oldUsername, newUsername, userId }) { @@ -825,7 +823,7 @@ document.getElementById('pause').addEventListener('click', function () { }); document.getElementById('reading-speed').addEventListener('change', function () { - socket.send(JSON.stringify({ type: 'set-reading-speed', value: this.value })); + socket.send(JSON.stringify({ type: 'set-reading-speed', readingSpeed: this.value })); }); document.getElementById('reading-speed').addEventListener('input', function () { @@ -855,7 +853,7 @@ document.getElementById('set-name').addEventListener('change', async function () socket.send(JSON.stringify({ type: 'set-set-name', - value: this.value, + setName: this.value, packetNumbers: rangeToArray(document.getElementById('packet-number').value) })); }); @@ -1008,6 +1006,6 @@ ReactDOM.createRoot(document.getElementById('category-modal-root')).render( ReactDOM.createRoot(document.getElementById('difficulty-dropdown-root')).render( socket.send(JSON.stringify({ type: 'set-difficulties', value: getDropdownValues('difficulties') }))} + onChange={() => socket.send(JSON.stringify({ type: 'set-difficulties', difficulties: getDropdownValues('difficulties') }))} /> ); diff --git a/client/multiplayer/room.min.js b/client/multiplayer/room.min.js index da2ff746..d5efb60d 100644 --- a/client/multiplayer/room.min.js +++ b/client/multiplayer/room.min.js @@ -1,8 +1,8 @@ -/* globals WebSocket */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"../scripts/utilities/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()),maxPacketNumber=24;/** +/* globals WebSocket */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()),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")||(await api.getRandomName());const socket=new 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-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("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("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:i=[],minYear:a,maxYear:b,packetNumbers:j=[],powermarkOnly:c,selectBySetName:d,setName:k="",standardOnly:e,validAlternateSubcategories:f,validCategories:g,validSubcategories:h}){setDifficulties({difficulties:i}),$("#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(j),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=k,maxPacketNumber=await api.getNumPackets(k),""!==k&&0===maxPacketNumber&&document.getElementById("set-name").classList.add("is-invalid"),document.getElementById("toggle-standard-only").checked=e,categoryManager.import(g,h,f),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,username:d}){logEvent(d,"updated the categories"),categoryManager.import(b,c,a),categoryManager.loadCategoryModal()}function setDifficulties({difficulties:a,username:b=void 0}){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"))})}function setPacketNumbers({username:a,value:b}){b=arrayToRange(b),logEvent(a,0{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("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:i=[],minYear:a,maxYear:b,packetNumbers:j=[],powermarkOnly:c,selectBySetName:d,setName:k="",standardOnly:e,validAlternateSubcategories:f,validCategories:g,validSubcategories:h}){setDifficulties({difficulties:i}),$("#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(j),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=k,maxPacketNumber=await api.getNumPackets(k),""!==k&&0===maxPacketNumber&&document.getElementById("set-name").classList.add("is-invalid"),document.getElementById("toggle-standard-only").checked=e,categoryManager.import(g,h,f),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,username:d}){logEvent(d,"updated the categories"),categoryManager.import(b,c,a),categoryManager.loadCategoryModal()}function setDifficulties({difficulties:a,username:b=void 0}){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"))})}function setPacketNumbers({username:a,packetNumbers:b}){b=arrayToRange(b),logEvent(a,0 @@ -32,4 +32,4 @@ k.className=`list-group-item ${b===USER_ID?"user-score":""} clickable`,k.id=`lis ${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",value: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",value: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",value:this.value,packetNumbers:rangeToArray(document.getElementById("packet-number").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{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,{onChange:()=>socket.send(JSON.stringify({type:"set-difficulties",value:getDropdownValues("difficulties")}))})); \ No newline at end of file + `),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",value: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("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{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,{onChange:()=>socket.send(JSON.stringify({type:"set-difficulties",difficulties:getDropdownValues("difficulties")}))})); \ No newline at end of file diff --git a/client/scripts/Timer.js b/client/scripts/Timer.js deleted file mode 100644 index 69b2bfb7..00000000 --- a/client/scripts/Timer.js +++ /dev/null @@ -1,40 +0,0 @@ -export default class Timer { - constructor () { - this.tenthsRemaining = 0; - } - - get seconds () { - return Math.floor(this.tenthsRemaining / 10); - } - - get tenths () { - return this.tenthsRemaining % 10; - } - - stopTimer () { - clearInterval(this.timerInterval); - } - - /** - * - * @param {number} duration - duration of the timer, in seconds - * @param {Function} callback - */ - startTimer (duration, callback) { - clearInterval(this.timerInterval); - this.tenthsRemaining = Math.floor(duration * 10); - this.timerInterval = setInterval(() => { - if (this.tenthsRemaining <= 0) { - clearInterval(this.timerInterval); - callback(); - } - this.updateDisplay(); - this.tenthsRemaining--; - }, 100); - } - - updateDisplay () { - document.querySelector('.timer .face').innerText = this.seconds; - document.querySelector('.timer .fraction').innerText = '.' + this.tenths; - } -} diff --git a/client/singleplayer/ClientTossupRoom.js b/client/singleplayer/ClientTossupRoom.js new file mode 100644 index 00000000..b1877c27 --- /dev/null +++ b/client/singleplayer/ClientTossupRoom.js @@ -0,0 +1,102 @@ +import api from '../scripts/api/index.js'; +import TossupRoom from '../../quizbowl/TossupRoom.js'; + +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, + skip: true, + showHistory: true, + typeToAnswer: true + }; + + this.checkAnswer = api.checkAnswer; + this.getRandomTossups = async (args) => await api.getRandomTossup({ number: 20, ...args }); + this.getSet = async ({ setName, packetNumbers }) => setName ? await api.getPacketTossups(setName, packetNumbers[0] ?? 1) : []; + this.getSetList = api.getSetList; + this.getNumPackets = api.getNumPackets; + + this.setList = this.getSetList(); + } + + async message (userId, message) { + switch (message.type) { + 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); + default: super.message(userId, message); + } + } + + buzz (userId) { + if (!this.settings.typeToAnswer && this.buzzes.includes(userId)) { + this.giveAnswer(userId, { givenAnswer: this.tossup.answer_sanitized }); + return; + } + + super.buzz(userId); + } + + get liveAnswer () { + return document.getElementById('answer-input').value; + } + + set liveAnswer (value) { + 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 }; + } + + toggleCorrect (userId, { correct }) { + const multiplier = correct ? 1 : -1; + + if (this.previous.inPower) { + this.players[userId].powers += multiplier * 1; + this.players[userId].points += multiplier * this.previous.powerValue; + } else { + this.players[userId].tens += multiplier * 1; + this.players[userId].points += multiplier * 10; + } + + if (this.previous.endOfQuestion) { + this.players[userId].dead += multiplier * -1; + } else { + this.players[userId].negs += multiplier * -1; + this.players[userId].points += multiplier * -this.previous.negValue; + } + + this.players[userId].celerity.correct.total += multiplier * this.previous.celerity; + this.players[userId].celerity.correct.average = this.players[userId].celerity.correct.total / (this.players[userId].powers + this.players[userId].tens); + + this.emitMessage({ type: 'toggle-correct', correct, userId }); + } + + toggleShowHistory (userId, { showHistory }) { + this.settings.showHistory = showHistory; + this.emitMessage({ type: 'toggle-show-history', showHistory, userId }); + } + + toggleTypeToAnswer (userId, { typeToAnswer }) { + this.settings.typeToAnswer = typeToAnswer; + this.emitMessage({ type: 'toggle-type-to-answer', typeToAnswer, userId }); + } +} diff --git a/client/singleplayer/bonuses.html b/client/singleplayer/bonuses/index.html similarity index 99% rename from client/singleplayer/bonuses.html rename to client/singleplayer/bonuses/index.html index 0e2998fa..2272004a 100644 --- a/client/singleplayer/bonuses.html +++ b/client/singleplayer/bonuses/index.html @@ -245,7 +245,7 @@ - + diff --git a/client/singleplayer/bonuses.jsx b/client/singleplayer/bonuses/index.jsx similarity index 97% rename from client/singleplayer/bonuses.jsx rename to client/singleplayer/bonuses/index.jsx index bd1457fa..579ab090 100644 --- a/client/singleplayer/bonuses.jsx +++ b/client/singleplayer/bonuses/index.jsx @@ -1,12 +1,12 @@ -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 { arrayToRange, createBonusCard, rangeToArray } from '../scripts/utilities/index.js'; -import CategoryManager from '../scripts/utilities/category-manager.js'; -import { getDropdownValues } from '../scripts/utilities/dropdown-checklist.js'; -import CategoryModal from '../scripts/components/CategoryModal.min.js'; -import DifficultyDropdown from '../scripts/components/DifficultyDropdown.min.js'; +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 { arrayToRange, createBonusCard, rangeToArray } from '../../scripts/utilities/index.js'; +import CategoryManager from '../../../quizbowl/category-manager.js'; +import { getDropdownValues } from '../../scripts/utilities/dropdown-checklist.js'; +import CategoryModal from '../../scripts/components/CategoryModal.min.js'; +import DifficultyDropdown from '../../scripts/components/DifficultyDropdown.min.js'; // Functions and variables specific to the bonuses page. diff --git a/client/singleplayer/bonuses.min.js b/client/singleplayer/bonuses/index.min.js similarity index 96% rename from client/singleplayer/bonuses.min.js rename to client/singleplayer/bonuses/index.min.js index 8d1c7341..9a05b923 100644 --- a/client/singleplayer/bonuses.min.js +++ b/client/singleplayer/bonuses/index.min.js @@ -1,4 +1,4 @@ -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{arrayToRange,createBonusCard,rangeToArray}from"../scripts/utilities/index.js";import CategoryManager from"../scripts/utilities/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";// Functions and variables specific to the bonuses page. +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{arrayToRange,createBonusCard,rangeToArray}from"../../scripts/utilities/index.js";import CategoryManager from"../../../quizbowl/category-manager.js";import{getDropdownValues}from"../../scripts/utilities/dropdown-checklist.js";import CategoryModal from"../../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../../scripts/components/DifficultyDropdown.min.js";// Functions and variables specific to the bonuses page. // Status variables let currentBonusPart=-1,maxPacketNumber=24,questionNumber=0,randomBonuses=[],bonuses=[{}];// WARNING: 1-indexed /** diff --git a/client/singleplayer/tossups.jsx b/client/singleplayer/tossups.jsx deleted file mode 100644 index 0dabda3e..00000000 --- a/client/singleplayer/tossups.jsx +++ /dev/null @@ -1,809 +0,0 @@ -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 Timer from '../scripts/Timer.js'; -import { arrayToRange, createTossupCard, rangeToArray } from '../scripts/utilities/index.js'; -import CategoryManager from '../scripts/utilities/category-manager.js'; -import { getDropdownValues } from '../scripts/utilities/dropdown-checklist.js'; -import { insertTokensIntoHTML } from '../scripts/utilities/insert-tokens-into-html.js'; -import CategoryModal from '../scripts/components/CategoryModal.min.js'; -import DifficultyDropdown from '../scripts/components/DifficultyDropdown.min.js'; - -// Functions and variables specific to the tossups page. - -const ANSWER_TIME_LIMIT = 10; -const DEAD_TIME_LIMIT = 5; - -// Status variables -let buzzpointIndex = -1; -let currentlyBuzzing = false; -let maxPacketNumber = 24; -let paused = false; -let questionNumber = 0; // WARNING: 1-indexed -const timer = new Timer(); - -/** - * An array of random questions. - * We get 20 random questions at a time so we don't have to make an HTTP request between every question. - */ -let randomTossups = []; -let timeoutID = -1; - -let tossups = [{}]; -let tossupText = ''; -let tossupTextSplit = []; - -const previous = { - isCorrect: true, - inPower: false, - negValue: -5, - powerValue: 15, - endOfQuestion: false, - celerity: 0 -}; - -const stats = window.sessionStorage.getItem('tossup-stats') - ? JSON.parse(window.sessionStorage.getItem('tossup-stats')) - : { - powers: 0, - tens: 0, - negs: 0, - dead: 0, - points: 0, - totalCorrectCelerity: 0 - }; - -const defaults = { - alternateSubcategories: [], - categories: [], - difficulties: [], - minYear: 2010, - maxYear: 2024, - packetNumbers: [], - powermarkOnly: false, - setName: '', - standardOnly: false, - subcategories: [], - version: '01-06-2024' -}; - -let query; -if (!window.localStorage.getItem('singleplayer-tossup-query')) { - query = defaults; -} else { - query = JSON.parse(window.localStorage.getItem('singleplayer-tossup-query')); - if (query.version !== defaults.version) { - query = defaults; - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); - } -} - -const categoryManager = new CategoryManager(query.categories, query.subcategories, query.alternateSubcategories); - -const settings = window.localStorage.getItem('singleplayer-tossup-settings') - ? JSON.parse(window.localStorage.getItem('singleplayer-tossup-settings')) - : { - readingSpeed: 50, - rebuzz: false, - selectBySetName: false, - showHistory: true, - timer: true, - typeToAnswer: true - }; - -// Load query and settings first so user doesn't see the default settings -if (settings.readingSpeed) { - document.getElementById('reading-speed-display').textContent = settings.readingSpeed; - document.getElementById('reading-speed').value = settings.readingSpeed; -} - -if (settings.rebuzz) { - document.getElementById('toggle-rebuzz').checked = true; -} - -if (settings.selectBySetName) { - document.getElementById('difficulty-settings').classList.add('d-none'); - document.getElementById('set-settings').classList.remove('d-none'); - document.getElementById('toggle-select-by-set-name').checked = true; - document.getElementById('toggle-powermark-only').disabled = true; - document.getElementById('toggle-standard-only').disabled = true; -} - -if (!settings.showHistory) { - document.getElementById('toggle-show-history').checked = false; - document.getElementById('room-history').classList.add('d-none'); -} - -if (!settings.timer) { - document.getElementById('toggle-timer').checked = false; - document.getElementById('timer').classList.add('d-none'); -} - -if (!settings.typeToAnswer) { - document.getElementById('type-to-answer').checked = false; - document.getElementById('toggle-rebuzz').disabled = true; -} - -if (query.packetNumbers) { - document.getElementById('packet-number').value = arrayToRange(query.packetNumbers); -} - -if (query.powermarkOnly) { - document.getElementById('toggle-powermark-only').checked = true; -} - -if (query.setName) { - document.getElementById('set-name').value = query.setName; - api.getNumPackets(query.setName).then(numPackets => { - maxPacketNumber = numPackets; - if (maxPacketNumber === 0) { - document.getElementById('set-name').classList.add('is-invalid'); - } else { - document.getElementById('packet-number').placeholder = `Packet Numbers (1-${maxPacketNumber})`; - } - }); -} - -updateStatDisplay(); - -function queryLock () { - document.getElementById('question').textContent = 'Fetching questions...'; - document.getElementById('start').disabled = true; - document.getElementById('next').disabled = true; - document.getElementById('pause').disabled = true; - document.getElementById('buzz').disabled = true; -} - -function queryUnlock () { - document.getElementById('question').textContent = ''; - document.getElementById('start').disabled = false; - document.getElementById('next').disabled = false; - document.getElementById('pause').disabled = false; - document.getElementById('buzz').disabled = false; -} - -/** - * @returns {Promise} Whether or not there is a next question - */ -async function advanceQuestion () { - if (settings.selectBySetName) { - // Get the next question if the current one is in the wrong category and subcategory - do { - questionNumber++; - - // Go to the next packet if you reach the end of this packet - if (questionNumber > tossups.length) { - query.packetNumbers.shift(); - if (query.packetNumbers.length === 0) { - window.alert('No more questions left'); - document.getElementById('buzz').disabled = true; - document.getElementById('pause').disabled = true; - document.getElementById('next').disabled = true; - return false; // alert the user if there are no more packets - } - - queryLock(); - try { - tossups = await api.getPacketTossups(query.setName, query.packetNumbers[0]); - } finally { - queryUnlock(); - } - - questionNumber = 1; - } - } while (!categoryManager.isValidCategory(tossups[questionNumber - 1])); - - if (Object.keys(tossups[0]).length > 0) { - tossupText = tossups[questionNumber - 1].question_sanitized; - tossupTextSplit = tossupText.split(' ').filter(word => word !== ''); - document.getElementById('question-number-info').textContent = questionNumber; - } - } else { - queryLock(); - try { - tossups = await getRandomTossup(query, categoryManager); - tossups = [tossups]; - } finally { - queryUnlock(); - } - - if (!tossups[0]) { - window.alert('No questions found'); - return false; - } - - query.setName = tossups[0].set.name; - query.packetNumbers = [tossups[0].packet.number]; - - tossupText = tossups[0].question_sanitized; - tossupTextSplit = tossupText.split(' ').filter(word => word !== ''); - document.getElementById('question-number-info').textContent = tossups[0].number; - questionNumber = 1; - } - - return true; -} - -/** - * Called when the users buzzes. - * The first "buzz" pauses the question, and the second "buzz" reveals the rest of the question - * and updates the score. - */ -function buzz () { - // Stop the question reading - clearTimeout(timeoutID); - currentlyBuzzing = true; - if (audio.soundEffects) audio.buzz.play(); - - buzzpointIndex = document.getElementById('question').textContent.length; - if (!tossupTextSplit.includes('(*)') && tossupText.includes('(*)')) { - buzzpointIndex += 3; - } - - // Include buzzpoint - document.getElementById('question').textContent += '(#) '; - - document.getElementById('buzz').textContent = 'Reveal'; - document.getElementById('next').disabled = true; - document.getElementById('start').disabled = true; - document.getElementById('pause').disabled = true; - - if (settings.timer) { - timer.stopTimer(); - timer.startTimer(ANSWER_TIME_LIMIT, () => document.getElementById('answer-submit').click()); - } -} - -/** - * Clears user stats. - */ -function clearStats () { - stats.powers = 0; - stats.tens = 0; - stats.negs = 0; - stats.dead = 0; - stats.points = 0; - stats.totalCorrectCelerity = 0; - - updateStatDisplay(); - window.sessionStorage.removeItem('tossup-stats'); -} - -async function giveAnswer (givenAnswer) { - currentlyBuzzing = false; - - const { directive, directedPrompt } = await api.checkAnswer(tossups[questionNumber - 1].answer, givenAnswer); - - switch (directive) { - case 'accept': { - const points = updateScore(true); - if (audio.soundEffects) { - if (points > 10) { - audio.power.play(); - } else { - audio.correct.play(); - } - } - revealQuestion(); - break; - } - case 'reject': - updateScore(false); - if (audio.soundEffects) audio.incorrect.play(); - if (settings.rebuzz) { - document.getElementById('buzz').disabled = false; - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('next').disabled = false; - document.getElementById('pause').disabled = false; - document.getElementById('start').disabled = false; - readQuestion(Date.now()); - } else { - revealQuestion(); - } - break; - case 'prompt': - document.getElementById('answer-input-group').classList.remove('d-none'); - document.getElementById('answer-input').focus(); - document.getElementById('answer-input').placeholder = directedPrompt ? `Prompt: "${directedPrompt}"` : 'Prompt'; - break; - } -} - -function isPace (setName) { - if (!setName) { return false; } - - return setName.includes('PACE'); -} - -async function loadRandomTossups ({ alternateSubcategories, categories, difficulties, maxYear, minYear, number = 1, powermarkOnly, standardOnly, subcategories } = {}) { - randomTossups = []; - randomTossups = await api.getRandomTossup({ alternateSubcategories, categories, difficulties, maxYear, minYear, number, powermarkOnly, standardOnly, subcategories }); -} - -/** - * Get a random tossup. - * @returns - */ -async function getRandomTossup ({ alternateSubcategories, categories, difficulties, minYear, maxYear, powermarkOnly, subcategories, standardOnly } = {}, categoryManager = null) { - if (categoryManager?.percentView) { - categories = [categoryManager.getRandomCategory()]; - subcategories = []; - alternateSubcategories = []; - await loadRandomTossups({ alternateSubcategories, categories, difficulties, maxYear, minYear, powermarkOnly, subcategories, standardOnly }); - return randomTossups.pop(); - } - - if (randomTossups.length === 0) { - await loadRandomTossups({ alternateSubcategories, categories, difficulties, maxYear, minYear, number: 20, powermarkOnly, subcategories, standardOnly }); - } - - const randomQuestion = randomTossups.pop(); - - // Begin loading the next batch of questions (asynchronously) - if (randomTossups.length === 0) { - loadRandomTossups({ alternateSubcategories, categories, difficulties, maxYear, minYear, number: 20, powermarkOnly, subcategories, standardOnly }); - } - - return randomQuestion; -} - -async function next () { - // Stop reading the current question: - clearTimeout(timeoutID); - currentlyBuzzing = false; - if (settings.timer) { - timer.stopTimer(); - timer.tenthsRemaining = 0; - timer.updateDisplay(); - } - - if (await account.getUsername() && document.getElementById('answer').innerHTML) { - const pointValue = previous.isCorrect ? (previous.inPower ? previous.powerValue : 10) : (previous.endOfQuestion ? 0 : previous.negValue); - questionStats.recordTossup(tossups[questionNumber - 1], previous.isCorrect, pointValue, previous.celerity, false); - } - - document.getElementById('answer').textContent = ''; - document.getElementById('question').textContent = ''; - document.getElementById('toggle-correct').textContent = 'I was wrong'; - document.getElementById('toggle-correct').classList.add('d-none'); - - const hasNextQuestion = await advanceQuestion(); - - if (!hasNextQuestion) { - return; - } - - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('buzz').disabled = false; - document.getElementById('next').textContent = 'Skip'; - document.getElementById('packet-number-info').textContent = query.packetNumbers[0]; - document.getElementById('packet-length-info').textContent = settings.selectBySetName ? tossups.length : '-'; - document.getElementById('pause').textContent = 'Pause'; - document.getElementById('pause').disabled = false; - document.getElementById('question').textContent = ''; - document.getElementById('set-name-info').textContent = query.setName; - - paused = false; - readQuestion(Date.now()); -} - -/** - * Toggles pausing or resuming the tossup. - */ -function pause () { - if (paused) { - document.getElementById('buzz').removeAttribute('disabled'); - document.getElementById('pause').textContent = 'Pause'; - readQuestion(Date.now()); - } else { - document.getElementById('buzz').setAttribute('disabled', 'disabled'); - document.getElementById('pause').textContent = 'Resume'; - clearTimeout(timeoutID); - } - paused = !paused; -} - -/** - * Recursively reads the question based on the reading speed. - */ -function readQuestion (expectedReadTime) { - if (!currentlyBuzzing && tossupTextSplit.length > 0) { - const word = tossupTextSplit.shift(); - if (word !== '(*)') { - document.getElementById('question').textContent += 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; } else if (word.endsWith(',') || word.slice(-2) === ',\u201d') { time += 0.75; } else if (word === '(*)') { time = 0; } - - time = time * 0.9 * (125 - settings.readingSpeed); - const delay = time - Date.now() + expectedReadTime; - - timeoutID = window.setTimeout(() => { - readQuestion(time + expectedReadTime); - }, delay); - } else { - document.getElementById('pause').disabled = true; - if (settings.timer) { - timer.startTimer(DEAD_TIME_LIMIT, revealQuestion); - } - } -} - -function revealQuestion () { - document.getElementById('question').innerHTML = insertTokensIntoHTML(tossups[questionNumber - 1].question, tossups[questionNumber - 1].question_sanitized, [[buzzpointIndex]], [' (#) ']); - document.getElementById('answer').innerHTML = 'ANSWER: ' + tossups[questionNumber - 1].answer; - - document.getElementById('buzz').disabled = true; - document.getElementById('buzz').textContent = 'Buzz'; - document.getElementById('next').disabled = false; - document.getElementById('next').textContent = 'Next'; - document.getElementById('start').disabled = false; - - document.getElementById('toggle-correct').classList.remove('d-none'); - document.getElementById('toggle-correct').textContent = previous.isCorrect ? 'I was wrong' : 'I was right'; -} - -function toggleCorrect () { - const multiplier = previous.isCorrect ? -1 : 1; - - if (previous.inPower) { - stats.powers += multiplier * 1; - stats.points += multiplier * previous.powerValue; - } else { - stats.tens += multiplier * 1; - stats.points += multiplier * 10; - } - - if (previous.endOfQuestion) { - stats.dead += multiplier * -1; - } else { - stats.negs += multiplier * -1; - stats.points += multiplier * -previous.negValue; - } - - stats.totalCorrectCelerity += multiplier * previous.celerity; - - previous.isCorrect = !previous.isCorrect; - document.getElementById('toggle-correct').textContent = previous.isCorrect ? 'I was wrong' : 'I was right'; - - updateStatDisplay(); - window.sessionStorage.setItem('tossup-stats', JSON.stringify(stats)); -} - -function updateScore (isCorrect) { - const endOfQuestion = (tossupTextSplit.length === 0); - const inPower = tossupTextSplit.includes('(*)') && tossupText.includes('(*)'); - const powerValue = isPace(query.setName) ? 20 : 15; - const negValue = isPace(query.setName) ? 0 : -5; - const points = isCorrect ? (inPower ? powerValue : 10) : (endOfQuestion ? 0 : negValue); - - const characterCount = tossupTextSplit.join(' ').length; - const celerity = characterCount / tossupText.length; - - let result; - - if (isCorrect) { - result = inPower ? 'powers' : 'tens'; - stats.totalCorrectCelerity += celerity; - } else { - result = endOfQuestion ? 'dead' : 'negs'; - } - - stats[result] += 1; - stats.points += points; - - previous.celerity = celerity; - previous.endOfQuestion = endOfQuestion; - previous.inPower = inPower; - previous.negValue = negValue; - previous.powerValue = powerValue; - previous.isCorrect = isCorrect; - - updateStatDisplay(); - window.sessionStorage.setItem('tossup-stats', JSON.stringify(stats)); - - return points; -} - -/** - * Updates the displayed stat line. - */ -function updateStatDisplay () { - const { powers, tens, negs, dead, points, totalCorrectCelerity } = stats; - const numTossups = powers + tens + negs + dead; - const numCorrectTossups = powers + tens; - let celerity = numCorrectTossups === 0 ? 0 : parseFloat(totalCorrectCelerity) / numCorrectTossups; - celerity = Math.round(1000 * celerity) / 1000; - const includePlural = (numTossups === 1) ? '' : 's'; - document.getElementById('statline').innerHTML = - `${powers}/${tens}/${negs} with ${numTossups} tossup${includePlural} seen (${points} pts, celerity: ${celerity})`; - - // disable clear stats button if no stats - document.getElementById('clear-stats').disabled = (numTossups === 0); -} - -document.getElementById('answer-form').addEventListener('submit', function (event) { - event.preventDefault(); - event.stopPropagation(); - - if (settings.timer) { - timer.stopTimer(); - timer.tenthsRemaining = 0; - timer.updateDisplay(); - } - - const answer = document.getElementById('answer-input').value; - - document.getElementById('answer-input').value = ''; - document.getElementById('answer-input').blur(); - document.getElementById('answer-input').placeholder = 'Enter answer'; - document.getElementById('answer-input-group').classList.add('d-none'); - - giveAnswer(answer); -}); - -document.getElementById('buzz').addEventListener('click', function () { - this.blur(); - - // reveal answer on second click - // when NOT using type to answer - if (currentlyBuzzing) { - currentlyBuzzing = false; - updateScore(true); - revealQuestion(); - return; - } - - buzz(); - - if (settings.typeToAnswer) { - document.getElementById('answer-input-group').classList.remove('d-none'); - document.getElementById('answer-input').focus(); - this.disabled = true; - } -}); - -document.getElementById('clear-stats').addEventListener('click', function () { - this.blur(); - clearStats(); -}); - -document.getElementById('next').addEventListener('click', function () { - this.blur(); - createTossupCard(tossups[questionNumber - 1]); - next(); -}); - -document.getElementById('packet-number').addEventListener('change', function () { - // if field is blank, store blank result in `query` - query.packetNumbers = rangeToArray(this.value.trim(), 0); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); - query.packetNumbers = rangeToArray(this.value.trim(), maxPacketNumber); -}); - -document.getElementById('pause').addEventListener('click', function () { - this.blur(); - pause(); -}); - -document.getElementById('reading-speed').addEventListener('input', function () { - settings.readingSpeed = this.value; - document.getElementById('reading-speed-display').textContent = this.value; - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify(settings)); -}); - -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 () { - query.setName = this.value.trim(); - - // make border red if set name is not in set list - if (api.getSetList().includes(this.value) || this.value.length === 0) { - this.classList.remove('is-invalid'); - } else { - this.classList.add('is-invalid'); - } - - maxPacketNumber = await api.getNumPackets(this.value); - - if (this.value === '' || maxPacketNumber === 0) { - document.getElementById('packet-number').placeholder = 'Packet Numbers'; - } else { - document.getElementById('packet-number').placeholder = `Packet Numbers (1-${maxPacketNumber})`; - } - - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); -}); - -document.getElementById('start').addEventListener('click', async function () { - this.blur(); - if (query.setName.length === 0 && settings.selectBySetName) { - window.alert('Please enter a set name.'); - return false; - } - - if (query.packetNumbers.length === 0 && settings.selectBySetName) { - query.packetNumbers = rangeToArray(document.getElementById('packet-number').value.trim(), maxPacketNumber); - } - - document.getElementById('next').disabled = false; - document.getElementById('next').textContent = 'Skip'; - document.getElementById('settings').classList.add('d-none'); - - if (settings.selectBySetName) { - queryLock(); - questionNumber = 0; - try { - tossups = await api.getPacketTossups(query.setName, query.packetNumbers[0]); - } finally { - queryUnlock(); - } - } - - next(); -}); - -document.getElementById('toggle-correct').addEventListener('click', function () { - this.blur(); - toggleCorrect(); -}); - -document.getElementById('toggle-powermark-only').addEventListener('click', function () { - this.blur(); - query.powermarkOnly = this.checked; - loadRandomTossups(query); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); -}); - -document.getElementById('toggle-select-by-set-name').addEventListener('click', function () { - this.blur(); - settings.selectBySetName = this.checked; - document.getElementById('toggle-powermark-only').disabled = this.checked; - document.getElementById('toggle-standard-only').disabled = this.checked; - - if (this.checked) { - document.getElementById('difficulty-settings').classList.add('d-none'); - document.getElementById('set-settings').classList.remove('d-none'); - } else { - document.getElementById('difficulty-settings').classList.remove('d-none'); - document.getElementById('set-settings').classList.add('d-none'); - } - - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify(settings)); -}); - -document.getElementById('toggle-show-history').addEventListener('click', function () { - this.blur(); - settings.showHistory = this.checked; - - if (this.checked) { - document.getElementById('room-history').classList.remove('d-none'); - } else { - document.getElementById('room-history').classList.add('d-none'); - } - - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify(settings)); -}); - -document.getElementById('toggle-standard-only').addEventListener('click', function () { - this.blur(); - query.standardOnly = this.checked; - loadRandomTossups(query); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); -}); - -document.getElementById('toggle-timer').addEventListener('click', function () { - this.blur(); - settings.timer = this.checked; - document.getElementById('timer').classList.toggle('d-none'); - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify(settings)); -}); - -document.getElementById('type-to-answer').addEventListener('click', function () { - this.blur(); - settings.typeToAnswer = this.checked; - document.getElementById('toggle-rebuzz').disabled = !this.checked; - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify(settings)); -}); - -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-rebuzz').addEventListener('click', function () { - this.blur(); - settings.rebuzz = this.checked; - window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify(settings)); -}); - -document.getElementById('year-range-a').onchange = function () { - query.minYear = $('#slider').slider('values', 0); - query.maxYear = $('#slider').slider('values', 1); - loadRandomTossups(query); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); -}; - -document.getElementById('year-range-b').onchange = function () { - query.minYear = $('#slider').slider('values', 0); - query.maxYear = $('#slider').slider('values', 1); - loadRandomTossups(query); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); -}; - -document.addEventListener('keydown', (event) => { - if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return; - - switch (event.key) { - case ' ': - document.getElementById('buzz').click(); - // Prevent spacebar from scrolling the page: - if (event.target === document.body) event.preventDefault(); - break; - case 'e': - document.getElementById('toggle-settings').click(); - break; - case 'k': - document.getElementsByClassName('card-header-clickable')[0].click(); - break; - case 't': - document.getElementsByClassName('star-tossup')[0].click(); - break; - case 'y': - navigator.clipboard.writeText(tossups[0]?._id ?? ''); - break; - case 'n': - document.getElementById('next').click(); - break; - case 'p': - document.getElementById('pause').click(); - break; - case 's': - document.getElementById('start').click(); - break; - } -}); - -$(document).ready(function () { - $('#slider').slider('values', 0, query.minYear); - $('#slider').slider('values', 1, query.maxYear); -}); -document.getElementById('year-range-a').textContent = query.minYear; -document.getElementById('year-range-b').textContent = query.maxYear; - -ReactDOM.createRoot(document.getElementById('category-modal-root')).render( - { - ({ categories: query.categories, subcategories: query.subcategories, alternateSubcategories: query.alternateSubcategories } = categoryManager.export()); - loadRandomTossups(query); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); - }} - /> -); - -ReactDOM.createRoot(document.getElementById('difficulty-dropdown-root')).render( - { - query.difficulties = getDropdownValues('difficulties'); - loadRandomTossups(query); - window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify(query)); - }} - /> -); diff --git a/client/singleplayer/tossups.min.js b/client/singleplayer/tossups.min.js deleted file mode 100644 index cb53d178..00000000 --- a/client/singleplayer/tossups.min.js +++ /dev/null @@ -1,36 +0,0 @@ -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 Timer from"../scripts/Timer.js";import{arrayToRange,createTossupCard,rangeToArray}from"../scripts/utilities/index.js";import CategoryManager from"../scripts/utilities/category-manager.js";import{getDropdownValues}from"../scripts/utilities/dropdown-checklist.js";import{insertTokensIntoHTML}from"../scripts/utilities/insert-tokens-into-html.js";import CategoryModal from"../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../scripts/components/DifficultyDropdown.min.js";// Functions and variables specific to the tossups page. -const ANSWER_TIME_LIMIT=10,DEAD_TIME_LIMIT=5;// Status variables -let buzzpointIndex=-1,currentlyBuzzing=!1,maxPacketNumber=24,paused=!1,questionNumber=0;// WARNING: 1-indexed -const timer=new Timer;/** - * An array of random questions. - * We get 20 random questions at a time so we don't have to make an HTTP request between every question. - */let randomTossups=[],timeoutID=-1,tossups=[{}],tossupText="",tossupTextSplit=[];const previous={isCorrect:!0,inPower:!1,negValue:-5,powerValue:15,endOfQuestion:!1,celerity:0},stats=window.sessionStorage.getItem("tossup-stats")?JSON.parse(window.sessionStorage.getItem("tossup-stats")):{powers:0,tens:0,negs:0,dead:0,points:0,totalCorrectCelerity:0},defaults={alternateSubcategories:[],categories:[],difficulties:[],minYear:2010,maxYear:2024,packetNumbers:[],powermarkOnly:!1,setName:"",standardOnly:!1,subcategories:[],version:"01-06-2024"};let query;window.localStorage.getItem("singleplayer-tossup-query")?(query=JSON.parse(window.localStorage.getItem("singleplayer-tossup-query")),query.version!==defaults.version&&(query=defaults,window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query)))):query=defaults;const categoryManager=new CategoryManager(query.categories,query.subcategories,query.alternateSubcategories),settings=window.localStorage.getItem("singleplayer-tossup-settings")?JSON.parse(window.localStorage.getItem("singleplayer-tossup-settings")):{readingSpeed:50,rebuzz:!1,selectBySetName:!1,showHistory:!0,timer:!0,typeToAnswer:!0};// Load query and settings first so user doesn't see the default settings -settings.readingSpeed&&(document.getElementById("reading-speed-display").textContent=settings.readingSpeed,document.getElementById("reading-speed").value=settings.readingSpeed),settings.rebuzz&&(document.getElementById("toggle-rebuzz").checked=!0),settings.selectBySetName&&(document.getElementById("difficulty-settings").classList.add("d-none"),document.getElementById("set-settings").classList.remove("d-none"),document.getElementById("toggle-select-by-set-name").checked=!0,document.getElementById("toggle-powermark-only").disabled=!0,document.getElementById("toggle-standard-only").disabled=!0),settings.showHistory||(document.getElementById("toggle-show-history").checked=!1,document.getElementById("room-history").classList.add("d-none")),settings.timer||(document.getElementById("toggle-timer").checked=!1,document.getElementById("timer").classList.add("d-none")),settings.typeToAnswer||(document.getElementById("type-to-answer").checked=!1,document.getElementById("toggle-rebuzz").disabled=!0),query.packetNumbers&&(document.getElementById("packet-number").value=arrayToRange(query.packetNumbers)),query.powermarkOnly&&(document.getElementById("toggle-powermark-only").checked=!0),query.setName&&(document.getElementById("set-name").value=query.setName,api.getNumPackets(query.setName).then(a=>{maxPacketNumber=a,0===maxPacketNumber?document.getElementById("set-name").classList.add("is-invalid"):document.getElementById("packet-number").placeholder=`Packet Numbers (1-${maxPacketNumber})`})),updateStatDisplay();function queryLock(){document.getElementById("question").textContent="Fetching questions...",document.getElementById("start").disabled=!0,document.getElementById("next").disabled=!0,document.getElementById("pause").disabled=!0,document.getElementById("buzz").disabled=!0}function queryUnlock(){document.getElementById("question").textContent="",document.getElementById("start").disabled=!1,document.getElementById("next").disabled=!1,document.getElementById("pause").disabled=!1,document.getElementById("buzz").disabled=!1}/** - * @returns {Promise} Whether or not there is a next question - */async function advanceQuestion(){if(settings.selectBySetName){// Get the next question if the current one is in the wrong category and subcategory -do// Go to the next packet if you reach the end of this packet -if(questionNumber++,questionNumber>tossups.length){if(query.packetNumbers.shift(),0===query.packetNumbers.length)return window.alert("No more questions left"),document.getElementById("buzz").disabled=!0,document.getElementById("pause").disabled=!0,document.getElementById("next").disabled=!0,!1;// alert the user if there are no more packets -queryLock();try{tossups=await api.getPacketTossups(query.setName,query.packetNumbers[0])}finally{queryUnlock()}questionNumber=1}while(!categoryManager.isValidCategory(tossups[questionNumber-1]));0""!==a),document.getElementById("question-number-info").textContent=questionNumber)}else{queryLock();try{tossups=await getRandomTossup(query,categoryManager),tossups=[tossups]}finally{queryUnlock()}if(!tossups[0])return window.alert("No questions found"),!1;query.setName=tossups[0].set.name,query.packetNumbers=[tossups[0].packet.number],tossupText=tossups[0].question_sanitized,tossupTextSplit=tossupText.split(" ").filter(a=>""!==a),document.getElementById("question-number-info").textContent=tossups[0].number,questionNumber=1}return!0}/** - * Called when the users buzzes. - * The first "buzz" pauses the question, and the second "buzz" reveals the rest of the question - * and updates the score. - */function buzz(){// Stop the question reading -// Include buzzpoint -clearTimeout(timeoutID),currentlyBuzzing=!0,audio.soundEffects&&audio.buzz.play(),buzzpointIndex=document.getElementById("question").textContent.length,!tossupTextSplit.includes("(*)")&&tossupText.includes("(*)")&&(buzzpointIndex+=3),document.getElementById("question").textContent+="(#) ",document.getElementById("buzz").textContent="Reveal",document.getElementById("next").disabled=!0,document.getElementById("start").disabled=!0,document.getElementById("pause").disabled=!0,settings.timer&&(timer.stopTimer(),timer.startTimer(ANSWER_TIME_LIMIT,()=>document.getElementById("answer-submit").click()))}/** - * Clears user stats. - */function clearStats(){stats.powers=0,stats.tens=0,stats.negs=0,stats.dead=0,stats.points=0,stats.totalCorrectCelerity=0,updateStatDisplay(),window.sessionStorage.removeItem("tossup-stats")}async function giveAnswer(a){currentlyBuzzing=!1;const{directive:b,directedPrompt:c}=await api.checkAnswer(tossups[questionNumber-1].answer,a);switch(b){case"accept":{const a=updateScore(!0);audio.soundEffects&&(10b.charCodeAt(b.length-2)||".\u201D"===b.slice(-2)||"!\u201D"===b.slice(-2)||"?\u201D"===b.slice(-2)?c+=2:b.endsWith(",")||",\u201D"===b.slice(-2)?c+=.75:"(*)"===b&&(c=0),c=.9*c*(125-settings.readingSpeed);const d=c-Date.now()+a;timeoutID=window.setTimeout(()=>{readQuestion(c+a)},d)}else document.getElementById("pause").disabled=!0,settings.timer&&timer.startTimer(DEAD_TIME_LIMIT,revealQuestion)}function revealQuestion(){document.getElementById("question").innerHTML=insertTokensIntoHTML(tossups[questionNumber-1].question,tossups[questionNumber-1].question_sanitized,[[buzzpointIndex]],[" (#) "]),document.getElementById("answer").innerHTML="ANSWER: "+tossups[questionNumber-1].answer,document.getElementById("buzz").disabled=!0,document.getElementById("buzz").textContent="Buzz",document.getElementById("next").disabled=!1,document.getElementById("next").textContent="Next",document.getElementById("start").disabled=!1,document.getElementById("toggle-correct").classList.remove("d-none"),document.getElementById("toggle-correct").textContent=previous.isCorrect?"I was wrong":"I was right"}function toggleCorrect(){const a=previous.isCorrect?-1:1;previous.inPower?(stats.powers+=1*a,stats.points+=a*previous.powerValue):(stats.tens+=1*a,stats.points+=10*a),previous.endOfQuestion?stats.dead+=-1*a:(stats.negs+=-1*a,stats.points+=a*-previous.negValue),stats.totalCorrectCelerity+=a*previous.celerity,previous.isCorrect=!previous.isCorrect,document.getElementById("toggle-correct").textContent=previous.isCorrect?"I was wrong":"I was right",updateStatDisplay(),window.sessionStorage.setItem("tossup-stats",JSON.stringify(stats))}function updateScore(a){const b=0===tossupTextSplit.length,c=tossupTextSplit.includes("(*)")&&tossupText.includes("(*)"),d=isPace(query.setName)?20:15,e=isPace(query.setName)?0:-5,f=a?c?d:10:b?0:e,g=tossupTextSplit.join(" ").length,h=g/tossupText.length;let i;return a?(i=c?"powers":"tens",stats.totalCorrectCelerity+=h):i=b?"dead":"negs",stats[i]+=1,stats.points+=f,previous.celerity=h,previous.endOfQuestion=b,previous.inPower=c,previous.negValue=e,previous.powerValue=d,previous.isCorrect=a,updateStatDisplay(),window.sessionStorage.setItem("tossup-stats",JSON.stringify(stats)),f}/** - * Updates the displayed stat line. - */function updateStatDisplay(){const{powers:a,tens:b,negs:c,dead:d,points:e,totalCorrectCelerity:f}=stats,g=a+b+c+d,h=a+b;let i=0===h?0:parseFloat(f)/h;i=Math.round(1e3*i)/1e3;const j=1===g?"":"s";// disable clear stats button if no stats -document.getElementById("statline").innerHTML=`${a}/${b}/${c} with ${g} tossup${j} seen (${e} pts, celerity: ${i})`,document.getElementById("clear-stats").disabled=0===g}document.getElementById("answer-form").addEventListener("submit",function(a){a.preventDefault(),a.stopPropagation(),settings.timer&&(timer.stopTimer(),timer.tenthsRemaining=0,timer.updateDisplay());const b=document.getElementById("answer-input").value;document.getElementById("answer-input").value="",document.getElementById("answer-input").blur(),document.getElementById("answer-input").placeholder="Enter answer",document.getElementById("answer-input-group").classList.add("d-none"),giveAnswer(b)}),document.getElementById("buzz").addEventListener("click",function(){// reveal answer on second click -// when NOT using type to answer -return this.blur(),currentlyBuzzing?(currentlyBuzzing=!1,updateScore(!0),void revealQuestion()):void(buzz(),settings.typeToAnswer&&(document.getElementById("answer-input-group").classList.remove("d-none"),document.getElementById("answer-input").focus(),this.disabled=!0))}),document.getElementById("clear-stats").addEventListener("click",function(){this.blur(),clearStats()}),document.getElementById("next").addEventListener("click",function(){this.blur(),createTossupCard(tossups[questionNumber-1]),next()}),document.getElementById("packet-number").addEventListener("change",function(){// if field is blank, store blank result in `query` -query.packetNumbers=rangeToArray(this.value.trim(),0),window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query)),query.packetNumbers=rangeToArray(this.value.trim(),maxPacketNumber)}),document.getElementById("pause").addEventListener("click",function(){this.blur(),pause()}),document.getElementById("reading-speed").addEventListener("input",function(){settings.readingSpeed=this.value,document.getElementById("reading-speed-display").textContent=this.value,window.localStorage.setItem("singleplayer-tossup-settings",JSON.stringify(settings))}),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(){query.setName=this.value.trim(),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").placeholder=""===this.value||0===maxPacketNumber?"Packet Numbers":`Packet Numbers (1-${maxPacketNumber})`,window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query))}),document.getElementById("start").addEventListener("click",async function(){if(this.blur(),0===query.setName.length&&settings.selectBySetName)return window.alert("Please enter a set name."),!1;if(0===query.packetNumbers.length&&settings.selectBySetName&&(query.packetNumbers=rangeToArray(document.getElementById("packet-number").value.trim(),maxPacketNumber)),document.getElementById("next").disabled=!1,document.getElementById("next").textContent="Skip",document.getElementById("settings").classList.add("d-none"),settings.selectBySetName){queryLock(),questionNumber=0;try{tossups=await api.getPacketTossups(query.setName,query.packetNumbers[0])}finally{queryUnlock()}}next()}),document.getElementById("toggle-correct").addEventListener("click",function(){this.blur(),toggleCorrect()}),document.getElementById("toggle-powermark-only").addEventListener("click",function(){this.blur(),query.powermarkOnly=this.checked,loadRandomTossups(query),window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query))}),document.getElementById("toggle-select-by-set-name").addEventListener("click",function(){this.blur(),settings.selectBySetName=this.checked,document.getElementById("toggle-powermark-only").disabled=this.checked,document.getElementById("toggle-standard-only").disabled=this.checked,this.checked?(document.getElementById("difficulty-settings").classList.add("d-none"),document.getElementById("set-settings").classList.remove("d-none")):(document.getElementById("difficulty-settings").classList.remove("d-none"),document.getElementById("set-settings").classList.add("d-none")),window.localStorage.setItem("singleplayer-tossup-settings",JSON.stringify(settings))}),document.getElementById("toggle-show-history").addEventListener("click",function(){this.blur(),settings.showHistory=this.checked,this.checked?document.getElementById("room-history").classList.remove("d-none"):document.getElementById("room-history").classList.add("d-none"),window.localStorage.setItem("singleplayer-tossup-settings",JSON.stringify(settings))}),document.getElementById("toggle-standard-only").addEventListener("click",function(){this.blur(),query.standardOnly=this.checked,loadRandomTossups(query),window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query))}),document.getElementById("toggle-timer").addEventListener("click",function(){this.blur(),settings.timer=this.checked,document.getElementById("timer").classList.toggle("d-none"),window.localStorage.setItem("singleplayer-tossup-settings",JSON.stringify(settings))}),document.getElementById("type-to-answer").addEventListener("click",function(){this.blur(),settings.typeToAnswer=this.checked,document.getElementById("toggle-rebuzz").disabled=!this.checked,window.localStorage.setItem("singleplayer-tossup-settings",JSON.stringify(settings))}),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-rebuzz").addEventListener("click",function(){this.blur(),settings.rebuzz=this.checked,window.localStorage.setItem("singleplayer-tossup-settings",JSON.stringify(settings))}),document.getElementById("year-range-a").onchange=function(){query.minYear=$("#slider").slider("values",0),query.maxYear=$("#slider").slider("values",1),loadRandomTossups(query),window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query))},document.getElementById("year-range-b").onchange=function(){query.minYear=$("#slider").slider("values",0),query.maxYear=$("#slider").slider("values",1),loadRandomTossups(query),window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query))},document.addEventListener("keydown",a=>{if(!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName))switch(a.key){case" ":document.getElementById("buzz").click(),a.target===document.body&&a.preventDefault();break;case"e":document.getElementById("toggle-settings").click();break;case"k":document.getElementsByClassName("card-header-clickable")[0].click();break;case"t":document.getElementsByClassName("star-tossup")[0].click();break;case"y":navigator.clipboard.writeText(tossups[0]?._id??"");break;case"n":document.getElementById("next").click();break;case"p":document.getElementById("pause").click();break;case"s":document.getElementById("start").click()}}),$(document).ready(function(){$("#slider").slider("values",0,query.minYear),$("#slider").slider("values",1,query.maxYear)}),document.getElementById("year-range-a").textContent=query.minYear,document.getElementById("year-range-b").textContent=query.maxYear,ReactDOM.createRoot(document.getElementById("category-modal-root")).render(/*#__PURE__*/React.createElement(CategoryModal,{categoryManager:categoryManager,onClose:()=>{({categories:query.categories,subcategories:query.subcategories,alternateSubcategories:query.alternateSubcategories}=categoryManager.export()),loadRandomTossups(query),window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query))}})),ReactDOM.createRoot(document.getElementById("difficulty-dropdown-root")).render(/*#__PURE__*/React.createElement(DifficultyDropdown,{startingDifficulties:query.difficulties,onChange:()=>{query.difficulties=getDropdownValues("difficulties"),loadRandomTossups(query),window.localStorage.setItem("singleplayer-tossup-query",JSON.stringify(query))}})); \ No newline at end of file diff --git a/client/singleplayer/tossups.html b/client/singleplayer/tossups/index.html similarity index 99% rename from client/singleplayer/tossups.html rename to client/singleplayer/tossups/index.html index 17a098e7..1023b801 100644 --- a/client/singleplayer/tossups.html +++ b/client/singleplayer/tossups/index.html @@ -259,7 +259,7 @@ - + diff --git a/client/singleplayer/tossups/index.jsx b/client/singleplayer/tossups/index.jsx new file mode 100644 index 00000000..aa965308 --- /dev/null +++ b/client/singleplayer/tossups/index.jsx @@ -0,0 +1,484 @@ +import account from '../../scripts/accounts.js'; +import api from '../../scripts/api/index.js'; +import questionStats from '../../scripts/auth/question-stats.js'; +import audio from '../../audio/index.js'; +// import Player from '../../../quizbowl/Player.js'; +import Player from '../../../quizbowl/Player.js'; +import ClientTossupRoom from '../ClientTossupRoom.js'; +import CategoryManager from '../../../quizbowl/category-manager.js'; +import { arrayToRange, createTossupCard, rangeToArray } from '../../scripts/utilities/index.js'; +import { getDropdownValues } from '../../scripts/utilities/dropdown-checklist.js'; +import CategoryModal from '../../scripts/components/CategoryModal.min.js'; +import DifficultyDropdown from '../../scripts/components/DifficultyDropdown.min.js'; + +let maxPacketNumber = 24; + +const categoryManager = new CategoryManager(); +const queryVersion = '2024-10-11'; +const settingsVersion = '2024-10-11'; +const USER_ID = 'user'; + +const room = new ClientTossupRoom(); +room.players[USER_ID] = new Player(USER_ID); + +const socket = { + send: onmessage, + sendToServer: (message) => room.message(USER_ID, message) +}; +room.sockets[USER_ID] = socket; + +function onmessage (message) { + const data = JSON.parse(message); + switch (data.type) { + case 'buzz': return buzz(data); + case 'clear-stats': return clearStats(data); + case 'end-of-set': return endOfSet(data); + case 'give-answer': return giveAnswer(data); + case 'next': return next(data); + case 'no-questions-found': return noQuestionsFound(data); + case 'pause': return pause(data); + case 'reveal-answer': return revealAnswer(data); + case 'set-categories': return setCategories(data); + case 'set-difficulties': return setDifficulties(data); + case 'set-reading-speed': return setReadingSpeed(data); + case 'set-packet-numbers': return setPacketNumbers(data); + case 'set-set-name': return setSetName(data); + case 'set-year-range': return setYearRange(data); + case 'skip': return next(data); + case 'start': return next(data); + case 'timer-update': return updateTimerDisplay(data.timeRemaining); + case 'toggle-correct': return toggleCorrect(data); + case 'toggle-powermark-only': return togglePowermarkOnly(data); + case 'toggle-rebuzz': return toggleRebuzz(data); + case 'toggle-select-by-set-name': return toggleSelectBySetName(data); + case 'toggle-show-history': return toggleShowHistory(data); + case 'toggle-standard-only': return toggleStandardOnly(data); + case 'toggle-timer': return toggleTimer(data); + case 'toggle-type-to-answer': return toggleTypeToAnswer(data); + case 'update-question': return updateQuestion(data); + } +} + +function buzz ({ timer, userId, username }) { + if (audio.soundEffects) { audio.buzz.play(); } + + const typeToAnswer = document.getElementById('type-to-answer').checked; + if (typeToAnswer) { + document.getElementById('answer-input-group').classList.remove('d-none'); + document.getElementById('answer-input').focus(); + document.getElementById('buzz').disabled = true; + } +} + +function clearStats ({ userId }) { + updateStatDisplay(room.players[userId]); +} + +function endOfSet () { + window.alert('No more questions left'); + document.getElementById('buzz').disabled = true; + document.getElementById('pause').disabled = true; + document.getElementById('next').disabled = true; +} + +async function giveAnswer ({ directive, directedPrompt, perQuestionCelerity, score, tossup, userId }) { + if (directive === 'prompt') { + document.getElementById('answer-input-group').classList.remove('d-none'); + document.getElementById('answer-input').focus(); + document.getElementById('answer-input').placeholder = directedPrompt ? `Prompt: "${directedPrompt}"` : 'Prompt'; + return; + } + + document.getElementById('answer-input').value = ''; + document.getElementById('answer-input').blur(); + document.getElementById('answer-input').placeholder = 'Enter answer'; + document.getElementById('answer-input-group').classList.add('d-none'); + document.getElementById('next').disabled = false; + document.getElementById('pause').disabled = false; + if (room.settings.rebuzz) { + document.getElementById('buzz').disabled = false; + document.getElementById('buzz').textContent = 'Buzz'; + } + + updateStatDisplay(room.players[USER_ID]); + + if (audio.soundEffects && userId === USER_ID) { + if (directive === 'accept' && score > 10) { + audio.power.play(); + } else if (directive === 'accept' && score === 10) { + audio.correct.play(); + } else if (directive === 'reject') { + audio.incorrect.play(); + } + } + + // if (directive !== 'prompt' && userId === USER_ID && await account.getUsername()) { + // questionStats.recordTossup(tossup, score > 0, score, perQuestionCelerity, true); + // } +} + +async function next ({ packetLength, oldTossup, tossup: nextTossup, type }) { + if (type === 'start') { + document.getElementById('next').disabled = false; + document.getElementById('next').textContent = 'Skip'; + document.getElementById('settings').classList.add('d-none'); + } + + if (type === 'next' || type === 'skip') { + createTossupCard(oldTossup); + } + + document.getElementById('answer').textContent = ''; + document.getElementById('question').textContent = ''; + document.getElementById('toggle-correct').textContent = 'I was wrong'; + document.getElementById('toggle-correct').classList.add('d-none'); + + document.getElementById('buzz').textContent = 'Buzz'; + document.getElementById('buzz').disabled = false; + document.getElementById('next').textContent = 'Skip'; + document.getElementById('packet-number-info').textContent = nextTossup.packet.number; + document.getElementById('packet-length-info').textContent = room.query.selectBySetName ? packetLength : '-'; + document.getElementById('pause').textContent = 'Pause'; + document.getElementById('pause').disabled = false; + document.getElementById('question-number-info').textContent = nextTossup.number; + document.getElementById('set-name-info').textContent = nextTossup.set.name; + + if (type === 'next' && await account.getUsername() && document.getElementById('answer').innerHTML) { + const pointValue = room.previous.isCorrect ? (room.previous.inPower ? room.previous.powerValue : 10) : (room.previous.endOfQuestion ? 0 : room.previous.negValue); + questionStats.recordTossup(room.previous.tossup, room.previous.isCorrect, pointValue, room.previous.celerity, false); + } +} + +function noQuestionsFound () { + window.alert('No questions found'); +} + +function pause ({ paused }) { + document.getElementById('buzz').disabled = paused; + document.getElementById('pause').textContent = paused ? 'Resume' : 'Pause'; +} + +function revealAnswer ({ answer, question }) { + document.getElementById('question').innerHTML = question; + document.getElementById('answer').innerHTML = 'ANSWER: ' + answer; + document.getElementById('pause').disabled = true; + + document.getElementById('buzz').disabled = true; + document.getElementById('buzz').textContent = 'Buzz'; + document.getElementById('next').disabled = false; + document.getElementById('next').textContent = 'Next'; + document.getElementById('start').disabled = false; + + document.getElementById('toggle-correct').classList.remove('d-none'); + document.getElementById('toggle-correct').textContent = room.previous.isCorrect ? 'I was wrong' : 'I was right'; +} + +function setCategories ({ alternateSubcategories, categories, subcategories }) { + categoryManager.import(categories, subcategories, alternateSubcategories); + categoryManager.loadCategoryModal(); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...room.query, version: queryVersion })); +} + +function setDifficulties ({ difficulties }) { + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...room.query, version: queryVersion })); +} + +function setPacketNumbers ({ packetNumbers }) { + document.getElementById('packet-number').value = arrayToRange(packetNumbers); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...room.query, version: queryVersion })); +} + +function setReadingSpeed ({ readingSpeed }) { + document.getElementById('reading-speed').value = readingSpeed; + document.getElementById('reading-speed-display').textContent = readingSpeed; + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...room.settings, version: settingsVersion })); +} + +async function setSetName ({ setName }) { + document.getElementById('set-name').value = setName; + // make border red if set name is not in set list + const valid = !setName || api.getSetList().includes(setName); + document.getElementById('set-name').classList.toggle('is-invalid', !valid); + maxPacketNumber = valid ? await api.getNumPackets(setName) : 0; + document.getElementById('packet-number').placeholder = 'Packet Numbers' + (maxPacketNumber ? ` (1-${maxPacketNumber})` : ''); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...room.query, version: queryVersion })); +} + +function setYearRange ({ minYear, maxYear }) { + $('#slider').slider('values', [minYear, maxYear]); + document.getElementById('year-range-a').textContent = minYear; + document.getElementById('year-range-b').textContent = maxYear; + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...room.query, version: queryVersion })); +} + +function toggleCorrect ({ correct, userId }) { + updateStatDisplay(room.players[USER_ID]); + document.getElementById('toggle-correct').textContent = correct ? 'I was wrong' : 'I was right'; +} + +function togglePowermarkOnly ({ powermarkOnly }) { + document.getElementById('toggle-powermark-only').checked = powermarkOnly; + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...room.query, version: queryVersion })); +} + +function toggleRebuzz ({ rebuzz }) { + document.getElementById('toggle-rebuzz').checked = rebuzz; + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...room.settings, version: settingsVersion })); +} + +function toggleSelectBySetName ({ selectBySetName }) { + document.getElementById('difficulty-settings').classList.toggle('d-none', selectBySetName); + document.getElementById('toggle-powermark-only').disabled = selectBySetName; + document.getElementById('toggle-select-by-set-name').checked = selectBySetName; + document.getElementById('toggle-standard-only').disabled = selectBySetName; + document.getElementById('set-settings').classList.toggle('d-none', !selectBySetName); + window.localStorage.setItem('singleplayer-tossup-query', JSON.stringify({ ...room.query, version: queryVersion })); +} + +function toggleShowHistory ({ showHistory }) { + document.getElementById('room-history').classList.toggle('d-none', !showHistory); + document.getElementById('toggle-show-history').checked = showHistory; + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...room.settings, version: settingsVersion })); +} + +function toggleStandardOnly ({ standardOnly }) { + document.getElementById('toggle-standard-only').checked = standardOnly; +} + +function toggleTimer ({ timer }) { + document.getElementById('timer').classList.toggle('d-none', !timer); + document.getElementById('toggle-timer').checked = timer; + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...room.settings, version: settingsVersion })); +} + +function toggleTypeToAnswer ({ typeToAnswer }) { + document.getElementById('type-to-answer').checked = typeToAnswer; + window.localStorage.setItem('singleplayer-tossup-settings', JSON.stringify({ ...room.settings, version: settingsVersion })); +} + +function updateQuestion ({ word }) { + if (word === '(*)') { return; } + document.getElementById('question').innerHTML += word + ' '; +} + +/** + * Updates the displayed stat line. + */ +function 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); +} + +function updateTimerDisplay (time) { + const seconds = Math.floor(time / 10); + const tenths = time % 10; + document.querySelector('.timer .face').innerText = seconds; + document.querySelector('.timer .fraction').innerText = '.' + tenths; +} + +document.getElementById('answer-form').addEventListener('submit', function (event) { + event.preventDefault(); + event.stopPropagation(); + const answer = document.getElementById('answer-input').value; + socket.sendToServer({ type: 'give-answer', givenAnswer: answer }); +}); + +document.getElementById('buzz').addEventListener('click', function () { + this.blur(); + if (audio.soundEffects) audio.buzz.play(); + socket.sendToServer({ type: 'buzz' }); +}); + +document.getElementById('clear-stats').addEventListener('click', function () { + this.blur(); + socket.sendToServer({ type: 'clear-stats' }); +}); + +document.getElementById('next').addEventListener('click', function () { + this.blur(); + if (this.innerHTML === 'Skip') { + socket.sendToServer({ type: 'skip' }); + } else { + socket.sendToServer({ type: 'next' }); + } +}); + +document.getElementById('packet-number').addEventListener('change', function () { + const range = rangeToArray(this.value.trim(), maxPacketNumber); + const invalid = range.some(num => num < 1 || num > maxPacketNumber); + if (invalid) { + document.getElementById('packet-number').classList.add('is-invalid'); + return; + } + + document.getElementById('packet-number').classList.remove('is-invalid'); + socket.sendToServer({ type: 'set-packet-numbers', packetNumbers: range }); +}); + +document.getElementById('pause').addEventListener('click', function () { + this.blur(); + const seconds = parseFloat(document.querySelector('.timer .face').innerText); + const tenths = parseFloat(document.querySelector('.timer .fraction').innerText); + 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('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 () { + socket.sendToServer({ + type: 'set-set-name', + setName: this.value.trim(), + packetNumbers: rangeToArray(document.getElementById('packet-number').value) + }); +}); + +document.getElementById('start').addEventListener('click', function () { + socket.sendToServer({ type: 'start' }); +}); + +document.getElementById('toggle-correct').addEventListener('click', function () { + this.blur(); + socket.sendToServer({ type: 'toggle-correct', correct: this.textContent === 'I was right' }); +}); + +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-select-by-set-name').addEventListener('click', function () { + this.blur(); + socket.sendToServer({ + type: 'toggle-select-by-set-name', + 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-show-history').addEventListener('click', function () { + this.blur(); + socket.sendToServer({ type: 'toggle-show-history', showHistory: this.checked }); +}); + +document.getElementById('toggle-standard-only').addEventListener('click', function () { + this.blur(); + socket.sendToServer({ type: 'toggle-standard-only', standardOnly: this.checked }); +}); + +document.getElementById('toggle-timer').addEventListener('click', function () { + this.blur(); + socket.sendToServer({ type: 'toggle-timer', timer: this.checked }); +}); + +document.getElementById('type-to-answer').addEventListener('click', function () { + this.blur(); + socket.sendToServer({ type: 'toggle-type-to-answer', typeToAnswer: this.checked }); +}); + +document.getElementById('year-range-a').onchange = function () { + const minYear = $('#slider').slider('values', 0); + const maxYear = $('#slider').slider('values', 1); + socket.sendToServer({ type: 'set-year-range', minYear, maxYear }); +}; + +document.getElementById('year-range-b').onchange = function () { + const minYear = $('#slider').slider('values', 0); + const maxYear = $('#slider').slider('values', 1); + socket.sendToServer({ type: 'set-year-range', minYear, maxYear }); +}; + +document.addEventListener('keydown', (event) => { + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return; + + switch (event.key) { + case ' ': + document.getElementById('buzz').click(); + if (event.target === document.body) { + // Prevent spacebar from scrolling the page: + 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('start').click(); + case 't': return document.getElementsByClassName('star-tossup')[0].click(); + case 'y': return navigator.clipboard.writeText(room.tossup._id ?? ''); + } +}); + +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(); } + categoryManager.import(savedQuery.categories, savedQuery.subcategories, savedQuery.alternateSubcategories); + socket.sendToServer({ type: 'set-packet-numbers', ...savedQuery }); + socket.sendToServer({ type: 'set-set-name', ...savedQuery }); + socket.sendToServer({ type: 'set-year-range', ...savedQuery }); + socket.sendToServer({ type: 'toggle-powermark-only', ...savedQuery }); + socket.sendToServer({ type: 'toggle-select-by-set-name', ...savedQuery }); + socket.sendToServer({ type: 'toggle-standard-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-reading-speed', ...savedSettings }); + socket.sendToServer({ type: 'toggle-rebuzz', ...savedSettings }); + socket.sendToServer({ type: 'toggle-show-history', ...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', ...categoryManager.export() })} + /> +); + +ReactDOM.createRoot(document.getElementById('difficulty-dropdown-root')).render( + socket.sendToServer({ type: 'set-difficulties', difficulties: getDropdownValues('difficulties') })} + /> +); diff --git a/client/singleplayer/tossups/index.min.js b/client/singleplayer/tossups/index.min.js new file mode 100644 index 00000000..4f8f001f --- /dev/null +++ b/client/singleplayer/tossups/index.min.js @@ -0,0 +1,6 @@ +import account from"../../scripts/accounts.js";import api from"../../scripts/api/index.js";import questionStats from"../../scripts/auth/question-stats.js";import audio from"../../audio/index.js";// import Player from '../../../quizbowl/Player.js'; +import Player from"../../../quizbowl/Player.js";import ClientTossupRoom from"../ClientTossupRoom.js";import CategoryManager from"../../../quizbowl/category-manager.js";import{arrayToRange,createTossupCard,rangeToArray}from"../../scripts/utilities/index.js";import{getDropdownValues}from"../../scripts/utilities/dropdown-checklist.js";import CategoryModal from"../../scripts/components/CategoryModal.min.js";import DifficultyDropdown from"../../scripts/components/DifficultyDropdown.min.js";let maxPacketNumber=24;const categoryManager=new CategoryManager,queryVersion="2024-10-11",settingsVersion="2024-10-11",USER_ID="user",room=new ClientTossupRoom;room.players[USER_ID]=new Player(USER_ID);const socket={send:onmessage,sendToServer:a=>room.message(USER_ID,a)};room.sockets[USER_ID]=socket;function onmessage(a){const b=JSON.parse(a);switch(b.type){case"buzz":return buzz(b);case"clear-stats":return clearStats(b);case"end-of-set":return endOfSet(b);case"give-answer":return giveAnswer(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-set-name":return setSetName(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-correct":return toggleCorrect(b);case"toggle-powermark-only":return togglePowermarkOnly(b);case"toggle-rebuzz":return toggleRebuzz(b);case"toggle-select-by-set-name":return toggleSelectBySetName(b);case"toggle-show-history":return toggleShowHistory(b);case"toggle-standard-only":return toggleStandardOnly(b);case"toggle-timer":return toggleTimer(b);case"toggle-type-to-answer":return toggleTypeToAnswer(b);case"update-question":return updateQuestion(b)}}function buzz({timer:a,userId:b,username:c}){audio.soundEffects&&audio.buzz.play();const d=document.getElementById("type-to-answer").checked;d&&(document.getElementById("answer-input-group").classList.remove("d-none"),document.getElementById("answer-input").focus(),document.getElementById("buzz").disabled=!0)}function clearStats({userId:a}){updateStatDisplay(room.players[a])}function endOfSet(){window.alert("No more questions left"),document.getElementById("buzz").disabled=!0,document.getElementById("pause").disabled=!0,document.getElementById("next").disabled=!0}async function giveAnswer({directive:a,directedPrompt:b,perQuestionCelerity:c,score:d,tossup:e,userId:f}){return"prompt"===a?(document.getElementById("answer-input-group").classList.remove("d-none"),document.getElementById("answer-input").focus(),void(document.getElementById("answer-input").placeholder=b?`Prompt: "${b}"`:"Prompt")):void(document.getElementById("answer-input").value="",document.getElementById("answer-input").blur(),document.getElementById("answer-input").placeholder="Enter answer",document.getElementById("answer-input-group").classList.add("d-none"),document.getElementById("next").disabled=!1,document.getElementById("pause").disabled=!1,room.settings.rebuzz&&(document.getElementById("buzz").disabled=!1,document.getElementById("buzz").textContent="Buzz"),updateStatDisplay(room.players[USER_ID]),audio.soundEffects&&f===USER_ID&&("accept"===a&&101>a||a>maxPacketNumber);return b?void document.getElementById("packet-number").classList.add("is-invalid"):void(document.getElementById("packet-number").classList.remove("is-invalid"),socket.sendToServer({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.sendToServer({type:"pause",pausedTime:10*(a+b)})}),document.getElementById("reading-speed").addEventListener("change",function(){socket.sendToServer({type:"set-reading-speed",readingSpeed: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(){socket.sendToServer({type:"set-set-name",setName:this.value.trim(),packetNumbers:rangeToArray(document.getElementById("packet-number").value)})}),document.getElementById("start").addEventListener("click",function(){socket.sendToServer({type:"start"})}),document.getElementById("toggle-correct").addEventListener("click",function(){this.blur(),socket.sendToServer({type:"toggle-correct",correct:"I was right"===this.textContent})}),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-select-by-set-name").addEventListener("click",function(){this.blur(),socket.sendToServer({type:"toggle-select-by-set-name",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-show-history").addEventListener("click",function(){this.blur(),socket.sendToServer({type:"toggle-show-history",showHistory:this.checked})}),document.getElementById("toggle-standard-only").addEventListener("click",function(){this.blur(),socket.sendToServer({type:"toggle-standard-only",standardOnly:this.checked})}),document.getElementById("toggle-timer").addEventListener("click",function(){this.blur(),socket.sendToServer({type:"toggle-timer",timer:this.checked})}),document.getElementById("type-to-answer").addEventListener("click",function(){this.blur(),socket.sendToServer({type:"toggle-type-to-answer",typeToAnswer:this.checked})}),document.getElementById("year-range-a").onchange=function(){const a=$("#slider").slider("values",0),b=$("#slider").slider("values",1);socket.sendToServer({type:"set-year-range",minYear:a,maxYear:b})},document.getElementById("year-range-b").onchange=function(){const a=$("#slider").slider("values",0),b=$("#slider").slider("values",1);socket.sendToServer({type:"set-year-range",minYear:a,maxYear:b})},document.addEventListener("keydown",a=>{if(!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName))switch(a.key){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"n":return document.getElementById("next").click();case"p":return document.getElementById("pause").click();case"s":return document.getElementById("start").click();case"t":return document.getElementsByClassName("star-tossup")[0].click();case"y":return navigator.clipboard.writeText(room.tossup._id??"")}});let startingDifficulties=[];if(window.localStorage.getItem("singleplayer-tossup-query"))try{const a=JSON.parse(window.localStorage.getItem("singleplayer-tossup-query"));if(a.version!==queryVersion)throw new Error;categoryManager.import(a.categories,a.subcategories,a.alternateSubcategories),socket.sendToServer({type:"set-packet-numbers",...a}),socket.sendToServer({type:"set-set-name",...a}),socket.sendToServer({type:"set-year-range",...a}),socket.sendToServer({type:"toggle-powermark-only",...a}),socket.sendToServer({type:"toggle-select-by-set-name",...a}),socket.sendToServer({type:"toggle-standard-only",...a}),startingDifficulties=a.difficulties}catch{window.localStorage.removeItem("singleplayer-tossup-query")}if(window.localStorage.getItem("singleplayer-tossup-settings"))try{const a=JSON.parse(window.localStorage.getItem("singleplayer-tossup-settings"));if(a.version!==settingsVersion)throw new Error;socket.sendToServer({type:"set-reading-speed",...a}),socket.sendToServer({type:"toggle-rebuzz",...a}),socket.sendToServer({type:"toggle-show-history",...a}),socket.sendToServer({type:"toggle-timer",...a}),socket.sendToServer({type:"toggle-type-to-answer",...a})}catch{window.localStorage.removeItem("singleplayer-tossup-settings")}ReactDOM.createRoot(document.getElementById("category-modal-root")).render(/*#__PURE__*/React.createElement(CategoryModal,{categoryManager:categoryManager,onClose:()=>socket.sendToServer({type:"set-categories",...categoryManager.export()})})),ReactDOM.createRoot(document.getElementById("difficulty-dropdown-root")).render(/*#__PURE__*/React.createElement(DifficultyDropdown,{startingDifficulties:startingDifficulties??[],onChange:()=>socket.sendToServer({type:"set-difficulties",difficulties:getDropdownValues("difficulties")})})); \ No newline at end of file diff --git a/constants.js b/constants.js index bd6478a0..e01db0f5 100644 --- a/constants.js +++ b/constants.js @@ -1,70 +1,6 @@ const DEFAULT_QUERY_RETURN_LENGTH = 25; const MAX_QUERY_RETURN_LENGTH = 10000; -const DEFAULT_MIN_YEAR = 2010; -const DEFAULT_MAX_YEAR = 2024; - -const DIFFICULTIES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - -const CATEGORIES = ['Literature', 'History', 'Science', 'Fine Arts', 'Religion', 'Mythology', 'Philosophy', 'Social Science', 'Current Events', 'Geography', 'Other Academic', 'Trash']; -const SUBCATEGORY_TO_CATEGORY = { - 'American Literature': 'Literature', - 'British Literature': 'Literature', - 'Classical Literature': 'Literature', - 'European Literature': 'Literature', - 'World Literature': 'Literature', - 'Other Literature': 'Literature', - 'American History': 'History', - 'Ancient History': 'History', - 'European History': 'History', - 'World History': 'History', - 'Other History': 'History', - Biology: 'Science', - Chemistry: 'Science', - Physics: 'Science', - 'Other Science': 'Science', - 'Visual Fine Arts': 'Fine Arts', - 'Auditory Fine Arts': 'Fine Arts', - 'Other Fine Arts': 'Fine Arts', - Religion: 'Religion', - Mythology: 'Mythology', - Philosophy: 'Philosophy', - 'Social Science': 'Social Science', - 'Current Events': 'Current Events', - Geography: 'Geography', - 'Other Academic': 'Other Academic', - Trash: 'Trash' -}; -const SUBCATEGORIES_FLATTENED = Object.keys(SUBCATEGORY_TO_CATEGORY); - -const ALTERNATE_SUBCATEGORY_TO_CATEGORY = { - Drama: 'Literature', - 'Long Fiction': 'Literature', - Poetry: 'Literature', - 'Short Fiction': 'Literature', - 'Misc Literature': 'Literature', - Math: 'Science', - Astronomy: 'Science', - 'Computer Science': 'Science', - 'Earth Science': 'Science', - Engineering: 'Science', - 'Misc Science': 'Science', - Architecture: 'Fine Arts', - Dance: 'Fine Arts', - Film: 'Fine Arts', - Jazz: 'Fine Arts', - Opera: 'Fine Arts', - Photography: 'Fine Arts', - 'Misc Arts': 'Fine Arts', - Anthropology: 'Social Science', - Economics: 'Social Science', - Linguistics: 'Social Science', - Psychology: 'Social Science', - Sociology: 'Social Science', - 'Other Social Science': 'Social Science' -}; -const ALTERNATE_SUBCATEGORIES_FLATTENED = Object.keys(ALTERNATE_SUBCATEGORY_TO_CATEGORY); - const COOKIE_MAX_AGE = 1000 * 60 * 60 * 24 * 7; // 7 days const WEBSOCKET_MAX_PAYLOAD = 1024 * 10 * 1; // 10 KB @@ -73,14 +9,6 @@ const QBREADER_EMAIL_ADDRESS = 'noreply@qbreader.org'; export { DEFAULT_QUERY_RETURN_LENGTH, MAX_QUERY_RETURN_LENGTH, - DEFAULT_MIN_YEAR, - DEFAULT_MAX_YEAR, - DIFFICULTIES, - CATEGORIES, - SUBCATEGORY_TO_CATEGORY, - ALTERNATE_SUBCATEGORY_TO_CATEGORY, - ALTERNATE_SUBCATEGORIES_FLATTENED, - SUBCATEGORIES_FLATTENED, COOKIE_MAX_AGE, WEBSOCKET_MAX_PAYLOAD, QBREADER_EMAIL_ADDRESS diff --git a/database/qbreader/get-frequency-list.js b/database/qbreader/get-frequency-list.js index 52510879..a8a9991a 100644 --- a/database/qbreader/get-frequency-list.js +++ b/database/qbreader/get-frequency-list.js @@ -1,5 +1,5 @@ import { bonuses, tossups } from './collections.js'; -import { DIFFICULTIES } from '../../constants.js'; +import { DIFFICULTIES } from '../../quizbowl/constants.js'; import mergeTwoSortedArrays from '../../server/merge-two-sorted-arrays.js'; /** diff --git a/database/qbreader/get-random-bonuses.js b/database/qbreader/get-random-bonuses.js index 26070f02..2bd9c9e5 100644 --- a/database/qbreader/get-random-bonuses.js +++ b/database/qbreader/get-random-bonuses.js @@ -1,6 +1,6 @@ import { bonuses } from './collections.js'; -import { DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED, DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR } from '../../constants.js'; +import { DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED, DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR } from '../../quizbowl/constants.js'; // eslint-disable-next-line no-unused-vars import * as types from '../../types.js'; diff --git a/database/qbreader/get-random-tossups.js b/database/qbreader/get-random-tossups.js index a4be6a1b..fa45b19b 100644 --- a/database/qbreader/get-random-tossups.js +++ b/database/qbreader/get-random-tossups.js @@ -1,6 +1,6 @@ import { tossups } from './collections.js'; -import { DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED, DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR } from '../../constants.js'; +import { DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED, DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR } from '../../quizbowl/constants.js'; // eslint-disable-next-line no-unused-vars import * as types from '../../types.js'; diff --git a/database/qbreader/get-set.js b/database/qbreader/get-set.js index e985a027..52d5a3f5 100644 --- a/database/qbreader/get-set.js +++ b/database/qbreader/get-set.js @@ -1,6 +1,6 @@ import { tossups, bonuses } from './collections.js'; -import { CATEGORIES, SUBCATEGORIES_FLATTENED } from '../../constants.js'; +import { CATEGORIES, SUBCATEGORIES_FLATTENED } from '../../quizbowl/constants.js'; // eslint-disable-next-line no-unused-vars import * as types from '../../types.js'; diff --git a/database/qbreader/update-subcategory.js b/database/qbreader/update-subcategory.js index e8a3f554..1cb39b7d 100644 --- a/database/qbreader/update-subcategory.js +++ b/database/qbreader/update-subcategory.js @@ -1,6 +1,6 @@ import { tossups, tossupData, bonuses, bonusData } from './collections.js'; -import { SUBCATEGORY_TO_CATEGORY } from '../../constants.js'; +import { SUBCATEGORY_TO_CATEGORY } from '../../quizbowl/constants.js'; /** * diff --git a/server/multiplayer/Player.js b/quizbowl/Player.js similarity index 84% rename from server/multiplayer/Player.js rename to quizbowl/Player.js index 494b1510..f4647ddf 100644 --- a/server/multiplayer/Player.js +++ b/quizbowl/Player.js @@ -1,11 +1,9 @@ -import { USERNAME_MAX_LENGTH } from './constants.js'; - class Player { - constructor (userId) { + constructor (userId, MAX_USERNAME_LENGTH = undefined) { this.userId = userId; + this.MAX_USERNAME_LENGTH = MAX_USERNAME_LENGTH; this.username = ''; - this.online = true; this.powers = 0; this.tens = 0; @@ -68,12 +66,11 @@ class Player { /** * Safely update the player's username, and return the new username. * @param {string} username - * @param {number} [maxLength=USERNAME_MAX_LENGTH] * @returns {string} newUsername */ - updateUsername (username, maxLength = USERNAME_MAX_LENGTH) { + safelySetUsername (username) { if (!username) { username = ''; } - username = username.substring(0, maxLength); + username = username.substring(0, this.MAX_USERNAME_LENGTH); this.username = username; return this.username; } diff --git a/quizbowl/Room.js b/quizbowl/Room.js new file mode 100644 index 00000000..029ecc92 --- /dev/null +++ b/quizbowl/Room.js @@ -0,0 +1,58 @@ +export default class Room { + /** + * @param {string} name - The name of the room + */ + constructor (name) { + this.name = name; + + this.players = {}; + this.sockets = {}; + this.timer = { + interval: null, + timeRemaining: 0 + }; + } + + async message (userId, message) { throw new Error('Not implemented'); } + + /** + * Sends a message to all sockets + * @param {{}} message + */ + emitMessage (message) { + message = JSON.stringify(message); + for (const socket of Object.values(this.sockets)) { + socket.send(message); + } + } + + /** + * Sends a message to a socket at a specific userId + * @param {string} userId + * @param {{}} message + */ + sendToSocket (userId, message) { + message = JSON.stringify(message); + this.sockets[userId].send(message); + } + + /** + * @param {number} time - time in ticks, where 10 ticks = 1 second + * @param {(time: number) => void} ontick - called every tick + * @param {() => void} callback - called when timer is up + * @returns {void} + */ + startServerTimer (time, ontick, callback) { + clearInterval(this.timer.interval); + this.timer.timeRemaining = time; + + this.timer.interval = setInterval(() => { + if (this.timer.timeRemaining <= 0) { + clearInterval(this.timer.interval); + callback(); + } + ontick(this.timer.timeRemaining); + this.timer.timeRemaining--; + }, 100); + } +} diff --git a/server/multiplayer/TossupRoom.js b/quizbowl/TossupRoom.js similarity index 60% rename from server/multiplayer/TossupRoom.js rename to quizbowl/TossupRoom.js index b2ffda9b..5e25aaca 100644 --- a/server/multiplayer/TossupRoom.js +++ b/quizbowl/TossupRoom.js @@ -1,28 +1,8 @@ -import { PERMANENT_ROOMS, ROOM_NAME_MAX_LENGTH } from './constants.js'; +import { DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR, CATEGORIES, SUBCATEGORIES_FLATTENED, ALTERNATE_SUBCATEGORIES_FLATTENED, SUBCATEGORY_TO_CATEGORY, ALTERNATE_SUBCATEGORY_TO_CATEGORY } from './constants.js'; +import CategoryManager from './category-manager.js'; +import insertTokensIntoHTML from './insert-tokens-into-html.js'; import Room from './Room.js'; -import { DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR, CATEGORIES, SUBCATEGORIES_FLATTENED, ALTERNATE_SUBCATEGORIES_FLATTENED, SUBCATEGORY_TO_CATEGORY, ALTERNATE_SUBCATEGORY_TO_CATEGORY } from '../../constants.js'; -import getRandomTossups from '../../database/qbreader/get-random-tossups.js'; -import getSet from '../../database/qbreader/get-set.js'; -import getSetList from '../../database/qbreader/get-set-list.js'; -import getNumPackets from '../../database/qbreader/get-num-packets.js'; - -import { insertTokensIntoHTML } from '../../client/scripts/utilities/insert-tokens-into-html.js'; - -import checkAnswer from 'qb-answer-checker'; - -import createDOMPurify from 'dompurify'; -import { JSDOM } from 'jsdom'; -import isAppropriateString from '../moderation/is-appropriate-string.js'; -const window = new JSDOM('').window; -const DOMPurify = createDOMPurify(window); - -const QuestionProgressEnum = Object.freeze({ - NOT_STARTED: 0, - READING: 1, - ANSWER_REVEALED: 2 -}); - /** * @returns {Number} The number of points scored on a tossup. */ @@ -32,10 +12,21 @@ function scoreTossup ({ isCorrect, inPower, endOfQuestion, isPace = false }) { return isCorrect ? (inPower ? powerValue : 10) : (endOfQuestion ? 0 : negValue); } -class TossupRoom extends Room { - constructor (name, isPermanent = false, categories = [], subcategories = [], alternateSubcategories = []) { +export default class TossupRoom extends Room { + constructor (name, categories = [], subcategories = [], alternateSubcategories = []) { super(name); - this.isPermanent = isPermanent; + + this.checkAnswer = async function checkAnswer (answerline, givenAnswer) { throw new Error('Not implemented'); }; + this.getRandomTossups = async function getRandomTossups (args) { throw new Error('Not implemented'); }; + this.getSet = async function getSet (args) { throw new Error('Not implemented'); }; + this.getSetList = async function getSetList (args) { throw new Error('Not implemented'); }; + this.getNumPackets = async function getNumPackets (setName) { throw new Error('Not implemented'); }; + + this.QuestionProgressEnum = Object.freeze({ + NOT_STARTED: 0, + READING: 1, + ANSWER_REVEALED: 2 + }); this.timeoutID = null; /** @@ -47,10 +38,10 @@ class TossupRoom extends Room { this.buzzes = []; this.buzzpointIndices = []; this.liveAnswer = ''; + this.packetLength = undefined; this.paused = false; this.queryingQuestion = false; - this.questionNumber = 0; - this.questionProgress = QuestionProgressEnum.NOT_STARTED; + this.questionProgress = this.QuestionProgressEnum.NOT_STARTED; this.questionSplit = []; this.tossup = {}; this.wordIndex = 0; @@ -74,9 +65,6 @@ class TossupRoom extends Room { }; this.settings = { - lock: false, - loginRequired: false, - public: true, rebuzz: false, readingSpeed: 50, skip: false, @@ -85,75 +73,13 @@ class TossupRoom extends Room { this.DEAD_TIME_LIMIT = 5; // time to buzz after question is read this.ANSWER_TIME_LIMIT = 10; // time to give answer after buzzing - - getSetList().then(setList => { this.setList = setList; }); - } - - close (userId) { - if (this.buzzedIn === userId) { - this.giveAnswer(userId, ''); - this.buzzedIn = null; - } - this.leave(userId); - } - - connection2 (socket, userId, username, isNew) { - socket.send(JSON.stringify({ - type: 'connection-acknowledged', - userId, - - players: this.players, - isPermanent: this.isPermanent, - - buzzedIn: this.buzzedIn, - canBuzz: this.settings.rebuzz || !this.buzzes.includes(userId), - questionProgress: this.questionProgress, - - settings: this.settings - })); - - socket.send(JSON.stringify({ - type: 'connection-acknowledged-query', - ...this.query - })); - - socket.send(JSON.stringify({ - type: 'connection-acknowledged-tossup', - tossup: this.tossup - })); - - if (this.questionProgress === QuestionProgressEnum.READING) { - socket.send(JSON.stringify({ - type: 'update-question', - word: this.questionSplit.slice(0, this.wordIndex).join(' ') - })); - } - - if (this.questionProgress === QuestionProgressEnum.ANSWER_REVEALED && this.tossup?.answer) { - socket.send(JSON.stringify({ - type: 'reveal-answer', - question: insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, [this.buzzpointIndices], [' (#) ']), - answer: this.tossup.answer - })); - } - - this.emitMessage({ - type: 'join', - isNew, - userId, - username, - user: this.players[userId] - }); } async message (userId, message) { switch (message.type) { case 'buzz': return this.buzz(userId, message); - case 'chat': return this.chat(userId, message); - case 'chat-live-update': return this.chatLiveUpdate(userId, message); case 'clear-stats': return this.clearStats(userId, message); case 'give-answer': return this.giveAnswer(userId, message); - case 'give-answer-live-update': return this.giveAnswerLiveUpdate(userId, message); case 'next': case 'skip': @@ -168,8 +94,6 @@ class TossupRoom extends Room { case 'set-set-name': return this.setSetName(userId, message); case 'set-username': return this.setUsername(userId, message); case 'set-year-range': return this.setYearRange(userId, message); - case 'toggle-lock': return this.toggleLock(userId, message); - case 'toggle-login-required': return this.toggleLoginRequired(userId, message); case 'toggle-powermark-only': return this.togglePowermarkOnly(userId, message); case 'toggle-rebuzz': return this.toggleRebuzz(userId, message); case 'toggle-select-by-set-name': return this.toggleSelectBySetName(userId, message); @@ -180,7 +104,7 @@ class TossupRoom extends Room { } } - adjustQuery (settings, values) { + async adjustQuery (settings, values) { if (settings.length !== values.length) { return; } for (let i = 0; i < settings.length; i++) { @@ -192,14 +116,10 @@ class TossupRoom extends Room { } if (this.query.selectBySetName) { - this.questionNumber = 0; - getSet(this.query).then(set => { - this.setCache = set; - }); + this.setCache = await this.getSet({ setName: this.query.setName, packetNumbers: [this.query.packetNumbers[0]] }); + this.packetLength = this.setCache.length; } else { - getRandomTossups(this.query).then(tossups => { - this.randomQuestionCache = tossups; - }); + this.randomQuestionCache = await this.getRandomTossups({ ...this.query, number: 1 }); } } @@ -207,24 +127,30 @@ class TossupRoom extends Room { this.queryingQuestion = true; if (this.query.selectBySetName) { - if (this.setCache.length === 0) { - this.emitMessage({ type: 'end-of-set' }); - return false; - } else { - this.tossup = this.setCache.pop(); - this.questionNumber = this.tossup.number; + const categoryManager = new CategoryManager(this.query.categories, this.query.subcategories, this.query.alternateSubcategories); + do { + if (this.setCache.length === 0) { + const packetNumber = this.query.packetNumbers.shift(); + if (packetNumber === undefined) { + this.emitMessage({ type: 'end-of-set' }); + return false; + } + this.setCache = await this.getSet({ setName: this.query.setName, packetNumbers: [packetNumber] }); + this.packetLength = this.setCache.length; + } + + this.tossup = this.setCache.shift(); this.query.packetNumbers = this.query.packetNumbers.filter(packetNumber => packetNumber >= this.tossup.packet.number); - } + } while (!categoryManager.isValidCategory(this.tossup)); } else { if (this.randomQuestionCache.length === 0) { - this.randomQuestionCache = await getRandomTossups(this.query); - if (this.randomQuestionCache.length === 0) { + this.randomQuestionCache = await this.getRandomTossups({ ...this.query, number: 20 }); + if (this.randomQuestionCache?.length === 0) { this.tossup = {}; this.emitMessage({ type: 'no-questions-found' }); return false; } } - this.tossup = this.randomQuestionCache.pop(); } @@ -257,34 +183,21 @@ class TossupRoom extends Room { ); } - chat (userId, { message }) { - // prevent chat messages if room is public, since they can still be sent with API - if (this.settings.public || typeof message !== 'string') { return false; } - const username = this.players[userId].username; - this.emitMessage({ type: 'chat', message, username, userId }); - } - - chatLiveUpdate (userId, { message }) { - if (this.settings.public || typeof message !== 'string') { return false; } - const username = this.players[userId].username; - this.emitMessage({ type: 'chat-live-update', message, username, userId }); - } - clearStats (userId) { this.players[userId].clearStats(); this.emitMessage({ type: 'clear-stats', userId }); } - setDifficulties (userId, { value }) { - const invalid = value.some((value) => typeof value !== 'number' || isNaN(value) || value < 0 || value > 10); + setDifficulties (userId, { difficulties }) { + const invalid = difficulties.some((value) => typeof value !== 'number' || isNaN(value) || value < 0 || value > 10); if (invalid) { return false; } const username = this.players[userId].username; - this.emitMessage({ type: 'set-difficulties', username, value }); - this.adjustQuery(['difficulties'], [value]); + this.emitMessage({ type: 'set-difficulties', username, difficulties }); + this.adjustQuery(['difficulties'], [difficulties]); } - giveAnswer (userId, { givenAnswer }) { + async giveAnswer (userId, { givenAnswer }) { if (typeof givenAnswer !== 'string') { return false; } if (this.buzzedIn !== userId) { return false; } @@ -294,11 +207,7 @@ class TossupRoom extends Room { if (Object.keys(this.tossup).length === 0) { return; } - const celerity = this.questionSplit.slice(this.wordIndex).join(' ').length / this.tossup.question.length; - const endOfQuestion = (this.wordIndex === this.questionSplit.length); - const inPower = this.questionSplit.indexOf('(*)') >= this.wordIndex; - const { directive, directedPrompt } = checkAnswer(this.tossup.answer, givenAnswer); - const points = scoreTossup({ isCorrect: directive === 'accept', inPower, endOfQuestion }); + const { celerity, directive, directedPrompt, points } = await this.scoreTossup({ givenAnswer }); switch (directive) { case 'accept': @@ -340,14 +249,6 @@ class TossupRoom extends Room { }); } - giveAnswerLiveUpdate (userId, { message }) { - if (typeof message !== 'string') { return false; } - if (userId !== this.buzzedIn) { return false; } - this.liveAnswer = message; - const username = this.players[userId].username; - this.emitMessage({ type: 'give-answer-live-update', message, username }); - } - leave (userId) { // this.deletePlayer(userId); this.players[userId].online = false; @@ -365,8 +266,7 @@ class TossupRoom extends Room { async next (userId, { type }) { if (this.buzzedIn) { return false; } // prevents skipping when someone has buzzed in if (this.queryingQuestion) { return false; } - if (this.questionProgress === QuestionProgressEnum.READING && !this.settings.skip) { return false; } - if (type === 'skip' && this.wordIndex < 5) { return false; } // prevents spam-skipping bots + if (this.questionProgress === this.QuestionProgressEnum.READING && !this.settings.skip) { return false; } clearTimeout(this.timeoutID); this.buzzedIn = null; @@ -374,17 +274,18 @@ class TossupRoom extends Room { this.buzzpointIndices = []; this.paused = false; - if (this.questionProgress !== QuestionProgressEnum.ANSWER_REVEALED) { this.revealQuestion(); } + if (this.questionProgress !== this.QuestionProgressEnum.ANSWER_REVEALED) { this.revealQuestion(); } + const oldTossup = this.tossup; const hasNextQuestion = await this.advanceQuestion(); this.queryingQuestion = false; if (!hasNextQuestion) { return; } const username = this.players[userId].username; - this.emitMessage({ type, userId, username, tossup: this.tossup }); + this.emitMessage({ type, packetLength: this.packetLength, userId, username, oldTossup, tossup: this.tossup }); this.wordIndex = 0; - this.questionProgress = QuestionProgressEnum.READING; + this.questionProgress = this.QuestionProgressEnum.READING; this.readQuestion(Date.now()); } @@ -408,51 +309,51 @@ class TossupRoom extends Room { this.emitMessage({ type: 'pause', paused: this.paused, username }); } - async setPacketNumbers (userId, { value }) { - const allowedPacketNumbers = await getNumPackets(this.query.setName); - if (value.some((value) => typeof value !== 'number' || value < 1 || value > allowedPacketNumbers)) { return false; } + async scoreTossup ({ givenAnswer }) { + const celerity = this.questionSplit.slice(this.wordIndex).join(' ').length / this.tossup.question.length; + const endOfQuestion = (this.wordIndex === this.questionSplit.length); + const inPower = this.questionSplit.indexOf('(*)') >= this.wordIndex; + const { directive, directedPrompt } = await this.checkAnswer(this.tossup.answer, givenAnswer); + const points = scoreTossup({ isCorrect: directive === 'accept', inPower, endOfQuestion }); + return { celerity, directive, directedPrompt, endOfQuestion, inPower, points }; + } + + async setPacketNumbers (userId, { packetNumbers }) { + const allowedPacketNumbers = await this.getNumPackets(this.query.setName); + if (packetNumbers.some((value) => typeof value !== 'number' || value < 1 || value > allowedPacketNumbers)) { return false; } const username = this.players[userId].username; - this.emitMessage({ type: 'set-packet-numbers', username, value }); - this.adjustQuery(['packetNumbers'], [value]); + this.adjustQuery(['packetNumbers'], [packetNumbers]); + this.emitMessage({ type: 'set-packet-numbers', username, packetNumbers }); } - setReadingSpeed (userId, { value }) { - if (isNaN(value)) { return false; } - if (value > 100) { value = 100; } - if (value < 0) { value = 0; } + setReadingSpeed (userId, { readingSpeed }) { + if (isNaN(readingSpeed)) { return false; } + if (readingSpeed > 100) { readingSpeed = 100; } + if (readingSpeed < 0) { readingSpeed = 0; } - this.settings.readingSpeed = value; + this.settings.readingSpeed = readingSpeed; const username = this.players[userId].username; - this.emitMessage({ type: 'set-reading-speed', username, value }); + this.emitMessage({ type: 'set-reading-speed', username, readingSpeed }); } - async setSetName (userId, { packetNumbers, value }) { - if (typeof value !== 'string') { return; } - if (!this.setList) { return; } - if (!this.setList.includes(value)) { return; } - const maxPacketNumber = await getNumPackets(value); - if (packetNumbers.some((num) => num > maxPacketNumber || num < 1)) { return; } - + async setSetName (userId, { packetNumbers, setName }) { + if (typeof setName !== 'string') { return; } const username = this.players[userId].username; - this.emitMessage({ type: 'set-set-name', username, value }); - this.adjustQuery(['setName', 'packetNumbers'], [value, packetNumbers]); + this.emitMessage({ type: 'set-set-name', username, setName }); + this.adjustQuery(['setName', 'packetNumbers'], [setName, packetNumbers]); } setUsername (userId, { username }) { if (typeof username !== 'string') { return false; } + const oldUsername = this.players[userId].username; + this.players[userId].username = username; + this.emitMessage({ type: 'set-username', userId, oldUsername, newUsername: username }); + } - if (!isAppropriateString(username)) { - this.sendToSocket(userId, { - type: 'force-username', - username: this.players[userId].username, - message: 'Your username contains an inappropriate word, so it has been reverted.' - }); - } else { - const oldUsername = this.players[userId].username; - const newUsername = this.players[userId].updateUsername(username); - this.emitMessage({ type: 'set-username', userId, oldUsername, newUsername }); - } + startServerTimer (time, ontick, callback) { + if (!this.settings.timer) { return; } + super.startServerTimer(time, ontick, callback); } async readQuestion (expectedReadTime) { @@ -492,7 +393,7 @@ class TossupRoom extends Room { revealQuestion () { if (Object.keys(this.tossup).length === 0) return; - this.questionProgress = QuestionProgressEnum.ANSWER_REVEALED; + this.questionProgress = this.QuestionProgressEnum.ANSWER_REVEALED; this.emitMessage({ type: 'reveal-answer', question: insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, [this.buzzpointIndices], [' (#) ']), @@ -500,40 +401,11 @@ class TossupRoom extends Room { }); } - toggleLock (userId, { lock }) { - if (this.settings.public) { return; } - - this.settings.lock = lock; - const username = this.players[userId].username; - this.emitMessage({ type: 'toggle-lock', lock, username }); - } - - toggleLoginRequired (userId, { loginRequired }) { - if (this.settings.public) { return; } - - this.settings.loginRequired = loginRequired; - const username = this.players[userId].username; - this.emitMessage({ type: 'toggle-login-required', loginRequired, username }); - } - togglePowermarkOnly (userId, { powermarkOnly }) { this.query.powermarkOnly = powermarkOnly; const username = this.players[userId].username; - this.emitMessage({ type: 'toggle-powermark-only', powermarkOnly, username }); this.adjustQuery(['powermarkOnly'], [powermarkOnly]); - } - - togglePublic (userId, { public: isPublic }) { - if (this.isPermanent) { return; } - - this.settings.public = isPublic; - this.settings.timer = true; - const username = this.players[userId].username; - if (isPublic) { - this.settings.lock = false; - this.settings.loginRequired = false; - } - this.emitMessage({ type: 'toggle-public', public: isPublic, username }); + this.emitMessage({ type: 'toggle-powermark-only', powermarkOnly, username }); } toggleRebuzz (userId, { rebuzz }) { @@ -543,14 +415,9 @@ class TossupRoom extends Room { } toggleSelectBySetName (userId, { selectBySetName, setName }) { - if (this.isPermanent) { return; } - if (!this.setList) { return; } - if (!this.setList.includes(setName)) { return; } - this.query.selectBySetName = selectBySetName; const username = this.players[userId].username; this.emitMessage({ type: 'toggle-select-by-set-name', selectBySetName, setName, username }); - this.adjustQuery(['setName'], [setName]); } toggleSkip (userId, { skip }) { @@ -562,19 +429,17 @@ class TossupRoom extends Room { toggleStandardOnly (userId, { standardOnly }) { this.query.standardOnly = standardOnly; const username = this.players[userId].username; - this.emitMessage({ type: 'toggle-standard-only', standardOnly, username }); this.adjustQuery(['standardOnly'], [standardOnly]); + this.emitMessage({ type: 'toggle-standard-only', standardOnly, username }); } toggleTimer (userId, { timer }) { - if (this.settings.public) { return; } this.settings.timer = timer; const username = this.players[userId].username; this.emitMessage({ type: 'toggle-timer', timer, username }); } setCategories (userId, { categories, subcategories, alternateSubcategories }) { - if (this.isPermanent) { return; } if (!Array.isArray(categories)) { return; } if (!Array.isArray(subcategories)) { return; } if (!Array.isArray(alternateSubcategories)) { return; } @@ -587,8 +452,8 @@ class TossupRoom extends Room { if (alternateSubcategories.some(sub => !categories.includes(ALTERNATE_SUBCATEGORY_TO_CATEGORY[sub]))) { return; } const username = this.players[userId].username; - this.emitMessage({ type: 'set-categories', categories, subcategories, alternateSubcategories, username }); this.adjustQuery(['categories', 'subcategories', 'alternateSubcategories'], [categories, subcategories, alternateSubcategories]); + this.emitMessage({ type: 'set-categories', categories, subcategories, alternateSubcategories, username }); } setYearRange (userId, { minYear, maxYear }) { @@ -611,31 +476,3 @@ class TossupRoom extends Room { } } } - -const tossupRooms = {}; - -for (const room of PERMANENT_ROOMS) { - const { name, categories, subcategories } = room; - tossupRooms[name] = new TossupRoom(name, true, categories, subcategories); -} - -/** - * Returns the room with the given room name. - * If the room does not exist, it is created. - * @param {String} roomName - * @returns {TossupRoom} - */ -function createAndReturnRoom (roomName, isPrivate = false) { - roomName = DOMPurify.sanitize(roomName); - roomName = roomName?.substring(0, ROOM_NAME_MAX_LENGTH) ?? ''; - - if (!Object.prototype.hasOwnProperty.call(tossupRooms, roomName)) { - const newRoom = new TossupRoom(roomName, false); - newRoom.settings.public = !isPrivate; - tossupRooms[roomName] = newRoom; - } - - return tossupRooms[roomName]; -} - -export { createAndReturnRoom, tossupRooms }; diff --git a/client/scripts/utilities/category-manager.js b/quizbowl/category-manager.js similarity index 97% rename from client/scripts/utilities/category-manager.js rename to quizbowl/category-manager.js index d833cc61..6afbff5f 100644 --- a/client/scripts/utilities/category-manager.js +++ b/quizbowl/category-manager.js @@ -48,11 +48,13 @@ export default class CategoryManager { return { categories: this.categories, subcategories: this.subcategories, - alternateSubcategories: this.alternateSubcategories + alternateSubcategories: this.alternateSubcategories, + percentView: this.percentView, + categoryPercents: this.categoryPercents }; } - import (categories = [], subcategories = [], alternateSubcategories = []) { + import (categories = [], subcategories = [], alternateSubcategories = [], percentView = false, categoryPercents = []) { if (categories.length > 0 && subcategories.length === 0) { categories.forEach(category => { SUBCATEGORIES[category].forEach(subcategory => { diff --git a/quizbowl/constants.js b/quizbowl/constants.js new file mode 100644 index 00000000..25806407 --- /dev/null +++ b/quizbowl/constants.js @@ -0,0 +1,63 @@ +export const DEFAULT_MIN_YEAR = 2010; +export const DEFAULT_MAX_YEAR = 2024; + +export const DIFFICULTIES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + +export const CATEGORIES = ['Literature', 'History', 'Science', 'Fine Arts', 'Religion', 'Mythology', 'Philosophy', 'Social Science', 'Current Events', 'Geography', 'Other Academic', 'Trash']; +export const SUBCATEGORY_TO_CATEGORY = { + 'American Literature': 'Literature', + 'British Literature': 'Literature', + 'Classical Literature': 'Literature', + 'European Literature': 'Literature', + 'World Literature': 'Literature', + 'Other Literature': 'Literature', + 'American History': 'History', + 'Ancient History': 'History', + 'European History': 'History', + 'World History': 'History', + 'Other History': 'History', + Biology: 'Science', + Chemistry: 'Science', + Physics: 'Science', + 'Other Science': 'Science', + 'Visual Fine Arts': 'Fine Arts', + 'Auditory Fine Arts': 'Fine Arts', + 'Other Fine Arts': 'Fine Arts', + Religion: 'Religion', + Mythology: 'Mythology', + Philosophy: 'Philosophy', + 'Social Science': 'Social Science', + 'Current Events': 'Current Events', + Geography: 'Geography', + 'Other Academic': 'Other Academic', + Trash: 'Trash' +}; +export const SUBCATEGORIES_FLATTENED = Object.keys(SUBCATEGORY_TO_CATEGORY); + +export const ALTERNATE_SUBCATEGORY_TO_CATEGORY = { + Drama: 'Literature', + 'Long Fiction': 'Literature', + Poetry: 'Literature', + 'Short Fiction': 'Literature', + 'Misc Literature': 'Literature', + Math: 'Science', + Astronomy: 'Science', + 'Computer Science': 'Science', + 'Earth Science': 'Science', + Engineering: 'Science', + 'Misc Science': 'Science', + Architecture: 'Fine Arts', + Dance: 'Fine Arts', + Film: 'Fine Arts', + Jazz: 'Fine Arts', + Opera: 'Fine Arts', + Photography: 'Fine Arts', + 'Misc Arts': 'Fine Arts', + Anthropology: 'Social Science', + Economics: 'Social Science', + Linguistics: 'Social Science', + Psychology: 'Social Science', + Sociology: 'Social Science', + 'Other Social Science': 'Social Science' +}; +export const ALTERNATE_SUBCATEGORIES_FLATTENED = Object.keys(ALTERNATE_SUBCATEGORY_TO_CATEGORY); diff --git a/client/scripts/utilities/insert-tokens-into-html.js b/quizbowl/insert-tokens-into-html.js similarity index 95% rename from client/scripts/utilities/insert-tokens-into-html.js rename to quizbowl/insert-tokens-into-html.js index aff4f32d..a657d4c2 100644 --- a/client/scripts/utilities/insert-tokens-into-html.js +++ b/quizbowl/insert-tokens-into-html.js @@ -7,7 +7,7 @@ * @param {string[]} arrayOfTokens - an array of tokens to insert into the HTML string. Default is `['', '']` * @returns {string} the dirty string with tokens inserted at the specified positions in the clean string */ -function insertTokensIntoHTML (dirty, clean, arrayOfIndices, arrayOfTokens = ['', '']) { +export default function insertTokensIntoHTML (dirty, clean, arrayOfIndices, arrayOfTokens = ['', '']) { dirty = dirty.replace(/…/g, '...'); const result = []; @@ -76,5 +76,3 @@ function insertTokensIntoHTML (dirty, clean, arrayOfIndices, arrayOfTokens = ['< return result.join('').replace(/〈/g, '<').replace(/〉/g, '>').replace(/\u0267/g, '&'); } - -export { insertTokensIntoHTML }; diff --git a/routes/api/multiplayer.js b/routes/api/multiplayer.js index 5f2ec0ff..56c626f6 100644 --- a/routes/api/multiplayer.js +++ b/routes/api/multiplayer.js @@ -1,4 +1,4 @@ -import { tossupRooms } from '../../server/multiplayer/TossupRoom.js'; +import { tossupRooms } from '../../server/multiplayer/handle-wss-connection.js'; import { Router } from 'express'; const router = Router(); diff --git a/routes/index.js b/routes/index.js index 19da0665..0eb815f2 100644 --- a/routes/index.js +++ b/routes/index.js @@ -33,6 +33,8 @@ router.use('/multiplayer', multiplayerRouter); router.use('/user', userRouter); router.use('/webhook', webhookRouter); +router.use('/quizbowl', express.static('quizbowl')); + router.use(express.static('client', { extensions: ['html'] })); router.use(express.static('node_modules')); diff --git a/server/multiplayer/Room.js b/server/multiplayer/Room.js deleted file mode 100644 index e23ee22a..00000000 --- a/server/multiplayer/Room.js +++ /dev/null @@ -1,95 +0,0 @@ -import { HEADER, ENDC, OKBLUE, OKGREEN } from '../bcolors.js'; -import Player from './Player.js'; -import RateLimit from '../RateLimit.js'; -const rateLimiter = new RateLimit(50, 1000); - -export default class Room { - /** - * @param {string} name - The name of the room - */ - constructor (name) { - this.name = name; - - this.players = {}; - this.sockets = {}; - this.rateLimitExceeded = new Set(); - this.timer = { - interval: null, - timeRemaining: 0 - }; - } - - connection (socket, userId, username) { - console.log(`Connection in room ${HEADER}${this.name}${ENDC} - userId: ${OKBLUE}${userId}${ENDC}, username: ${OKBLUE}${username}${ENDC} - with settings ${OKGREEN}${Object.keys(this.settings).map(key => [key, this.settings[key]].join(': ')).join('; ')};${ENDC}`); - - const isNew = !(userId in this.players); - if (isNew) { this.players[userId] = new Player(userId); } - this.players[userId].online = true; - this.sockets[userId] = socket; - username = this.players[userId].updateUsername(username); - - socket.on('message', message => { - if (rateLimiter(socket) && !this.rateLimitExceeded.has(username)) { - console.log(`Rate limit exceeded for ${username} in room ${this.name}`); - this.rateLimitExceeded.add(username); - return; - } - - try { - message = JSON.parse(message); - } catch (error) { - console.log(`Error parsing message: ${message}`); - return; - } - this.message(userId, message); - }); - - socket.on('close', this.close.bind(this, userId)); - this.connection2(socket, userId, username, isNew); - } - - connection2 (socket, userId, username, isNew) { throw new Error('Not implemented'); } - close (userId) { throw new Error('Not implemented'); } - async message (userId, message) { throw new Error('Not implemented'); } - - /** - * Sends a message to all sockets - * @param {{}} message - */ - emitMessage (message) { - message = JSON.stringify(message); - for (const socket of Object.values(this.sockets)) { - socket.send(message); - } - } - - /** - * Sends a message to a socket at a specific userId - * @param {string} userId - * @param {{}} message - */ - sendToSocket (userId, message) { - message = JSON.stringify(message); - this.sockets[userId].send(message); - } - - /** - * @param {number} time - time in ticks, where 10 ticks = 1 second - * @param {(time: number) => void} ontick - called every tick - * @param {() => void} callback - called when timer is up - * @returns {void} - */ - startServerTimer (time, ontick, callback) { - clearInterval(this.timer.interval); - this.timer.timeRemaining = time; - - this.timer.interval = setInterval(() => { - if (this.timer.timeRemaining <= 0) { - clearInterval(this.timer.interval); - callback(); - } - ontick(this.timer.timeRemaining); - this.timer.timeRemaining--; - }, 100); - } -} diff --git a/server/multiplayer/ServerPlayer.js b/server/multiplayer/ServerPlayer.js new file mode 100644 index 00000000..54ab67d0 --- /dev/null +++ b/server/multiplayer/ServerPlayer.js @@ -0,0 +1,9 @@ +import Player from '../../quizbowl/Player.js'; +import { USERNAME_MAX_LENGTH } from './constants.js'; + +export default class ServerPlayer extends Player { + constructor (userId) { + super(userId, USERNAME_MAX_LENGTH); + this.online = true; + } +} diff --git a/server/multiplayer/ServerTossupRoom.js b/server/multiplayer/ServerTossupRoom.js new file mode 100644 index 00000000..482a74ba --- /dev/null +++ b/server/multiplayer/ServerTossupRoom.js @@ -0,0 +1,213 @@ +import ServerPlayer from './ServerPlayer.js'; +import { HEADER, ENDC, OKBLUE, OKGREEN } from '../bcolors.js'; +import isAppropriateString from '../moderation/is-appropriate-string.js'; +import insertTokensIntoHTML from '../../quizbowl/insert-tokens-into-html.js'; +import TossupRoom from '../../quizbowl/TossupRoom.js'; +import RateLimit from '../RateLimit.js'; + +import getRandomTossups from '../../database/qbreader/get-random-tossups.js'; +import getSet from '../../database/qbreader/get-set.js'; +import getSetList from '../../database/qbreader/get-set-list.js'; +import getNumPackets from '../../database/qbreader/get-num-packets.js'; + +import checkAnswer from 'qb-answer-checker'; + +export default class ServerTossupRoom extends TossupRoom { + constructor (name, isPermanent = false, categories = [], subcategories = [], alternateSubcategories = []) { + super(name, categories, subcategories, alternateSubcategories); + this.isPermanent = isPermanent; + this.checkAnswer = checkAnswer; + this.getNumPackets = getNumPackets; + this.getRandomTossups = getRandomTossups; + this.getSet = getSet; + this.getSetList = getSetList; + + this.rateLimiter = new RateLimit(50, 1000); + this.rateLimitExceeded = new Set(); + this.settings = { + ...this.settings, + lock: false, + loginRequired: false, + public: true + }; + + this.getSetList().then(setList => { this.setList = setList; }); + } + + async message (userId, message) { + switch (message.type) { + case 'chat': return this.chat(userId, message); + case 'chat-live-update': return this.chatLiveUpdate(userId, message); + case 'give-answer-live-update': return this.giveAnswerLiveUpdate(userId, message); + case 'toggle-lock': return this.toggleLock(userId, message); + case 'toggle-login-required': return this.toggleLoginRequired(userId, message); + case 'toggle-public': return this.togglePublic(userId, message); + default: super.message(userId, message); + } + } + + connection (socket, userId, username) { + console.log(`Connection in room ${HEADER}${this.name}${ENDC} - userId: ${OKBLUE}${userId}${ENDC}, username: ${OKBLUE}${username}${ENDC} - with settings ${OKGREEN}${Object.keys(this.settings).map(key => [key, this.settings[key]].join(': ')).join('; ')};${ENDC}`); + + const isNew = !(userId in this.players); + if (isNew) { this.players[userId] = new ServerPlayer(userId); } + this.players[userId].online = true; + this.sockets[userId] = socket; + username = this.players[userId].safelySetUsername(username); + + socket.on('message', message => { + if (this.rateLimiter(socket) && !this.rateLimitExceeded.has(username)) { + console.log(`Rate limit exceeded for ${username} in room ${this.name}`); + this.rateLimitExceeded.add(username); + return; + } + + try { + message = JSON.parse(message); + } catch (error) { + console.log(`Error parsing message: ${message}`); + return; + } + this.message(userId, message); + }); + + socket.on('close', this.close.bind(this, userId)); + + socket.send(JSON.stringify({ + type: 'connection-acknowledged', + userId, + + players: this.players, + isPermanent: this.isPermanent, + + buzzedIn: this.buzzedIn, + canBuzz: this.settings.rebuzz || !this.buzzes.includes(userId), + questionProgress: this.questionProgress, + + settings: this.settings + })); + + socket.send(JSON.stringify({ type: 'connection-acknowledged-query', ...this.query })); + socket.send(JSON.stringify({ type: 'connection-acknowledged-tossup', tossup: this.tossup })); + + if (this.questionProgress === this.QuestionProgressEnum.READING) { + socket.send(JSON.stringify({ + type: 'update-question', + word: this.questionSplit.slice(0, this.wordIndex).join(' ') + })); + } + + if (this.questionProgress === this.QuestionProgressEnum.ANSWER_REVEALED && this.tossup?.answer) { + socket.send(JSON.stringify({ + type: 'reveal-answer', + question: insertTokensIntoHTML(this.tossup.question, this.tossup.question_sanitized, [this.buzzpointIndices], [' (#) ']), + answer: this.tossup.answer + })); + } + + this.emitMessage({ type: 'join', isNew, userId, username, user: this.players[userId] }); + } + + chat (userId, { message }) { + // prevent chat messages if room is public, since they can still be sent with API + if (this.settings.public || typeof message !== 'string') { return false; } + const username = this.players[userId].username; + this.emitMessage({ type: 'chat', message, username, userId }); + } + + chatLiveUpdate (userId, { message }) { + if (this.settings.public || typeof message !== 'string') { return false; } + const username = this.players[userId].username; + this.emitMessage({ type: 'chat-live-update', message, username, userId }); + } + + close (userId) { + if (this.buzzedIn === userId) { + this.giveAnswer(userId, ''); + this.buzzedIn = null; + } + this.leave(userId); + } + + giveAnswerLiveUpdate (userId, { message }) { + if (typeof message !== 'string') { return false; } + if (userId !== this.buzzedIn) { return false; } + this.liveAnswer = message; + const username = this.players[userId].username; + this.emitMessage({ type: 'give-answer-live-update', message, username }); + } + + next (userId, { type }) { + if (type === 'skip' && this.wordIndex < 5) { return false; } // prevents spam-skipping bots + super.next(userId, { type }); + } + + setCategories (userId, { categories, subcategories, alternateSubcategories }) { + if (this.isPermanent) { return; } + super.setCategories(userId, { categories, subcategories, alternateSubcategories }); + } + + async setSetName (userId, { packetNumbers, setName }) { + if (!this.setList) { return; } + if (!this.setList.includes(setName)) { return; } + const maxPacketNumber = await this.getNumPackets(setName); + if (packetNumbers.some((num) => num > maxPacketNumber || num < 1)) { return; } + super.setSetName(userId, { packetNumbers, setName }); + } + + setUsername (userId, { username }) { + if (typeof username !== 'string') { return false; } + + if (!isAppropriateString(username)) { + this.sendToSocket(userId, { + type: 'force-username', + username: this.players[userId].username, + message: 'Your username contains an inappropriate word, so it has been reverted.' + }); + return; + } + + const oldUsername = this.players[userId].username; + const newUsername = this.players[userId].safelySetUsername(username); + this.emitMessage({ type: 'set-username', userId, oldUsername, newUsername }); + } + + toggleLock (userId, { lock }) { + if (this.settings.public) { return; } + this.settings.lock = lock; + const username = this.players[userId].username; + this.emitMessage({ type: 'toggle-lock', lock, username }); + } + + toggleLoginRequired (userId, { loginRequired }) { + if (this.settings.public) { return; } + this.settings.loginRequired = loginRequired; + const username = this.players[userId].username; + this.emitMessage({ type: 'toggle-login-required', loginRequired, username }); + } + + togglePublic (userId, { public: isPublic }) { + if (this.isPermanent) { return; } + this.settings.public = isPublic; + this.settings.timer = true; + const username = this.players[userId].username; + if (isPublic) { + this.settings.lock = false; + this.settings.loginRequired = false; + } + this.emitMessage({ type: 'toggle-public', public: isPublic, username }); + } + + toggleSelectBySetName (userId, { selectBySetName, setName }) { + if (this.isPermanent) { return; } + if (!this.setList) { return; } + if (!this.setList.includes(setName)) { return; } + super.toggleSelectBySetName(userId, { selectBySetName, setName }); + this.adjustQuery(['setName'], [setName]); + } + + toggleTimer (userId, { timer }) { + if (this.settings.public) { return; } + super.toggleTimer(userId, { timer }); + } +} diff --git a/server/multiplayer/constants.js b/server/multiplayer/constants.js index 9f8fb87b..3b229dcd 100644 --- a/server/multiplayer/constants.js +++ b/server/multiplayer/constants.js @@ -1,4 +1,4 @@ -import { CATEGORIES, SUBCATEGORIES_FLATTENED } from '../../constants.js'; +import { CATEGORIES, SUBCATEGORIES_FLATTENED } from '../../quizbowl/constants.js'; export const ROOM_NAME_MAX_LENGTH = 32; export const USERNAME_MAX_LENGTH = 32; diff --git a/server/multiplayer/handle-wss-connection.js b/server/multiplayer/handle-wss-connection.js index e9c19d3e..56b2e2b9 100644 --- a/server/multiplayer/handle-wss-connection.js +++ b/server/multiplayer/handle-wss-connection.js @@ -1,13 +1,43 @@ +import { PERMANENT_ROOMS, ROOM_NAME_MAX_LENGTH } from './constants.js'; +import ServerTossupRoom from './ServerTossupRoom.js'; import { checkToken } from '../authentication.js'; +import getRandomName from '../get-random-name.js'; import hasValidCharacters from '../moderation/has-valid-characters.js'; import isAppropriateString from '../moderation/is-appropriate-string.js'; -import { createAndReturnRoom } from './TossupRoom.js'; - -import getRandomName from '../get-random-name.js'; +import createDOMPurify from 'dompurify'; +import { JSDOM } from 'jsdom'; import url from 'url'; import * as uuid from 'uuid'; +const window = new JSDOM('').window; +const DOMPurify = createDOMPurify(window); + +const tossupRooms = {}; +for (const room of PERMANENT_ROOMS) { + const { name, categories, subcategories } = room; + tossupRooms[name] = new ServerTossupRoom(name, true, categories, subcategories); +} + +/** + * Returns the room with the given room name. + * If the room does not exist, it is created. + * @param {String} roomName + * @returns {TossupRoom} + */ +function createAndReturnRoom (roomName, isPrivate = false) { + roomName = DOMPurify.sanitize(roomName); + roomName = roomName?.substring(0, ROOM_NAME_MAX_LENGTH) ?? ''; + + if (!Object.prototype.hasOwnProperty.call(tossupRooms, roomName)) { + const newRoom = new ServerTossupRoom(roomName, false); + newRoom.settings.public = !isPrivate; + tossupRooms[roomName] = newRoom; + } + + return tossupRooms[roomName]; +} + /** * Handle WebSocket connection * @param {WebSocket} ws @@ -85,3 +115,5 @@ export default function handleWssConnection (ws, req) { } }); } + +export { tossupRooms };