Portfolio

StyroBotPy

About a month ago, I started a new project which really piqued my interest. It is rare for me to find a passion project where I don’t want to do anything but work on it. But this one in particular really had me going and for 3 weeks, I worked on a project that my friend had originally created. This project was called StyroBotPy. StyroBotPy was created by a friend of mine in C# to be a Discord bot. This bot had various functionality such as being able to talk to cleverbot in Discord chat, playing music in the voice channels, getting quotes from a text channel and more. Unfortunately for him, something in the C# API for Discord broke the night before his project was due and he re-wrote it in Python. This was all before Discord even had an official API (which it does now).

This project interested me because it was a neat idea; and since I use Discord, I thought it could be cool to mess around with this on my own. So with his permission, I became a contributor on his project and came up with a grand plan for changing it. Now, the original project was nicely structured and setup to be fairly modular, but when it was hastily re-written in python, it was messy and basically a god class. It was quite the mess and the interwovenness of the code plus the implicit nature of python made it somewhat difficult to decipher at first. But this only made it more fun to work on. And so I set off on my journey to refactor and update his Discord bot.

For about a week, I was playing around with an idea of turning the entire thing to a Plugin based architecture. This meant making it extremely modular and super easy to extend in the future. Add on top of this some of python’s quirks and you’ve got a pretty great system. The only problem was that the project in its current form wouldn’t work well with this. It was mostly because the repo and the code wasn’t really setup for multiple files. If you’ve ever done python development with more than 1 file, you will understand how it can be a problem when your project isn’t properly setup. I was also working in linux and he created the python in windows. Add on top of this the fact that there was no automation like make/batch. It was a nightmare. So I ended up spending a few days restructuring the repo and creating some automation that would allow me to have a decent workflow. I tried my best to make sure it still ran on windows, but I honestly still have no idea if those batch scripts work. I kind of abandoned the windows specific stuff to focus my efforts on the python itself.

Once everything was setup, I began experimenting with the Plugin architecture and after a bit of researching, I found a solution that I liked a lot. I am using a python module called yapsy which is short for Yet Another Plugin SYstem. It allows me to very easily create plugins and load them dynamically at runtime with almost no effort at all. And on top of this, it is built on Python’s standard library meaning it has no dependencies of its own, making it light weight and simple. Once I had found this, I had what I wanted working in minutes. Then over the course of roughly 2 weeks, I slowly tweaked my abstract Plugin class to hold the functionality I needed for the system. The result can be seen below.

The comments explain everything fairly clearly, but the basic idea is that a Plugin has all the functions it needs to do things based off of input from Discord. With this simple but effective Plugin system created, I began refactoring the codebase into plugins.

It took a little bit of time, but I eventually implemented 5 different plugins. They each show off different things you can do with the system, but they also have useful functions from the perspective of a bot. I will talk about each one individually so that I can talk about some of the interesting things about them and any challenges I faced.

CleverBot Chat

To start off, I will begin with the Cleverbot plugin. This was one of my favorite parts about the bot before I started working on it and I knew that implementing it was a top priority. Luckily for me, there was a python module for cleverbot and it was something that took less than 5 minutes to implement. The idea for this plugin is that with a specific command, you could talk with cleverbot and the bot would post the response to the channel you sent the command in. The result was spectacular and you can see an example conversation below.

1

Quotes

The next plugin I would like to show is the Quotes plugin. This plugin was designed based off some functionality the original C# bot had which would let you say messages from a specific text channel. The intent is that you create a channel for memorable quotes and then at any time you can have the bot read a random quote from this channel. When converting the bot to python, this feature got lost and I wanted to make sure it lived on. But making sure it lived on wasn’t enough, I also wanted it to be more flexible. The previous implementation had hard-coded channels and that made it so only 1 channel could be used for quotes and you couldn’t change it without modifying the python script. I wanted to make this more modular with a server side solution so that even if the bot was run on a different machine or by a different person in the same server, it would have the same settings. I eventually came up with the idea of storing this information in a text channel. This solved the problem, but it isn’t perfect. I will talk more about its shortcomings later in this post. The result of all of this was that the plugin was able to give you quotes from a channel (you could change which at any time) and it would remember this information across sessions and computers for that server. You can also see it in action below!

2

Music

The next plugin I want to talk about was also a feature of the original bot. This is the music plugin. This plugin allows the user to download and play music playlists in the voice channels in Discord. You just simply tell the bot to enter a specific voice channel, queue up the songs and play. These songs are local to the machine the bot is running on, but you can always download songs through one of the commands. This would download the youtube video you specified and save it as a mp3 which would then be used for playback later. This was originally used to create playlists in the voice channel so that you could listen to Mom’s Spaghetti and other great songs while talking with friends or playing games with them. Below you can see the bot in the voice channel General playing the song Starfall which I downloaded and queued to play.

3

4

 

High Roller

The second to last plugin I want to talk about is a new feature to the bot and was created to help show how the bot is capable of doing just about anything, such as dice rolls. This plugin lets you do typical things like roll a dice of any size or flip a coin. The result is obviously said in the chat and this could be used for things like your weekly D&D with friends or perhaps even to settle disputes such as who is the best high/low roller. Another small feature is the ability to call a coin flip before it happens. If two users do this, the coin is then flipped and it will tell you who won/lost. It is possible for everyone to win or lose depending on what the two of you called before (you don’t have to call different things).

6

5

ChatMod

The final plugin I created is also a new feature to the bot. This plugin handles chat moderation based off criteria defined by the server admins, specifically based off of banned words. The bot stores this information in a text channel, just like with the Quotes plugin so that it persists between sessions and machines. It also supports other settings such as how many warnings a user gets and whether to kick or ban them if they have too many infractions. The plugin is also fairly good at scrubbing through text to find words that users try to hide. It is capable of stripping out all the markdown that Discord supports, as well as other common things such as hyphens to better determine when a banned word is used. It is by no means perfect, but this plugin tries to show how one might create a plugin which doesn’t run off of commands but parses messages directly to do an action, such as moderating chat. As you can see in the images below, I have a message with a bunch of markdown which still finds the banned word (in this case, “poop”) and warns me. The plugin is also nice enough to tell you when you’ve reached your last warning before it takes action.

7

8

The Server Side Problem

Earlier I mentioned that my current server side solution has problems and isn’t very good. What I mean by this is that for starters there is no established way of doing what I want so its kind of “hacked” together. It is also not supported as a feature in the bot itself, but is instead done by the plugin itself which has drawbacks. Not to mention that in its current form, the bot doesn’t handle multiple servers very well at all.

Looking Forward

While there may only be 5 plugins currently implemented, I know I have a few more ideas that given the time I would love to add. Things such as an improved High Roller plugin which allows for a loot system similar to World of Warcraft’s Need/Greed system and making the roll use dice notation so you can specify 2d6 or something like that to roll multiple dice like you would need in a lot of roleplaying board games. I also have other ideas such as improving the help command and adding a system to help disambiguate between commands with the same name. I would also like to create a better, more in-depth server side solution for storing settings that plugins can easily use through the bot instead of having to “hack” it. I could go on and on, but I think I made my point.

Reflection

Every time I think about this project, I get excited about the potential features and all of the cool things I could do to improve the system. During the 3-4 weeks I was developing this constantly, I learned a lot more about python, but more importantly I had fun. I had the opportunity to explore something I’ve never done before and looking back, I never expected to like it so much. My hopes are definitely to continue working on this to see where I can take it and perhaps it will even become an interesting portfolio piece down the road. I also think that there is a lot of room for improvement. While building this bot, I experimented with a lot of structural things and the further I progressed, the less I implemented due to time constraints. My hopes are to return to this project in the near future to continue developing it on the side.

 

Check this project out on Github!

PDGenerator

Roughly 8 weeks ago, I started a project which seemed interesting and required some thinking. I purposely decided when I started that I didn’t want to lookup how to do something like this in an effort to see what I was capable of coming up with. I knew there would be problems which I had never tackled and it was a great experience to learn as I went, improving my abilities to solve these complex problems. What I created in this time was a thing I have decided to call PDGenerator. PDGenerator is a static API for creating procedurally generated dungeons in C++.

The approach I took for this was inspired by the dungeons created in Runescape for the skill Dungeoneering. In Runescape, they generate the dungeons in a very grid-like fashion.

A map of a Large dungeon in Runescape.
Image Courtesy of Zybez.net

Having played a lot of Runescape, it was easy for me to come up with some basic rules to use for the algorithms needed. I have put them in a bulleted form below.

  • A dungeon should always be a grid. The width and height of the grid don’t need to be the same.
  • Rooms can only connect in the 4 cardinal directions (North, East, South, West)
  • Every room should always be reachable from any other room (directly or indirectly).
  • The dungeon should always be fully solvable.

This of course doesn’t encompass the features I was looking to have, but they were guidelines to keep my end result focused and served as a baseline for testing as I progressed through the project. Since I was already basing the basic rules for the dungeon generation on Runescape, I decided to also base some of the features off of the game as well. In particular, the use of locked doors and keys where each door/key combination is unique. This made it tricky towards the end when I was generating keys for the doors, which I will go into more detail about later.

I would like to spend a little bit of time in this post to go over the various components of the generator and while this won’t encompass all the details, it will broadly go over the important parts. To start this off, I am providing two code snippets. The first snippet is the BuildDungParams structure which contains the information needed to create a dungeon. The second snippet is the BuildDungeon function in the API.

I will avoid mentioning the individual values in the BuildDungParams structure because I have them nicely commented to explain their functionality. This brings us to the BuildDungeon function which I have provided. Before we dig in too far, I would like to point out that I have some typedefs such as pd_dung_ptr and I will do my best to include these typedefs at the top of the code snippets when they are used in the code snippets. I will now breakdown this function into its parts and talk about each one in more detail. To make things easier to explain, I will be using a 5×5 (25 room) dungeon as an example throughout this process.

Random Seed

In the settings for the dungeon, you can specify whether or not to use a custom seed. This is to allow the user the flexibility of creating a dungeon using a specific seed or to do it truly randomly. The generator always makes sure to seed the rand function to avoid problems such as the user forgetting to.

Create Dungeon

This function is fairly simple so I won’t show a code snippet but i’ll briefly summarize it. It creates a dungeon of the width and height specified by the user. It then populates the dungeon with rooms based off this width and height. These rooms are based off the room settings provided by the user in variables such as RoomMinWidth and RoomWidth. If the min and max are different, it uses a range, adding some randomness to the room size. It returns the result of this process as a pd_dung_ptr. In the case of our 5×5 dungeon, we will be getting a dungeon with 25 rooms in a grid something along the lines of this.

1

Connect Dungeon

The goal of this function is to generate the connections for the dungeon. Up to this point, we only have a dungeon with rooms that are not connected to anything. This function takes in a dungeon and creates connections for the rooms. This will guarantee that every room is connected to at least 1 other room and that every room can reach any other room. I have provided pseudocode below due to the complexity of the implementation.

Given our 5×5 dungeon example, you could expect a connected dungeon to look something like this. In this particular case, I had a setting which was adding a few redundant connections during the algorithm to improve the connectivity of the dungeon.

2

Seed Dungeon

I created this function as a way to help ensure that a dungeon can have more connections than what the ConnectDungeon function gives. Since we are dealing with RNG, you never really know how it could end up. Worst case scenario, its just a really long winding hallway. This function calculates a number of connections to add (dungeon size * percent), picks a room, picks a random neighbor and adds a connection there if one doesn’t exist. You can also tell it to enforce the number it calculates to add which basically just means it will guarantee that many are made (unless you hit the max number of connections in the dungeon). If we were to seed our 5×5 dungeon example, we might now have a dungeon that looks something like this.

3

Create Locked Doors

This function is also fairly simple. It just gets a list of the connections, calculates how many locked doors to add (num connections * percent) and adds that many locked door to random connections. This function is always enforced, but like before, it will stop if it hits the max amount of locked doors. If we took our 5×5 dungeon example, we might see something like this.

4

Create Keys

This is either the first or second most complicated function for the generation. What made this tricky is ensuring that a dungeon is always fully solvable. I’m sure there are lots of great ways to do this, but I decided to take an approach similar to a flood fill algorithm. The goal is to get all accessible rooms from a starting point and pick a random room to spawn a key in. I have provided some pseudocode below. Just keep in mind that this leaves out some details.

If we were to visualize this with our 5×5 dungeon, you could expect something like this. In this particular case, the seed I was using didn’t do a great job of placing the keys. The algorithm also doesn’t discourage placing more than one key in a room which you could see as both good and bad. Regardless, this dungeon should be fully solvable and is ready to return to the user in the form of a pd_dung_ptr.

5

Notes

The dungeon itself tries to store the data as separately as possible. The goal is to keep things as decoupled as possible, making it easier to deal with the data. A dungeon stores an array of rooms. These rooms only know their grid position, if they are the start of the dungeon and what keys (if any) they have in them. If this were to be expanded further, they might also contain information such as what the room contains (such as items, monsters, the layout, etc).

The dungeon also stores the connections in an array. Connections just store which two rooms are connected and if that connection is locked. Because these are stored in an array, it means that finding a connection for a room could be time consuming. To counter this, the dungeon also has a lookup for connections. This lookup stores a reference to the indexes of all the connections a room has, making it quick and easy to grab a connection for a room. Since a connection also stores if it is locked (aka the locked door), I implemented a lookup for these locked doors which makes it quick and easy to find which connections are locked. Granted, this lookup wouldn’t do much if a large amount of connections were locked.

The dungeon also provides a ton of helper functions for accessing information that is stored. Anything from finding rooms, connections, locked doors, room floods and so forth.

The dungeon also has the ability to export itself to JSON. This was useful for debugging and also makes it a convenient way to export the important information in the case that you want to create your own storage objects for the data or want to save it to a file or even use it in an external application.

Reflection

Looking back on this project, I was able to accomplish a lot in the roughly 8 weeks I had. I also learned a lot about how to generate a good procedural dungeon (something that I think I was not able to accomplish). In particular, the way I did the generation made it difficult to intelligently layout a dungeon and if I were to change things, I would try to incorporate many of the things I did for generating a dungeon as part of the process of connecting rooms. There are a few other tricks like this that can help make a dungeon more interesting to traverse and explore. One thing I wish I had spent time on doing was incorporating dead ends to the dungeon. Currently, the only time there is a dead end is if the room connection algorithm got unlucky in the RNG and this doesn’t really count.