schat2-clive/src/games/trivia.js

213 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 &amp; 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,
};