How a grumpy programmer writes Python IRC bots

Why Python and why IRC?

My longest-running hobby is being a member of tabletop simulation baseball league that uses dice and cards and charts to determine outcomes. There are a lot of charts so a signifcant amount of time is spent looking up results that players haven't memorized.

Our league uses IRC as the way to play the games -- the players connect to the server we host and then play games out using a bot that rolls dice and communicating with each other via text. Very old school.

Making things a little more player-friendly will help the league get team owners up to speed quicker, so naturally I turned to automation and programming as a way to do it.

I chose Python because it had been a while since I had done anything with it, so why not sharpen the dull edge of my experience there a little. Thankfully there are lots of examples on how to work with IRC using just the default libraries that come with Python. In this case, the server the bot would be running on runs Python 3.8. I started off with this blog post and went from there.

Initial implementation

From a high level, we are doing the following

1) connect to the IRC server 2) loop endlessly while grabbing any responses from the server 3) examine those responses for my chosen chartbot trigger 4) if triggered, then look for dice rolls 5) display results in-channel

Now, I am sure there are better implementations for what I am trying to do here involving more commonly-accepted design patterns. In fact, the code analysis tools I am using are already complaining that the cyclomatic complexity is too high.

So, here is an example of what I have been doing

while True:
    text = irc.get_response()

    if "PRIVMSG" in text and ".c ifr" in text:
        details = text.split(' ')
        msgChannel = details[-4].strip()
        batterHand[msgChannel] = details[-1].strip()
        lookForIfrRoll[msgChannel] = "yes"

An example command that this would look for would be '.c ifr rsp'. This maps to:

  • .c is "activate chartbot"
  • ifr is "Infield Range"
  • rsp is "right-spray hitter", with other options available

Early on I realized that I needed to have some kind of "state" in here because it was a two-step process. Once it knew which chart it was supposed to refer to, it then needed to wait to get a die roll. Again, this feels like a brute-force method but I could not think of any other way. Maybe, again, there is a better pattern for keeping track of this.

So, having figured out we want to refer to the "infield range chart" (commonly known as IFR in game terms), we tell the bot "the next roll in this channel should be checked to see if it works with the IFR chart."

Here is a sample of the code that watches for an IFR roll:

    if "rolled" in text:
        details = text.split(' ')
        msgChannel = details[-4].strip()
        roll = details[-1].strip()

        if (msgChannel in lookForIfrRoll and
                batterHand[msgChannel] in validBatterHand and
                len(roll) == 2):
                 msgChannel, ifrChart.lookup(batterHand[msgChannel], roll)
            del lookForIfrRoll[msgChannel]
            del batterHand[msgChannel]

All rolls for the IFR chart will be from 00-99, so we make sure the rollbot returned something 2 characters in length. We also make sure that the batter hand type matches our expectations. If that is all good, then we call an object that contains our chart information, do a lookup, and then send the results of that lookup into the channel.

Here is a sample of what the IFR chart object looks like:

class IFRChart:
    chart = {}

    def __init__(self):
        self.chart = {
            'lp': {
                "00": "Up the middle P",
                "01": "High chopper P",
                "02": "Line drive P",
                "03": "Down the line 1B",
                "04": "Down the line 1B",
                "05": "Down the line 1B",
                # more results snipped

    def lookup(self, bats, roll):
        return self.chart[bats][roll]

I will say that creating this chart objects helped me get my muscle memory for Emacs in a better place. So much cut-and-pasting-and-replacing of things!

So there you have it, a small example of how I have started writing an IRC bot that:

  • reads responses for a trigger
  • figures out what chart to read
  • waits for a die roll that matches expectations
  • spits out the lookup result in channel

As always, I am happy to get some advice on better ways to refactor and implement solutions for this code.

Categories: tools