The Worst Discord Bot

2021/01/18 | back

A few years ago, I played a tabletop RPG campaign with some friends using Advanced Dungeons & Dragons 2nd Edition.

d&d 2e

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.

markov chain (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

  1. A log of all messages said directly to Havelbot or mentioning him
  2. A log of all messages said in servers that Havelbot is in
  3. 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.

havelbot_1

He seems to be obsessed with being physically fit, which is frankly in character.

havelbot_2

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.

havelbot_3

havelbot_4

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.

buff command


Giving me access to a computer was a mistake.