Added rudimentary Trivia game.

This commit is contained in:
ThePendulum 2021-11-06 01:33:52 +01:00
parent 14fb0f90bf
commit 09f1f5a14a
8 changed files with 231 additions and 5 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v17.0.1

18
assets/curate-jeopardy.js Normal file
View File

@ -0,0 +1,18 @@
'use strict';
const fs = require('fs').promises;
const questions = require('./jeopardy_raw.json');
async function init() {
const curatedQuestions = questions.map((question) => ({
...question,
question: question.question.replace(/^'|'$/g, ''),
}));
await fs.writeFile('assets/jeopardy.json', JSON.stringify(curatedQuestions));
console.log(curatedQuestions);
}
init();

1
assets/jeopardy.json Normal file

File diff suppressed because one or more lines are too long

1
assets/jeopardy_raw.json Executable file

File diff suppressed because one or more lines are too long

View File

@ -14,11 +14,16 @@ module.exports = {
uniqueUsername: true, uniqueUsername: true,
socket: 'ws://127.0.0.1:3000/socket', socket: 'ws://127.0.0.1:3000/socket',
api: 'http://127.0.0.1:3000/api', api: 'http://127.0.0.1:3000/api',
reconnectDelay: 10, // seconds
prefix: '~', prefix: '~',
style: { style: {
color: 'var(--message-56)', color: 'var(--message-56)',
}, },
games: ['mash', 'cursed'],
channels: ['GamesNight'], channels: ['GamesNight'],
reconnectDelay: 10, // seconds games: ['mash', 'trivia'],
trivia: {
mode: 'first', // first or timeout
rounds: 10,
timeout: 30,
},
}; };

View File

@ -44,6 +44,11 @@ async function getWsId(httpSession) {
} }
async function setPoints(gameKey, user, value, mode = 'add') { async function setPoints(gameKey, user, value, mode = 'add') {
if (!user) {
logger.warn(`Failed to set ${gameKey} points for missing user`);
return;
}
const userKey = `${user.id}:${user.username}`; const userKey = `${user.id}:${user.username}`;
if (!points[gameKey]) { if (!points[gameKey]) {
@ -85,7 +90,7 @@ function getLeaderboard(game, { user, room }) {
.slice(-10) .slice(-10)
.join(', '); .join(', ');
game.sendMessage(`The top ${Math.min(Object.keys(curatedLeaderboard).length, 10)} ${game.name} players are ${curatedLeaderboard}`, room.id); game.sendMessage(`The top ${Math.min(Object.keys(curatedLeaderboard).length, 10)} *${game.name}* players are: ${curatedLeaderboard}`, room.id);
} }
function onConnect(data, bot) { function onConnect(data, bot) {
@ -109,6 +114,19 @@ function onRooms({ rooms, users }, bot) {
}); });
} }
/* eslint-disable no-param-reassign */
function onJoin(data, bot) {
bot.users[data.user.id] = data.user;
if (!bot.rooms[data.roomId].includes(data.user.id)) {
bot.rooms[data.roomId].push(data.user.id);
}
}
function onLeave(data, bot) {
bot.rooms[data.roomId].users = bot.rooms[data.roomId].users.filter((userId) => userId !== data.userId);
}
function onMessage(message, bot, games) { function onMessage(message, bot, games) {
const [, command, subcommand] = message.body?.match(new RegExp(`^${config.prefix}(\\w+)(?:\\:(\\w+))?`)) || []; const [, command, subcommand] = message.body?.match(new RegExp(`^${config.prefix}(\\w+)(?:\\:(\\w+))?`)) || [];
const user = bot.users[message.userId]; const user = bot.users[message.userId];
@ -160,6 +178,8 @@ const messageHandlers = {
connect: onConnect, connect: onConnect,
rooms: onRooms, rooms: onRooms,
message: onMessage, message: onMessage,
join: onJoin,
leave: onLeave,
}; };
async function initPoints() { async function initPoints() {
@ -179,7 +199,7 @@ async function initPoints() {
function getGames(bot) { function getGames(bot) {
const games = config.games.reduce((acc, key) => { const games = config.games.reduce((acc, key) => {
const game = require(`./games/${key}`); // eslint-disable-line global-require, import/no-dynamic-require const game = require(`./games/${key.game || key}`); // eslint-disable-line global-require, import/no-dynamic-require
const sendMessage = (body, roomId) => { const sendMessage = (body, roomId) => {
bot.socket.transmit('message', { bot.socket.transmit('message', {
@ -195,6 +215,7 @@ function getGames(bot) {
...acc, ...acc,
[key]: { [key]: {
...game, ...game,
...(key.game && key),
name: game.name || key, name: game.name || key,
key, key,
sendMessage, sendMessage,

View File

@ -48,7 +48,9 @@ function start(length, context, attempt = 0) {
context.logger.info(`Mash started, '${anagram}' with answers ${answers.map((answer) => `'${answer.word}'`).join(', ')}`); context.logger.info(`Mash started, '${anagram}' with answers ${answers.map((answer) => `'${answer.word}'`).join(', ')}`);
} }
function play(word, context) { function play(rawWord, context) {
const word = rawWord.toLowerCase();
if (word.length !== mash.key.length) { if (word.length !== mash.key.length) {
context.sendMessage(`Your answer needs to be ${mash.key.length} letters, @${context.user.username}`, context.room.id); context.sendMessage(`Your answer needs to be ${mash.key.length} letters, @${context.user.username}`, context.room.id);
return; return;

177
src/games/trivia.js Normal file
View File

@ -0,0 +1,177 @@
'use strict';
const config = require('config');
const timers = require('timers/promises');
const questions = require('../../assets/jeopardy.json');
const settings = {
rounds: config.trivia.rounds,
timeout: config.trivia.timeout,
mode: config.trivia.mode,
};
let game = null;
function shuffle(unshuffledQuestions, limit = 10) {
const shuffled = unshuffledQuestions;
for (let i = shuffled.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
const temp = shuffled[i];
shuffled[i] = shuffled[j];
shuffled[j] = temp;
}
return shuffled.slice(0, limit);
}
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 }) => {
context.setPoints(user, 1);
game.points[user.username] = (game.points[user.username] || 0) + 1;
return `**@${user.username}** gets a point`;
}).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.question}`, context.room.id);
context.logger.info(`Trivia asked "${question.question}" with answer: ${question.answer}`);
try {
await timers.setTimeout(game.timeout * 1000, null, {
signal: ac.signal,
});
} catch (error) {
// abort expected, 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.answer}** is the right answer after **${((new Date() - now) / 1000).toFixed(3)}s**! ${scores}`, context.room.id);
}
if (game.mode === 'timeout') {
context.sendMessage(`**STOP!** The correct answer is **${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;
}
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! 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);
}
}
async function onMessage(message, context) {
if (!game) {
return;
}
const { answer } = game.questions[game.round];
if (new RegExp(answer, 'i').test(message.body)) {
game.answers.push({
user: context.user,
answer: message.body,
});
if (settings.mode === 'first') {
game.ac.abort();
}
}
}
module.exports = {
name: 'Trivia',
onCommand,
onMessage,
};