From d061a9a21c7931e0cf3da05cbd81afdd1c15c3c8 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Sat, 8 Jul 2023 22:47:53 -0400 Subject: [PATCH] close #187 --- client/geoword/compare.html | 85 +++++++++++++++++++++++++++ client/geoword/compare.js | 111 ++++++++++++++++++++++++++++++++++++ client/geoword/stats.html | 3 + client/geoword/stats.js | 1 + database/geoword.js | 81 +++++++++++++++----------- routes/api/geoword.js | 31 +++++++++- routes/geoword.js | 13 +++++ 7 files changed, 291 insertions(+), 34 deletions(-) create mode 100644 client/geoword/compare.html create mode 100644 client/geoword/compare.js diff --git a/client/geoword/compare.html b/client/geoword/compare.html new file mode 100644 index 000000000..a6352160c --- /dev/null +++ b/client/geoword/compare.html @@ -0,0 +1,85 @@ + + + + + QB Reader + + + + + + + + + + + + + + + + +
+

+ Compare your stats on () against another player. +

+
+
+ + +
+ +
+
+
+ + + + + + + + + diff --git a/client/geoword/compare.js b/client/geoword/compare.js new file mode 100644 index 000000000..cb3ad8af5 --- /dev/null +++ b/client/geoword/compare.js @@ -0,0 +1,111 @@ +const packetName = window.location.pathname.split('/').pop(); +const packetTitle = titleCase(packetName); +document.getElementById('packet-name').textContent = packetTitle; + +let division; + +fetch('/api/geoword/division-choice?' + new URLSearchParams({ packetName })) + .then(response => response.json()) + .then(data => { + division = data.division; + document.getElementById('division').textContent = division; + }); + +document.getElementById('form').addEventListener('submit', event => { + event.preventDefault(); + + const opponent = document.getElementById('opponent').value; + + fetch('/api/geoword/compare?' + new URLSearchParams({ packetName, division, opponent })) + .then(response => response.json()) + .then(data => { + const { myBuzzes, opponentBuzzes } = data; + + if (myBuzzes.length === 0) { + document.getElementById('root').innerHTML = ` + `; + return; + } + + if (opponentBuzzes.length === 0) { + document.getElementById('root').innerHTML = ` + `; + return; + } + + let myPoints = 0; + let myTossupCount = 0; + let opponentPoints = 0; + let opponentTossupCount = 0; + + let innerHTML = ''; + + for (let i = 0; i < Math.min(myBuzzes.length, opponentBuzzes.length); i++) { + const myBuzz = myBuzzes[i]; + const opponentBuzz = opponentBuzzes[i]; + + if (myBuzz.points > 0 && opponentBuzz.points === 0) { + myPoints += myBuzz.points; + myTossupCount++; + } else if (myBuzz.points === 0 && opponentBuzz.points > 0) { + opponentPoints += opponentBuzz.points; + opponentTossupCount++; + } else if (myBuzz.points > 0 && opponentBuzz.points > 0) { + if (myBuzz.celerity > opponentBuzz.celerity) { + myPoints += myBuzz.points; + myTossupCount++; + } else if (myBuzz.celerity < opponentBuzz.celerity) { + opponentPoints += opponentBuzz.points; + opponentTossupCount++; + } else { + myPoints += myBuzz.points / 2; + myTossupCount++; + opponentPoints += opponentBuzz.points / 2; + opponentTossupCount++; + } + } + + innerHTML += ` +
+
+
+
#${myBuzz.questionNumber}
+
Celerity: ${(myBuzz.celerity ?? 0.0).toFixed(3)}
+
Points: ${myBuzz.points}
+
Given answer: ${escapeHTML(myBuzz.givenAnswer)}
+
+
+
Answer: ${removeParentheses(myBuzz.formatted_answer ?? myBuzz.answer)}
+
Celerity: ${(opponentBuzz.celerity ?? 0.0).toFixed(3)}
+
Points: ${opponentBuzz.points}
+
Given answer: ${escapeHTML(opponentBuzz.givenAnswer)}
+
+
`; + } + + innerHTML = ` +
+
+
Your stats:
+ ${myPoints} points (${myTossupCount} tossups) +
+
+
${escapeHTML(opponent)}'s stats:
+ ${opponentPoints} points (${opponentTossupCount} tossups) +
+
+ ` + innerHTML; + + + + document.getElementById('root').innerHTML = innerHTML; + }); +}); + +function removeParentheses(answer) { + return answer.replace(/[([].*/g, ''); +} diff --git a/client/geoword/stats.html b/client/geoword/stats.html index 522cbc570..5ad06d809 100644 --- a/client/geoword/stats.html +++ b/client/geoword/stats.html @@ -73,6 +73,9 @@

View the tossups from this packet:

+

+ Compare your stats against another player by clicking here. +

diff --git a/client/geoword/stats.js b/client/geoword/stats.js index ff97e4f54..ead09c1b6 100644 --- a/client/geoword/stats.js +++ b/client/geoword/stats.js @@ -1,6 +1,7 @@ const packetName = window.location.pathname.split('/').pop(); const packetTitle = titleCase(packetName); +document.getElementById('compare-link').href = `/geoword/compare/${packetName}`; document.getElementById('packet-name').textContent = packetTitle; fetch('/api/geoword/stats?' + new URLSearchParams({ packetName })) diff --git a/database/geoword.js b/database/geoword.js index 168743a6c..43f139773 100644 --- a/database/geoword.js +++ b/database/geoword.js @@ -98,6 +98,50 @@ async function getAnswer(packetName, division, questionNumber) { } } +/** + * + * @param {String} packetName + * @param {String} division + * @param {ObjectId} user_id + * @param {Boolean} protests - whether to include protests (default: false) + */ +async function getBuzzes(packetName, division, user_id, protests=false) { + const projection = { + _id: 0, + celerity: 1, + points: 1, + questionNumber: 1, + answer: '$tossup.answer', + formatted_answer: '$tossup.formatted_answer', + givenAnswer: 1, + }; + + if (protests) { + projection.pendingProtest = 1; + projection.decision = 1; + projection.reason = 1; + } + + return await buzzes.aggregate([ + { $match: { packetName, division, user_id } }, + { $sort: { questionNumber: 1 } }, + { $lookup: { + from: 'tossups', + let: { questionNumber: '$questionNumber', packetName, division }, + pipeline: [ + { $match: { $expr: { $and: [ + { $eq: ['$questionNumber', '$$questionNumber'] }, + { $eq: ['$packetName', '$$packetName'] }, + { $eq: ['$division', '$$division'] }, + ] } } }, + ], + as: 'tossup', + } }, + { $unwind: '$tossup' }, + { $project: projection }, + ]).toArray(); +} + async function getBuzzCount(packetName, username) { const user_id = await getUserId(username); return await buzzes.countDocuments({ packetName, user_id }); @@ -236,42 +280,12 @@ async function getQuestionCount(packetName, division) { } /** - * @param {Object} params - * @param {String} params.packetName + * @param {String} packetName * @param {ObjectId} user_id */ -async function getUserStats({ packetName, user_id }) { +async function getUserStats(packetName, user_id) { const division = await getDivisionChoiceById(packetName, user_id); - - const buzzArray = await buzzes.aggregate([ - { $match: { packetName, user_id } }, - { $sort: { questionNumber: 1 } }, - { $lookup: { - from: 'tossups', - let: { questionNumber: '$questionNumber', packetName, division }, - pipeline: [ - { $match: { $expr: { $and: [ - { $eq: ['$questionNumber', '$$questionNumber'] }, - { $eq: ['$packetName', '$$packetName'] }, - { $eq: ['$division', '$$division'] }, - ] } } }, - ], - as: 'tossup', - } }, - { $unwind: '$tossup' }, - { $project: { - _id: 0, - celerity: 1, - pendingProtest: 1, - points: 1, - questionNumber: 1, - answer: '$tossup.answer', - formatted_answer: '$tossup.formatted_answer', - givenAnswer: 1, - decision: 1, - reason: 1, - } }, - ]).toArray(); + const buzzArray = await getBuzzes(packetName, division, user_id, true); const leaderboard = await buzzes.aggregate([ { $match: { packetName, division, active: true } }, @@ -391,6 +405,7 @@ export { getAdminStats, getAnswer, getBuzzCount, + getBuzzes, getCost, getDivisionChoice, getDivisions, diff --git a/routes/api/geoword.js b/routes/api/geoword.js index 9a7728eae..4ce9f1ab6 100644 --- a/routes/api/geoword.js +++ b/routes/api/geoword.js @@ -9,6 +9,21 @@ import stripeClass from 'stripe'; const router = Router(); const stripe = new stripeClass(process.env.STRIPE_SECRET_KEY); +router.get('/compare', async (req, res) => { + const { username, token } = req.session; + if (!checkToken(username, token)) { + delete req.session; + res.redirect('/geoword/login'); + return; + } + + const { packetName, division, opponent } = req.query; + const myBuzzes = await geoword.getBuzzes(packetName, division, await getUserId(username)); + const opponentBuzzes = (await geoword.getBuzzes(packetName, division, await getUserId(opponent))).slice(0, myBuzzes.length); + + return res.json({ myBuzzes, opponentBuzzes }); +}); + router.post('/create-payment-intent', async (req, res) => { const { username, token } = req.session; if (!checkToken(username, token)) { @@ -42,6 +57,20 @@ router.get('/check-answer', async (req, res) => { res.json({ actualAnswer: answer, directive, directedPrompt }); }); +router.get('/division-choice', async (req, res) => { + const { username, token } = req.session; + if (!checkToken(username, token)) { + delete req.session; + res.redirect('/geoword/login'); + return; + } + + const { packetName } = req.query; + const division = await geoword.getDivisionChoice(packetName, username); + + res.json({ division }); +}); + router.get('/get-progress', async (req, res) => { const { username, token } = req.session; if (!checkToken(username, token)) { @@ -173,7 +202,7 @@ router.get('/stats', async (req, res) => { const user_id = await getUserId(username); const { packetName } = req.query; - const { buzzArray, division, leaderboard } = await geoword.getUserStats({ packetName, user_id }); + const { buzzArray, division, leaderboard } = await geoword.getUserStats(packetName, user_id); res.json({ buzzArray, division, leaderboard }); }); diff --git a/routes/geoword.js b/routes/geoword.js index b89330724..9fe72194d 100644 --- a/routes/geoword.js +++ b/routes/geoword.js @@ -62,6 +62,19 @@ router.use('/*/:packetName', async (req, res, next) => { next(); }); +router.get('/compare/:packetName', async (req, res) => { + const { username } = req.session; + const packetName = req.params.packetName; + const paid = await geoword.checkPayment({ packetName, username }); + + if (!paid) { + res.redirect('/geoword/payment/' + packetName); + return; + } + + res.sendFile('compare.html', { root: './client/geoword' }); +}); + router.get('/division/:packetName', async (req, res) => { const { username } = req.session; const packetName = req.params.packetName;