The Worst Discord Bot
2021/01/18 | backA few years ago, I played a tabletop RPG campaign with some friends using Advanced Dungeons & Dragons 2nd Edition.
Being over 30 years old, it has a few rough spots. It's notoriously deadly compared to modern D&D editions, and the learning curve for new characters is quite steep.
Enter Havelpov, my level 1 wizard. Being 2e, Havelpov started with 4 hit points and could cast 1 first level spell per day. After that was blown, he could throw darts that did 1d3 damage. He was… ineffective, to say the least.
Naturally, Havelpov considered himself to be the greatest wizard who ever lived. Although old (80+!), he had a penchant for getting into dire situations. He was never afraid to let everyone around him know how fantastic he was.
One thing I remember fondly during my formative childhood programming years was writing markov chain chat bots.
If you're unfamiliar, a markov chain (or process) is a model that represents a sequence of events that act stochastically (and satisfy the "markov property" of no memory).
Basically, a markov chain is a set of states and transitions between them. The transitions take on some probability they will be followed.
(image via Wikipedia)
One can model speech as a markov chain, where each state is a short sequence of words. Each transition represents the probability that another sequence will follow the current one. You build up these models from a corpus.
My college friends made a discord server to coordinate playing games together remotely during quarantine. This gave me an awful idea. I wanted to bring Havelpov back to life.
To my surprise, getting a discord bot up and running in 2021 is trivial - library authors have already done all of the hard work. Getting a markov text generator working was similarly easy, thanks to a few simple python packages.
So, I set out building Havelbot. He would learn from a combined corpus of chat logs and RPG related text files. He would respond to a few commands (like !roll
for rolling dice), and reply to any mention of his name with generated non sequitur havel-sayings.
I found the following Python libs:
- discord.py - this made connecting to discord and fetching data a breeze
- markovify - functional text generation out of the box! supports non-text models too
- d20 - dice rolling (and request parsing!)
The rest was just stringing these together, along with a few novel features. Here's a truncated implementation:
class Havelbot(discord.Client):
def __init__(self):
super().__init__()
self.model = speech.build_model()
async def send_havelsaying(self, message):
async with message.channel.typing():
speech.add_corpus_line(message.content)
self.model = speech.build_model() # retrain including from new line
res_message = self.model.make_sentence().upper()
await asyncio.sleep(1) # pretend to type for a sec
await message.channel.send(res_message)
async def on_message(self, message):
if message.author == client.user:
return # don't listen to self
speech.add_log_line(message.content) # not a command to havelbot, go ahead and log
if isinstance(message.channel, discord.channel.DMChannel):
await self.send_havelsaying(message)
return
if client.user in message.mentions:
await self.send_havelsaying(message)
return
for name in HAVELNAMES:
if name in message.content.lower():
await self.send_havelsaying(message)
return
The markov corpus is split between
- A log of all messages said directly to Havelbot or mentioning him
- A log of all messages said in servers that Havelbot is in
- A bunch of text files grabbed from textfiles.com's rpg section
The markovify lib supports merging multiple models with various weights, so Havelbot uses the empirically tested [200, 50, 1]
weights for direct, chat log, and text file models respectively.
def build_model():
models = []
for file_name in [CORPUS_FILE, FULL_LOG_FILE, RPG_TEXT_FILE]:
with open(file_name) as f:
text = f.read()
models.append(markovify.Text(text, state_size=1))
return markovify.combine(models, [200, 50, 1])
This results in some… colorful generated text.
He seems to be obsessed with being physically fit, which is frankly in character.
Nothing wrong with showing off a little bit, if you're healthy.
Of course, it wasn't long before he picked up some rather unscrupulous language, being such an acute listener.
Perhaps opening up his chatlog to anything and everything said around him was a mistake, considering my friends.
I decided to add one final feature.
When you use the !buff
command, havelbot randomly posts an image of a buff wizard, pre-curated into a text file from google images by yours truly.
This adds a great amount of annoyance to our chat logs, as any train of thought can be interrupted by smoking hot wizard abs summoned at the push of a button.
Giving me access to a computer was a mistake.