Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a trivia game #19

Merged
merged 24 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ba6a17c
Add trivia functionality without checking answers
0xVR Aug 28, 2020
78b847e
Implement answer checking and use of tempDB
0xVR Aug 28, 2020
c512b3a
Add comments and replace hardcoded ID
0xVR Aug 31, 2020
2c03789
replace find with get
0xVR Aug 31, 2020
dca1434
Add trivia functionality without checking answers
0xVR Aug 28, 2020
a2724f0
Implement answer checking and use of tempDB
0xVR Aug 28, 2020
436a7f5
Add comments and replace hardcoded ID
0xVR Aug 31, 2020
11e7342
replace find with get
0xVR Aug 31, 2020
533e659
Merge branch 'feature/trivia' of https://github.com/VRTheDerp/IveBot …
0xVR Aug 31, 2020
24bacbd
Clean up trivia lists
0xVR Sep 24, 2020
d381ce8
Move TriviaSession to trivia.ts, exclude triviaLists, and document /t…
0xVR Jun 24, 2021
dfcb8fc
Apply suggestions from code review
0xVR Jul 24, 2021
2675409
Fix bugs with /trivia and remove redundant code
0xVR Jul 24, 2021
f54b6a6
Merge branch 'master' into feature/trivia
retrixe Jul 26, 2021
074f83e
Apply suggestions from code review
0xVR Jul 28, 2021
d9eb41b
Use fs.promises and move constants
0xVR Jul 29, 2021
902caea
Apply suggestions from code review
0xVR Jul 29, 2021
0b8e058
Merge branch 'master' into feature/trivia
retrixe Jul 29, 2021
fef4e53
Fix broken yarn.lock from merge
retrixe Jul 29, 2021
af379b8
Remove redundant code and fix bugs following code review
0xVR Jul 30, 2021
7d044d7
Apply suggestions from code review
0xVR Jul 30, 2021
866e672
Apply suggestions from code review
0xVR Jul 30, 2021
4940ea8
Fix errors and use more type inference.
retrixe Jul 30, 2021
64e7b4d
Add TODOs, use template strings.
retrixe Jul 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ typings/

# dotenv environment variables file
.env

# trivia lists
triviaLists/
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ It's planned to have some nifty commands like /assistant which directly communic
- `/randomword`
- `/choose`
- `/reverse`
- `/trivia`
- `/8ball`
- `/repeat`
- `/calculate`
Expand Down Expand Up @@ -83,6 +84,8 @@ It's planned to have some nifty commands like /assistant which directly communic

Set up a MongoDB instance and note its URL. You can set it to store its data in `database` within this folder (you must first make the folder before starting MongoDB)

Get the trivia lists zip from [here](https://siasky.net/nAHYx0Qe7NFag-RuMZTSGizq5ral6Q6m6BZrSHKzzx7r_g) and extract it so that the triviaLists folder is in the top-level directory.
retrixe marked this conversation as resolved.
Show resolved Hide resolved

Make a file named `config.json5` in the top-level directory. It should be something like this:

```json
Expand Down
1 change: 1 addition & 0 deletions server/bot/commands/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const generalHelp = {
\`/randomword\` - Returns a random word.
\`/choose\` - Choose between multiple options.
\`/reverse\` - Reverse a sentence.
\`/trivia\` - Start a trivia session.
0xVR marked this conversation as resolved.
Show resolved Hide resolved
\`/8ball\` - Random answers to random questions.
\`/repeat\` - Repeat a string.
\`/calculate\` - Calculate an expression.
Expand Down
250 changes: 250 additions & 0 deletions server/bot/commands/trivia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { Message, GuildTextableChannel, TextableChannel, User, EmbedOptions, Client } from 'eris'
import { Command, DB } from '../imports/types'
import { getInsult } from '../imports/tools'
import fs from 'fs'

async function parseTriviaList (fileName: string) {
const data = await fs.promises.readFile(`./triviaLists/${fileName}.txt`, 'utf8')
const triviaList = new Map<string, string[]>()
data.split('\n').forEach(el => {
const splitEl = el.split('`').map(ans => ans.trim()).filter(ans => !!ans)
if (splitEl.length >= 2) triviaList.set(splitEl[0], splitEl.slice(1))
})
return triviaList
}

export class TriviaSession {
settings: { maxScore: number, timeout: number, timeLimit: number, botPlays: boolean, revealAnswer: boolean }
currentQuestion: [string, string[]]
channel: TextableChannel
author: User
retrixe marked this conversation as resolved.
Show resolved Hide resolved
message: Message
questionList: Map<string, string[]>
scores: { [id: string]: number } = {}
0xVR marked this conversation as resolved.
Show resolved Hide resolved
stopped: boolean = false
timer: number = null
timeout: number = Date.now()
count: number = 0
tempDB: DB
client: Client

constructor (triviaList: Map<string, string[]>, message: Message, botPlays: boolean, timeLimit: number, maxScore: number, revealAnswer: boolean, tempDB: DB, client: Client) {
this.channel = message.channel
this.author = message.author
retrixe marked this conversation as resolved.
Show resolved Hide resolved
this.message = message
this.questionList = triviaList
this.settings = { maxScore: maxScore, timeout: 120000, timeLimit, botPlays, revealAnswer }
this.tempDB = tempDB
this.client = client
}

getScores () {
const currentScores = Object.entries(this.scores).sort(([, a], [, b]) => b - a)
0xVR marked this conversation as resolved.
Show resolved Hide resolved
const member = this.message.member.guild.members.get(this.client.user.id)
const color = member ? (member.roles.map(i => member.guild.roles.get(i)).sort(
(a, b) => a.position > b.position ? -1 : 1
).find(i => i.color !== 0) || { color: 0 }).color : 0
const embed: EmbedOptions = {
title: 'Scores',
color,
timestamp: new Date().toISOString(),
fields: currentScores.map(player => ({
name: this.message.member.guild.members.get(player[0]).username,
0xVR marked this conversation as resolved.
Show resolved Hide resolved
value: player[1].toString(),
inline: true
}))
}
return { embed }
}

async endGame () {
this.stopped = true
delete this.tempDB.trivia[this.channel.id]
if (Object.keys(this.scores).length !== 0) await this.channel.createMessage(this.getScores())
0xVR marked this conversation as resolved.
Show resolved Hide resolved
}

async newQuestion () {
for (let i of Object.values(this.scores)) {
if (i === this.settings.maxScore) {
await this.endGame()
return true
}
}
if (!this.questionList) {
await this.endGame()
return true
}
this.currentQuestion = Array.from(this.questionList.entries())[Math.floor(Math.random() * this.questionList.size)]
this.questionList.delete(this.currentQuestion[0])
this.count += 1
this.timer = Date.now()
await this.channel.createMessage(`**Question number ${this.count}!**\n\n${this.currentQuestion[0]}`)

while (this.currentQuestion !== null && (Date.now() - this.timer) <= this.settings.timeLimit) {
if (Date.now() - this.timeout >= this.settings.timeout) {
await this.channel.createMessage(`If you ${getInsult(true)} aren't going to play then I might as well stop.`)
await this.endGame()
return true
}
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait for answer or timeout
}

const revealMessages = ['I know this: ', 'Easy: ', 'Of course, it\'s: ']
const failMessages = ['You suck at this', 'That was trivial, really', 'Moving on...']

if (this.currentQuestion === null) {
await new Promise(resolve => setTimeout(resolve, 1000))
if (this.stopped === false) await this.newQuestion()
} else if (this.stopped) {
return true
} else {
let message: string
if (this.settings.revealAnswer) {
message = revealMessages[Math.floor(Math.random() * revealMessages.length)] + this.currentQuestion[1][0]
} else {
message = failMessages[Math.floor(Math.random() * failMessages.length)]
}
if (this.settings.botPlays) {
message += '\n**+1** for me!'
if (!this.scores[this.client.user.id]) {
this.scores[this.client.user.id] = 1
} else {
this.scores[this.client.user.id] += 1
}
}
this.currentQuestion = null
await this.channel.createMessage(message)
await this.channel.sendTyping()
await new Promise(resolve => setTimeout(resolve, 1000))
if (this.stopped === false) await this.newQuestion()
}
}

async checkAnswer (message: Message) {
if (message.author.bot || this.currentQuestion === null) {
return false
}
this.timeout = Date.now()
let hasGuessed = false

for (let i = 0; i < this.currentQuestion[1].length; i++) {
let answer = this.currentQuestion[1][i].toLowerCase()
let guess = message.content.toLowerCase()
if (!answer.includes(' ')) { // Strict answer checking for one word answers
const guessWords = guess.split(' ')
for (let j = 0; j < guessWords.length; j++) {
if (guessWords[j] === answer) {
hasGuessed = true
}
}
} else if (guess.includes(answer)) { // The answer has spaces, checking isn't as strict
hasGuessed = true
}
}

if (hasGuessed) {
this.currentQuestion = null
if (!this.scores[message.author.id]) {
this.scores[message.author.id] = 1
} else {
this.scores[message.author.id] += 1
}
await this.channel.createMessage(`You got it ${message.author.username}! **+1** to you!`)
}
}
}

export const handleTrivia: Command = {
name: 'trivia',
opts: {
description: 'Start a trivia session',
fullDescription: 'Start a trivia session\nDefault settings are: Ive gains points: false, seconds to answer: 15, points needed to win: 30, reveal answer on timeout: true\nDuring a trivia session, the following commands may also be run:`/trivia score` or `/trivia leaderboard` and `/trivia stop`\nTo see available trivia genres, use `/trivia list`',
usage: '/trivia <topic> (--bot-plays=true|false) (--time-limit=<time longer than 4s>) (--max-score=<points greater than 0>) (--reveal-answer=true|false)',
0xVR marked this conversation as resolved.
Show resolved Hide resolved
example: '/trivia greekmyth --bot-plays=true',
guildOnly: true,
argsRequired: true
},
generator: async (message: Message, args: string[], { tempDB, client }) => {
let botPlays = false
let timeLimit = 15000
let maxScore = 30
let revealAnswer = true

if (args.find(element => element.includes('--bot-plays='))) {
if (args.find(element => element.includes('--bot-plays=')).split('=')[1] === 'true') {
botPlays = true
} else if (args.find(element => element === '--bot-plays=').split('=')[1] !== 'false') {
return 'Invalid usage. It must be either true or false.'
}
}
if (args.find(element => element.includes('--time-limit='))) {
if (+args.find(element => element.includes('--time-limit=')).split('=')[1] > 4) {
timeLimit = +args.find(element => element.includes('--time-limit=')).split('=')[1]
} else {
return 'Invalid usage. It must be a number greater than 4.'
}
}
if (args.find(element => element.includes('--max-score='))) {
if (+args.find(element => element.includes('--max-score=')).split('=')[1] > 0) {
maxScore = +args.find(element => element.includes('--max-score')).split('=')[1]
} else {
return 'Invalid usage. It must be a number greater than 0.'
}
}
if (args.find(element => element.includes('--reveal-answer='))) {
if (args.find(element => element.includes('--reveal-answer=')).split('=')[1] === 'false') {
revealAnswer = false
} else if (args.find(element => element === '--reveal-answer=').split('=')[1] !== 'true') {
return 'Invalid usage. It must be either true or false.'
}
}
if (args.length === 1 && ['scores', 'score', 'leaderboard'].includes(args[0])) {
const session = tempDB.trivia[message.channel.id]
if (session) {
return session.getScores()
} else {
return 'There is no trivia session ongoing in this channel.'
}
} else if (args.length === 1 && args[0] === 'list') {
const lists = await fs.promises.readdir('./triviaLists/')
const member = message.member.guild.members.get(client.user.id)
const color = member ? (member.roles.map(i => member.guild.roles.get(i)).sort(
(a, b) => a.position > b.position ? -1 : 1
).find(i => i.color !== 0) || { color: 0 }).color : 0
const embed: EmbedOptions = {
title: 'Available trivia lists',
color,
fields: lists.map(name => ({ name: name.replace('.txt', ''), value: '_ _', inline: true }))
}
return { embed }
} else if (args.length === 1 && args[0] === 'stop') {
const session = tempDB.trivia[message.channel.id]
const channel = message.channel as GuildTextableChannel
if (session) {
if (message.author === session.author || channel.permissionsOf(message.author.id).has('manageMessages')) {
await session.endGame()
return 'Trivia stopped.'
} else {
return 'You are not authorized to do that.'
}
} else {
return 'There is no trivia session ongoing in this channel.'
}
} else {
const session = tempDB.trivia[message.channel.id]
if (!session) {
let triviaList
try {
triviaList = await parseTriviaList(args[0])
} catch (err) {
return 'That trivia list doesn\'t exist.'
}
const t = new TriviaSession(triviaList, message, botPlays, timeLimit, maxScore, revealAnswer, tempDB, client)
tempDB.trivia[message.channel.id] = t
await t.newQuestion()
} else {
return 'A trivia session is already ongoing in this channel.'
}
}
}
}
7 changes: 5 additions & 2 deletions server/bot/imports/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ export const getChannel = (message: Message, arg: string) => {
}

// Fresh insults. They come and go, I suppose.
export const getInsult = () => {
export const getInsult = (plural = false) => {
const insults = [
'pathetic lifeform', 'ungrateful bastard', 'idiotic slimeball', 'worthless ass', 'dumb dolt',
'one pronged fork', 'withered oak', 'two pump chump', 'oompa loompa'
]
return insults[Math.floor(Math.random() * insults.length)]
const insult = insults[Math.floor(Math.random() * insults.length)]
if (plural) insult.concat('s')
if (insult.includes('asss')) insult.replace('asss', 'asses')
return insult
0xVR marked this conversation as resolved.
Show resolved Hide resolved
}

export const fetchLimited = async (url: string, limit: number, opts = {}): Promise<false | Buffer> => {
Expand Down
4 changes: 4 additions & 0 deletions server/bot/imports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { AdvancedMessageContent, Client, Message } from 'eris'
import CommandParser from '../client'
import { Db } from 'mongodb'
import { TriviaSession } from '../commands/trivia'

export type DB = {
gunfight: {
Expand All @@ -17,6 +18,9 @@ export type DB = {
// Channels.
[index: string]: string
},
trivia: {
[index: string]: TriviaSession
},
mute: {
// Servers with userIDs contained.
[index: string]: string[]
Expand Down
5 changes: 5 additions & 0 deletions server/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export default async (message: Message, client: Client, tempDB: DB, db: Db) => {
// Content of message and sendResponse.
const sendResponse = message.channel.createMessage
const command = message.content.toLowerCase()
// Handle answers to trivia
const session = tempDB.trivia[message.channel.id]
if (session) {
await session.checkAnswer(message)
}
// Auto responses and easter eggs.
if (command.startsWith('is dot a good boy')) await sendResponse('Shame on you. He\'s undefined.')
else if (command.startsWith('iphone x')) await sendResponse(`You don't deserve it. 😎`)
Expand Down
4 changes: 3 additions & 1 deletion server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ MongoClient.connect(mongoURL === 'dotenv' ? process.env.MONGO_URL : mongoURL, {
if (!Object.keys(commands).length) return
// ..register the commands.
Object.keys(commands).forEach((commandName: string) => {
// exclude TriviaSession from commands
if (commandName === 'TriviaSession') return
const command = commands[commandName]
commandParser.registerCommand(command)
})
Expand Down Expand Up @@ -117,7 +119,7 @@ client.on('error', (err: Error, id: string) => {

// Create a database to handle certain stuff.
const tempDB: DB = {
gunfight: {}, say: {}, link: {}, leave: [], mute: {}, cooldowns: { request: [] }
gunfight: {}, say: {}, trivia: {}, link: {}, leave: [], mute: {}, cooldowns: { request: [] }
}

/* SERVER CODE STARTS HERE */
Expand Down