schat2-clive/src/games/trivia.js

231 lines
8.3 KiB
JavaScript
Raw Normal View History

2021-11-06 00:33:52 +00:00
'use strict';
const config = require('config');
const timers = require('timers/promises');
const { decode } = require('html-entities');
2021-11-06 00:33:52 +00:00
const questions = require('../../assets/jeopardy.json');
const shuffle = require('../utils/shuffle');
2022-10-17 02:06:55 +00:00
const style = require('../utils/style');
2022-10-21 03:07:32 +00:00
const getLeaders = require('../utils/get-leaders');
2021-11-06 00:33:52 +00:00
const settings = { ...config.trivia };
const help = {
mode: '\'first\' or \'timeout\'',
rounds: 'rounds per game as a number',
timeout: 'seconds as a number',
};
2021-11-06 00:33:52 +00:00
2022-10-17 02:06:55 +00:00
// let game = null;
const games = new Map();
2021-11-06 00:33:52 +00:00
function scoreRound(context, round) {
2022-10-17 02:06:55 +00:00
const game = games.get(context.room.id);
if (game.answers.size === 0) {
2021-11-06 00:33:52 +00:00
return `No one scored in round ${round + 1}, better luck next time!`;
}
return Array.from(game.answers.values()).map(({ user }) => {
2021-11-09 13:51:37 +00:00
if (user) {
context.setPoints(user, 1);
game.points[user.username] = (game.points[user.username] || 0) + 1;
2021-11-06 00:33:52 +00:00
2022-10-17 02:06:55 +00:00
return `${style.bold(style.cyan(`${config.usernamePrefix}${user.username}`))} gets a point`;
2021-11-09 13:51:37 +00:00
}
return null;
}).filter(Boolean).join(', ');
2021-11-06 00:33:52 +00:00
}
async function playRound(context, round = 0) {
2022-10-17 02:06:55 +00:00
const game = games.get(context.room.id);
2021-11-06 00:33:52 +00:00
const ac = new AbortController(); // eslint-disable-line no-undef
const now = new Date();
game.answers = new Map();
2021-11-06 00:33:52 +00:00
game.round = round;
game.ac = ac;
const question = game.questions[round];
2022-10-21 03:07:32 +00:00
context.sendMessage(`${style.bold(style.pink(`Question ${round + 1}/${game.questions.length}`))} ${style.silver(`(${question.category})`)}: ${question.question}`, context.room.id);
2021-11-06 00:33:52 +00:00
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
if (question.answer.length >= 3) {
2022-10-21 03:07:32 +00:00
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3) * 2} seconds`))} left, first hint for ${style.bold(style.pink(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.slice(0, 1)} ${question.answer.slice(1).replace(/\s/g, '').replace(/[^\s]/g, '_ ').trim()}`)}`, context.room.id);
} else {
// giving the first letter gives too much away, only give the placeholders
2022-10-21 03:07:32 +00:00
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3) * 2} seconds`))} left, first hint for ${style.bold(style.pink(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.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 >= 4) {
2022-10-21 03:07:32 +00:00
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3)} seconds`))} left, second hint for ${style.bold(style.pink(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${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, {
2021-11-06 00:33:52 +00:00
signal: ac.signal,
});
} catch (error) {
// abort expected, probably not an error
2021-11-06 00:33:52 +00:00
}
if (!ac.signal.aborted) {
ac.abort();
}
if (game.stopped) {
2022-10-21 03:07:32 +00:00
context.sendMessage(`The game was stopped by ${style.cyan(`${config.usernamePrefix}${game.stopped.username}`)}. The answer to the last question was: ${style.bold(question.answer)}. Best players: ${getLeaders(game.points)}`, context.room.id);
2022-10-17 02:06:55 +00:00
games.delete(context.room.id);
2021-11-06 00:33:52 +00:00
return;
}
if (game.answers.size === 0) {
2022-10-17 02:06:55 +00:00
context.sendMessage(`${style.bold(style.red('TIME\'S UP!'))} No one guessed the answer: ${style.bold(question.answer)}`, context.room.id);
2021-11-06 00:33:52 +00:00
} else {
const scores = scoreRound(context, round);
if (game.mode === 'first') {
2022-10-17 02:06:55 +00:00
context.sendMessage(`${style.bold(style.yellow(question.fullAnswer || question.answer))} is the right answer, played in ${style.bold(style.green(`${((new Date() - now) / 1000).toFixed(3)}s`))}! ${scores}`, context.room.id);
2021-11-06 00:33:52 +00:00
}
if (game.mode === 'timeout') {
2022-10-17 02:06:55 +00:00
context.sendMessage(`${style.bold(style.red('STOP!'))} The correct answer is ${style.bold(style.green(question.fullAnswer || question.answer))}. ${scores}`, context.room.id);
2021-11-06 00:33:52 +00:00
}
}
if (round < game.questions.length - 1) {
await timers.setTimeout(5000);
if (game.stopped) {
2022-10-21 03:07:32 +00:00
context.sendMessage(`The game was stopped by ${config.usernamePrefix}${game.stopped.username}. The answer to the last question was: ${style.bold(question.answer)}. Best players: ${getLeaders(game.points)}`, context.room.id);
2022-10-17 02:06:55 +00:00
games.delete(context.room.id);
2021-11-06 00:33:52 +00:00
return;
}
playRound(context, round + 1);
return;
}
await timers.setTimeout(2000);
2022-10-21 03:07:32 +00:00
context.sendMessage(`That's the end of the game! Best players: ${getLeaders(game.points)}`, context.room.id);
2021-11-06 00:33:52 +00:00
2022-10-17 02:06:55 +00:00
games.delete(context.room.id);
2021-11-06 00:33:52 +00:00
}
async function start(context) {
const roundQuestions = shuffle(questions, settings.rounds);
2022-10-17 02:06:55 +00:00
games.set(context.room.id, {
2021-11-06 00:33:52 +00:00
round: 0,
questions: roundQuestions,
2022-10-21 03:07:32 +00:00
answers: new Map(),
2021-11-06 00:33:52 +00:00
points: {},
...settings,
2022-10-17 02:06:55 +00:00
});
2021-11-06 00:33:52 +00:00
playRound(context, 0);
}
async function stop(context) {
2022-10-17 02:06:55 +00:00
const game = games.get(context.room.id);
2021-11-06 00:33:52 +00:00
game.stopped = context.user;
game.ac.abort();
}
function onCommand(args, context) {
2022-10-17 02:06:55 +00:00
const game = games.get(context.room.id);
2021-11-06 00:33:52 +00:00
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);
2021-11-06 00:33:52 +00:00
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);
}
const subcommand = context.subcommand?.toLowerCase();
if (subcommand && settings[subcommand]) {
if (args[0]) {
const bounds = config.trivia.bounds[subcommand];
const curatedSetting = typeof settings[subcommand] === 'number' ? Number(args[0]) : args[0];
if (Number.isNaN(curatedSetting)) {
context.sendMessage(`${subcommand} must be a valid number`, context.room.id);
}
if (Array.isArray(bounds) && typeof settings[subcommand] === 'number' && (curatedSetting < bounds[0] || curatedSetting > bounds[1])) {
context.sendMessage(`${subcommand} must be between ${bounds[0]} and ${bounds[1]}`, context.room.id);
return;
}
settings[subcommand] = curatedSetting;
context.sendMessage(`${subcommand} set to ${settings[subcommand]}`, context.room.id);
} else if (help[subcommand]) {
context.sendMessage(`Please give ${help[subcommand]}`, context.room.id);
}
}
2021-11-06 00:33:52 +00:00
}
async function onMessage(message, context) {
2022-10-18 23:24:13 +00:00
const game = games.get(context.room?.id);
2022-10-17 02:06:55 +00:00
if (!game || context.user?.id === config.user?.id) {
2021-11-06 00:33:52 +00:00
return;
}
const { answer, fullAnswer } = game.questions[game.round];
2021-11-06 00:33:52 +00:00
if (new RegExp(answer, 'i').test(decode(message.originalBody || message.body)) && !game.answers.has(context.user.id)) { // resolve HTML entities in case original body is not available, such as &amp; to &
game.answers.set(context.user.id, {
2021-11-06 00:33:52 +00:00
user: context.user,
answer: message.body,
});
if (settings.mode === 'first') {
game.ac.abort();
}
if (settings.mode === 'timeout' && !game.ac.signal.aborted) {
if (message.type === 'message') {
2022-10-17 02:06:55 +00:00
context.sendMessage(`${style.bold(fullAnswer || answer)} is the correct answer! You might want to ${style.bold('/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 {
2022-10-17 02:06:55 +00:00
context.sendMessage(`${style.bold(fullAnswer || answer)} is the correct answer! You will receive a point at the end of the round.`, context.room.id, null, context.user.username);
}
}
2021-11-06 00:33:52 +00:00
}
}
module.exports = {
name: 'Trivia',
onCommand,
onMessage,
2022-10-21 23:23:11 +00:00
help: `Boast your pointless knowledge! Start a game with ${config.prefix}trivia. Available subcommands are :stop, :mode [first|timeout], :rounds [${config.trivia.bounds.rounds[0]}-${config.trivia.bounds.rounds[1]}] and :timeout [${config.trivia.bounds.timeout[0]}-${config.trivia.bounds.timeout[1]}]`,
2021-11-06 00:33:52 +00:00
};