213 lines
6.5 KiB
JavaScript
213 lines
6.5 KiB
JavaScript
'use strict';
|
||
|
||
const config = require('config');
|
||
const timers = require('timers/promises');
|
||
const { decode } = require('html-entities');
|
||
|
||
const questions = require('../../assets/jeopardy.json');
|
||
const shuffle = require('../utils/shuffle');
|
||
|
||
const settings = { ...config.trivia };
|
||
const help = {
|
||
mode: '\'first\' or \'timeout\'',
|
||
rounds: 'rounds per game as a number',
|
||
timeout: 'seconds as a number',
|
||
};
|
||
|
||
let game = null;
|
||
|
||
function scoreRound(context, round) {
|
||
if (game.answers.length === 0) {
|
||
return `No one scored in round ${round + 1}, better luck next time!`;
|
||
}
|
||
|
||
return game.answers.map(({ user }) => {
|
||
if (user) {
|
||
context.setPoints(user, 1);
|
||
game.points[user.username] = (game.points[user.username] || 0) + 1;
|
||
|
||
return `**@${user.username}** gets a point`;
|
||
}
|
||
|
||
return null;
|
||
}).filter(Boolean).join(', ');
|
||
}
|
||
|
||
async function playRound(context, round = 0) {
|
||
const ac = new AbortController(); // eslint-disable-line no-undef
|
||
const now = new Date();
|
||
|
||
game.answers = [];
|
||
game.round = round;
|
||
game.ac = ac;
|
||
|
||
const question = game.questions[round];
|
||
|
||
context.sendMessage(`**Question ${round + 1}/${game.questions.length}** (${question.category}): ${question.question}`, context.room.id);
|
||
context.logger.info(`Trivia asked "${question.question}" with answer: ${question.answer}`);
|
||
|
||
try {
|
||
await timers.setTimeout((game.timeout / 3) * 1000, null, {
|
||
signal: ac.signal,
|
||
});
|
||
|
||
// replace space with U+2003 Em Space to separate words, since a single space separates the placeholders, and double spaces are removed during Markdown render
|
||
context.sendMessage(`**${Math.floor(game.timeout / 3) * 2} seconds** left, first hint for **question ${round + 1}/${game.questions.length}**: **${question.answer.slice(0, 1)} ${question.answer.slice(1).replace(/\s/g, ' ').replace(/[^\s]/g, '_ ').trim()}**`, context.room.id);
|
||
|
||
await timers.setTimeout((game.timeout / 3) * 1000, null, {
|
||
signal: ac.signal,
|
||
});
|
||
|
||
if (question.answer.length > 3) {
|
||
context.sendMessage(`**${Math.floor(game.timeout / 3)} seconds** left, second hint for **question ${round + 1}/${game.questions.length}**: **${question.answer.slice(0, 1)} ${question.answer.slice(1, -1).replace(/\s/g, ' ').replace(/[^\s]/g, '_ ')}${question.answer.slice(-1)}**`, context.room.id);
|
||
}
|
||
|
||
await timers.setTimeout((game.timeout / 3) * 1000, null, {
|
||
signal: ac.signal,
|
||
});
|
||
} catch (error) {
|
||
// abort expected, probably not an error
|
||
}
|
||
|
||
if (!ac.signal.aborted) {
|
||
ac.abort();
|
||
}
|
||
|
||
if (game.stopped) {
|
||
context.sendMessage(`The game was stopped by @${game.stopped.username}. The answer to the last question was: **${question.answer}**`, context.room.id);
|
||
game = null;
|
||
|
||
return;
|
||
}
|
||
|
||
if (game.answers.length === 0) {
|
||
context.sendMessage(`**TIME'S UP!** No one guessed the answer: **${question.answer}**`, context.room.id);
|
||
} else {
|
||
const scores = scoreRound(context, round);
|
||
|
||
if (game.mode === 'first') {
|
||
context.sendMessage(`**${question.fullAnswer || question.answer}** is the right answer, played in **${((new Date() - now) / 1000).toFixed(3)}s**! ${scores}`, context.room.id);
|
||
}
|
||
|
||
if (game.mode === 'timeout') {
|
||
context.sendMessage(`**STOP!** The correct answer is **${question.fullAnswer || question.answer}**. ${scores}`, context.room.id);
|
||
}
|
||
}
|
||
|
||
if (round < game.questions.length - 1) {
|
||
await timers.setTimeout(5000);
|
||
|
||
if (game.stopped) {
|
||
context.sendMessage(`The game was stopped by ${game.stopped.username}`, context.room.id);
|
||
game = null;
|
||
|
||
return;
|
||
}
|
||
|
||
playRound(context, round + 1);
|
||
return;
|
||
}
|
||
|
||
await timers.setTimeout(2000);
|
||
|
||
const leaders = Object.entries(game.points).sort(([, scoreA], [, scoreB]) => scoreB - scoreA).map(([username, score], index) => {
|
||
if (index === 0) {
|
||
return `**@${username}** with **${score}** points`;
|
||
}
|
||
|
||
return `**@${username}** with **${score}** points`;
|
||
}).join(', ');
|
||
|
||
context.sendMessage(`That's the end of the game! Best players: ${leaders}`, context.room.id);
|
||
|
||
game = null;
|
||
}
|
||
|
||
async function start(context) {
|
||
const roundQuestions = shuffle(questions, settings.rounds);
|
||
|
||
game = {
|
||
round: 0,
|
||
questions: roundQuestions,
|
||
answers: [],
|
||
points: {},
|
||
...settings,
|
||
};
|
||
|
||
playRound(context, 0);
|
||
}
|
||
|
||
async function stop(context) {
|
||
game.stopped = context.user;
|
||
game.ac.abort();
|
||
}
|
||
|
||
function onCommand(args, context) {
|
||
if (!context.subcommand && !game) {
|
||
start(context);
|
||
return;
|
||
}
|
||
|
||
if (!context.subcommand && game) {
|
||
context.sendMessage(`There is already a game going on! Use ${config.prefix}trivia:stop to reset. The current question for round ${game.round + 1} is: ${game.questions[game.round].question}`, context.room.id);
|
||
return;
|
||
}
|
||
|
||
if (context.subcommand === 'stop' && game) {
|
||
stop(context);
|
||
}
|
||
|
||
if (context.subcommand === 'stop' && !game) {
|
||
context.sendMessage(`There is no game going on at the moment. Start one with ${config.prefix}trivia!`, context.room.id);
|
||
}
|
||
|
||
if (['help', 'commands'].includes(context.subcommand)) {
|
||
context.sendMessage(`Available subcommands for ${config.prefix}trivia are :stop, :mode, :rounds and :timeout`, context.room.id);
|
||
}
|
||
|
||
const subcommand = context.subcommand?.toLowerCase();
|
||
|
||
if (subcommand && settings[subcommand]) {
|
||
if (args[0]) {
|
||
settings[subcommand] = typeof settings[subcommand] === 'number' ? (Number(args[0]) || settings[subcommand]) : args[0];
|
||
|
||
context.sendMessage(`${subcommand} set to ${settings[subcommand]}`, context.room.id);
|
||
} else if (help[subcommand]) {
|
||
context.sendMessage(`Please give ${help[subcommand]}`, context.room.id);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function onMessage(message, context) {
|
||
if (!game || context.user?.id === config.user?.id) {
|
||
return;
|
||
}
|
||
|
||
const { answer, fullAnswer } = game.questions[game.round];
|
||
|
||
if (new RegExp(answer, 'i').test(decode(message.originalBody || message.body))) { // resolve HTML entities in case original body is not available, such as & to &
|
||
game.answers.push({
|
||
user: context.user,
|
||
answer: message.body,
|
||
});
|
||
|
||
if (settings.mode === 'first') {
|
||
game.ac.abort();
|
||
}
|
||
|
||
if (settings.mode === 'timeout') {
|
||
if (message.type === 'message') {
|
||
context.sendMessage(`**${fullAnswer || answer}** is the correct answer! You might want to **/whisper** the answer to me instead, so others can't leech off your impeccable knowledge. You will receive a point at the end of the round.`, context.room.id, null, context.user.username);
|
||
} else {
|
||
context.sendMessage(`**${fullAnswer || answer}** is the correct answer! You will receive a point at the end of the round.`, context.room.id, null, context.user.username);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
name: 'Trivia',
|
||
onCommand,
|
||
onMessage,
|
||
};
|