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 11 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
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ module.exports = {
'@typescript-eslint/no-angle-bracket-type-assertion': ['error'],
// Fix no-unused-vars.
'@typescript-eslint/no-unused-vars': ['error'],

// Make TypeScript ESLint less strict.
'@typescript-eslint/member-delimiter-style': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
Expand Down
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/
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# IveBot

The bot that created the iPhone X. It's strictly private. You may run it locally, but it's not a useful bot to others and tailored for a specific need.
The bot that created the iPhone X. It's strictly private. You may run it locally, but it's not a useful bot to others and tailored for a specific need.

**Requires Node.js 8.5.0 or higher.**

Expand All @@ -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 @@ -79,6 +80,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
301 changes: 301 additions & 0 deletions server/bot/commands/trivia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import { Message, GuildTextableChannel, TextableChannel, User, EmbedOptions, Client } from 'eris'
import { Command, DB } from '../imports/types'
import fs from 'fs'
import { promisify } from 'util'
import { getInsult } from '../imports/tools'

async function parseTriviaList (fileName: string) {
const readFile = promisify(fs.readFile)
const data = await readFile(`./triviaLists/${fileName}.txt`, 'utf8')
0xVR marked this conversation as resolved.
Show resolved Hide resolved
const triviaList = data.split('\n')
return triviaList
.map(el => el.replace('\n', '').split('`'))
.filter(el => el.length >= 2 && el[0])
.map(el => ({
question: el[0],
answers: el.slice(1).map(ans => ans.trim())
}))
0xVR marked this conversation as resolved.
Show resolved Hide resolved
}

export class TriviaSession {
revealMessages = ['I know this: ', 'Easy: ', 'Of course, it\'s: ']
failMessages = ['You suck at this', 'That was trivial, really', 'Moving on...']
0xVR marked this conversation as resolved.
Show resolved Hide resolved
settings: { maxScore: number, timeout: number, delay: number, botPlays: boolean, revealAnswer: boolean }
0xVR marked this conversation as resolved.
Show resolved Hide resolved
currentLine: {question: string, answers: string[]}
channel: TextableChannel
author: User
retrixe marked this conversation as resolved.
Show resolved Hide resolved
message: Message
questionList: {question: string, answers: string[]}[]
scores: { [char: string]: number } = {}
retrixe marked this conversation as resolved.
Show resolved Hide resolved
status = ''
0xVR marked this conversation as resolved.
Show resolved Hide resolved
timer: number
timeout: number
count: number
0xVR marked this conversation as resolved.
Show resolved Hide resolved
tempDB: DB
client: Client

constructor (triviaList: {question: string, answers: string[]}[], message: Message, botPlays: boolean, timeLimit: number, maxScore: number, revealAnswers: boolean, tempDB: DB, client: Client) {
0xVR marked this conversation as resolved.
Show resolved Hide resolved
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.status = 'new question'
this.settings = { maxScore: maxScore, timeout: 120000, delay: timeLimit, botPlays: botPlays, revealAnswer: revealAnswers }
this.timer = null
this.count = 0
this.timeout = Date.now()
0xVR marked this conversation as resolved.
Show resolved Hide resolved
this.tempDB = tempDB
this.client = client
}

async sendScores () {
0xVR marked this conversation as resolved.
Show resolved Hide resolved
const currentScores = Object.values(this.scores).sort((a: number, b: number) => b - a)
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.forEach(score => {
embed.fields.push({
name: Object.keys(this.scores).find(key => this.scores[key] === score),
value: String(score),
inline: true
})
})
0xVR marked this conversation as resolved.
Show resolved Hide resolved
return { embed }
}

async stopTrivia () {
this.status = 'stop'
delete this.tempDB.trivia[this.channel.id]
this.settings = {
maxScore: 30,
timeout: 120000,
delay: 15000,
botPlays: false,
revealAnswer: true
}
this.scores = {}
}

async endGame () {
this.status = 'stop'
delete this.tempDB.trivia[this.channel.id]
if (this.scores) { this.channel.createMessage(await this.sendScores()) }
0xVR marked this conversation as resolved.
Show resolved Hide resolved
this.settings = {
maxScore: 30,
timeout: 120000,
delay: 15000,
botPlays: false,
revealAnswer: true
}
this.scores = {}
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) {
this.endGame()
0xVR marked this conversation as resolved.
Show resolved Hide resolved
return true
}
}
if (!this.questionList) {
this.endGame()
0xVR marked this conversation as resolved.
Show resolved Hide resolved
return true
}
this.currentLine = this.questionList[Math.floor(Math.random() * this.questionList.length)]
this.questionList.splice(this.questionList.indexOf(this.currentLine), 1)
this.status = 'waiting for answer'
this.count += 1
this.timer = Date.now()
this.channel.createMessage(`**Question number ${this.count}!**\n\n${this.currentLine.question}`)
0xVR marked this conversation as resolved.
Show resolved Hide resolved

while (this.status !== 'correct answer' && (Date.now() - this.timer) <= this.settings.delay) {
0xVR marked this conversation as resolved.
Show resolved Hide resolved
if (Date.now() - this.timeout >= this.settings.timeout) {
const msg = `If you ${getInsult}s aren't going to play then I might as well stop.`
0xVR marked this conversation as resolved.
Show resolved Hide resolved
if (msg.includes('asss')) {
msg.replace('asss', 'asses')
}
0xVR marked this conversation as resolved.
Show resolved Hide resolved
this.channel.createMessage(msg)
0xVR marked this conversation as resolved.
Show resolved Hide resolved
await this.stopTrivia()
return true
}
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait for answer or timeout
}

if (this.status === 'correct answer') {
this.status = 'new question'
await new Promise(resolve => setTimeout(resolve, 1000))
if (this.status !== 'stop') { this.newQuestion() }
0xVR marked this conversation as resolved.
Show resolved Hide resolved
} else if (this.status === 'stop') {
return true
} else {
let message: string
if (this.settings.revealAnswer) {
message = this.revealMessages[Math.floor(Math.random() * this.revealMessages.length)] + this.currentLine.answers[0]
} else {
message = this.failMessages[Math.floor(Math.random() * this.failMessages.length)]
}
if (this.settings.botPlays) {
message += '\n**+1** for me!'
if (!this.scores['Jony Ive']) {
this.scores['Jony Ive'] = 1
} else {
this.scores['Jony Ive'] += 1
}
}
this.currentLine = null
this.channel.createMessage(message)
this.channel.sendTyping()
await new Promise(resolve => setTimeout(resolve, 1000))
if (this.status !== 'stop') { this.newQuestion() }
0xVR marked this conversation as resolved.
Show resolved Hide resolved
}
}

checkAnswer (message: Message) {
0xVR marked this conversation as resolved.
Show resolved Hide resolved
if (message.author.bot || this.currentLine === null) {
return false
}
this.timeout = Date.now()
let hasGuessed = false

for (let i = 0; i < this.currentLine.answers.length; i++) {
let answer = this.currentLine.answers[i].toLowerCase()
let guess = message.content.toLowerCase()
if (!(answer.includes(' '))) { // Strict answer checking for one word answers
0xVR marked this conversation as resolved.
Show resolved Hide resolved
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, checkign isn't as strict
hasGuessed = true
}
}
0xVR marked this conversation as resolved.
Show resolved Hide resolved
}

if (hasGuessed) {
this.currentLine = null
this.status = 'correct answer'
if (!this.scores[message.author.username]) {
this.scores[message.author.username] = 1
} else {
this.scores[message.author.username] += 1
}
this.channel.createMessage(`You got it ${message.author.username}! **+1** to you!`)
0xVR marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

export const handleTrivia: Command = {
name: 'trivia',
opts: {
description: 'Start a trivia session',
fullDescription: 'Start a trivia session\nDefault settings are:\nIve gains points: false\nSeconds to answer: 15\nPoints needed to win: 30\nReveal answer on timeout: true',
usage: '/trivia <topic> (--bot-plays=true|false) (--time-limit=<time longer than 4s>) (--max-score=<points greater than 0>) (--reveal-answer=true|false)\n During a trivia session, the following commands may also be run:\n/trivia score\n/trivia stop',
example: '/trivia greekmyths --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) {
message.channel.createMessage(await session.sendScores())
0xVR marked this conversation as resolved.
Show resolved Hide resolved
} else {
return 'There is no trivia session ongoing in this channel.'
}
} else if (args.length === 1 && args[0] === 'list') {
const readdir = promisify(fs.readdir)
let lists = await readdir('./triviaLists/')
lists = lists.map(list => list.slice(0, -4))
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.forEach(list => {
embed.fields.push({
name: list,
value: '_ _',
inline: true
})
})
return { embed }
0xVR marked this conversation as resolved.
Show resolved Hide resolved
} 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 if (args.length === 1) {
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
t.newQuestion()
0xVR marked this conversation as resolved.
Show resolved Hide resolved
} else {
return 'A trivia session is already ongoing in this channel.'
}
} else {
return 'Invalid usage. Use `/help trivia` to see proper usage.'
}
}
}
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 { Client, Message, MessageContent, EmbedOptions } 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 = (m: string) => client.createMessage(message.channel.id, m)
const command = message.content.toLowerCase()
// Handle answers to trivia
const session = tempDB.trivia[message.channel.id]
if (session) {
session.checkAnswer(message)
0xVR marked this conversation as resolved.
Show resolved Hide resolved
}
// Auto responses and easter eggs.
if (command.startsWith('is dot a good boy')) sendResponse('Shame on you. He\'s undefined.')
else if (command.startsWith('iphone x')) 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 @@ -74,6 +74,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 }
0xVR marked this conversation as resolved.
Show resolved Hide resolved
const command = commands[commandName]
commandParser.registerCommand(command)
})
Expand Down Expand Up @@ -116,7 +118,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
Loading