Building a Bot for Your Discord Community — /skillcheck Overview

Building a community around a game is no small challenge. One thing that is sure to help, however, is building a bot for your Discord server. A bot enables you to offer unique features to your community, allowing them to interact with each other and your game. One feature we built for Blood Vessels was the ability for users to perform skill checks against their “vampires” (i.e. a collection of stats), like you’d see in many TTRPG’s. We’ll take a quick look at how we implement skill checks, which you can use to help guide your own bot development efforts.

Like anything programming-related, there are multiple ways to implement a bot. A Discord bot is a program (in our case, a node.js app) that can be accessed over the internet. Whether to host the program on a 24/7 server or using a serverless model is dependent on your needs, but it’s worth knowing that both are possible. To keep things simple, our initial implementation utilizes a 24/7 server, an EC2 instance to be specific. To make the bot available to the internet, we start it like any other node-app using the command:

node — require ts-node/register index.ts

Of course, such ease-of-use doesn’t just magically appear, and one key node package we use is discord.js. This little module wraps Discord’s API and does a lot of heavy lifting for us. We want to avoid duplicating more thorough documentation found elsewhere, so at a high-level the flow between user, bot, and Discord can be described as follows: a user sends a command message to Discord, Discord relays the relevant information to the bot in the form of an interaction, and the bot responds within a set amount of time (usually involving a reply message). So all your bot needs to do is describe what commands are available on it, and how it responds whenever they’re issued. To keep things organized, we structure the relevant project files like so:

Fig. 1: Basic bot folder structure

One thing discord.js doesn’t talk about is to treat each message component (i.e. a button, a dropdown, etc.) as its own event handler, as opposed to cramming all component logic into the command handler that spawned it. That little bit of organization goes a long way; helping to locate bugs more quickly, and potentially facilitating a serverless approach in the future.

Setup done, we move onto a specific feature: skill checks. As mentioned previously, this is the standard mechanic you’d expect in a game like Dungeons & Dragons; players own a character with several traits, each having a numeric value associated with it. They can then roll some dice (or RNG) and compare against those values to determine a success or failure. How that would play out in Discord took a little experimenting, but we landed on the following, roughly:

  1. User issues a bot command (/skillcheck)
  2. Bot responds with a message acting as a skill check configuration form
  3. User fills the form, then submits it.
  4. Bot reads character data associated with the User, rolls a few random numbers, then responds with a message describing the result.

Overall, not so bad… Well, there are a few more details to consider. We will first ask, “How does a user get a character?” Since we will offer users NFTs which hold such data, we’d pull the Ethereum wallet address a user submitted to us when onboarding on our landing site, then read the wallet contents. A more likely scenario, however, is that the bot can be stood up well before your NFT drop. This was the case with us, but we still wanted people to have some fun with the bot ahead of that. Our solution was to have another bot command (/betavampire) that creates a row in a database for the issuing Discord user, and creates plain old character data on that row. Then we have a simple if-else to decide which way to read vampire stats, depending on the timing. Any database would work; we went with AWS DynamoDB.

The second detail comes from the desire to make the bot stateless, or as stateless as possible. That is, the bot should avoid holding data that persists outside of a single handler invocation, be it that of a command or message component. We save the state of the higher-level flow as a single row in the database (i.e. SkillCheckEntry), then each handler may update that row given a known state ID. To persist the ID across handlers, Discord allows message components to carry a limited, developer-set string which it passes back to the bot on interaction.

Fig. 2: SkillCheckEntry, i.e. Bot-side representation of persisted skill check data
Fig. 3: Bot’s response to /skillcheck; player-filled form
Fig. 4: Bot’s response after a completed skill check

With all that, the user is able to build up the data necessary to perform a skill check, and in a more familiar way when compared to typing commands with a particular syntax. Other things to think about going forward could be how to: enforce a limit on per-user usage of the command, prevent users spamming buttons which are backed by database operations, or implementing the stat-creation command in a similar fashion. So as to not make this too long, we’ll table those for now (though we did build them out internally). Despite skipping over some details, hopefully this helps develop some insight into how a bot functions.

If you’d like to learn more about Blood Vessels and win a spot on the allowlist, join us October 21st at 12:15pm PST for the 3rd episode of our Blood Vessels livestream series here.

The Blood Vessels NFT sale takes place October 26th.

We look forward to seeing your vampires in Discord!