I sometimes forget just how much fun programming can be. I do a lot of programming in my day life, and that's just not the same. And when I do find the time to work on something, it'll often be on one of those large projects that will take an eternity to go anywhere. On the other hand, it's the small projects that don't really have to go anywhere that bring out the pure fun of programming itself for me.
So, that's why I've decided I'm going to make an addition to my Discord bot. And I've decided to take you with me on the journey to build this feature. I'm not going to share the code. The rest of the bot is on GitHub, so you can find it there if you want to. However, today I want to capture the experience of building the feature rather than the code, so I'm not going to post the code in this post.
I created this bot as my first foray into the world of Node. The reason for making it was really that I wanted to make a Discord bot. It sounded fun. And it was. That's one of the reasons I picked this project: it's just fun to work on it. The bot runs in a Discord channel that I frequent, in which we only use the text channels. It has features such rolling dice (~roll 2d7
), selecting a random member of the current channel and a complex system that lets you store secrets you can reveal later on.
I want to add a feature that's simple to make. Something that I can easily complete today even though I'm also writing about the process. It probably would be best if it doesn't integrate with other services. Yet, it still has to be interesting to make. And preferably, to use as well.
I want to build something that actually adds something new to the bot that isn't there in Discord already. For example, you can already search Google Maps for a place and then paste the link in Discord, so I don't want to build something that makes that easier. I want to make something where the storage, the timing of a bot, storing a value or quick access to Discord APIs adds some kind of value. I know that doesn't narrow it down too much, but at least it does so a little.
One of the more interesting things I have done with the bot is to abuse the emoji reaction system that Discord has to create sort of a user interface to communicate with the bot. However, that system is mostly copy-paste at the moment, and I think I have taken it about as far as I can without refactoring it. And refactoring is just not what I'm looking for at the moment. Another interesting thing I made is the random person picker (or lottery). It lets you set up simple filters on the channel's members based on user roles and/or current status, and after filtering the bot will randomly pick a person.
Taking inspiration from that, I have decided I want to build a scoreboard. Like the lottery feature, it uses access to the list of users in a channel. Except it doesn't really, but I'll get to that in a moment. And then, instead of adding value by generating a random number, it adds value by remembering a score for each person. And it should be able to present all participants ordered by score.
I don't need to link it to the users. I can allow any string as a participant and anything that has ever received any number of points is on the scoreboard. I'll need to check that if scores are added using mentioning, it mentions them when reason out the scores as well. That isn't my first concern, though.
The bot started out pretty messy, but eventually I restructured it to have separate modules that can declare which text commands they implements. So, the first step is to copy one of these files, rename the copy, strip it off most of its content and register it in a different files.
I filled out a few details and after restarting the bot, I can already see that the new command shows up in the bot's interactive(ish) help:
These are some of the commands I respond to:
[..]
~scoreboard <scoreboard-name>
Next up is adding a log statement so I can see it does indeed respond to the ~scoreboard command. Yep, works:
Someone said ~scoreboard
Alright, then. Let's start working on some real stuff. First of all, the ~scoreboard
command needs to take exactly one word, which is the name of the scoreboard you want to see. That's something I have done many times already, so I just copy it from another "command module", tweak the exact number and write a new message. Oops, it tells you you need to supply a name if args.length < 2
(the first is the command itself) but doesn't complain about too many words unless args.length > 3
. That's not right of course, but it's easily fixed.
Alright, I'll make a map that stores the scoreboards. If the scoreboard exists, we'll print it. For now, printing whatever the value was will do. If it doesn't, we'll add it. For now, we'll just put the string "This is a scoreboard"
as a value in the map. I also need to tell the user that the scoreboard was created.
It's been a while since I last worked with this API, so I quickly look up in one of the other files how one simply posts a message again. After looking that up, it is easy enough to write the new code, though.
me: ~scoreboard aaa
bot: Scoreboard aaa has been created
me: ~scoreboard aaa
bot: Scoreboard aaa: This is a scoreboard
Next, I add a new command: ~score
. It is a bit more complex (<scoreboard> <user> <score>
, and <score> needs to be a number) so it needs a bit more in the way of validation. It's pretty straight forward though. For a moment, though, it seemed that the number detection wasn't working. After a restart it does work, so I just assume that I didn't save and restart properly the first time.
It's time to make the actual scoreboard. It'll be another map. This means I'll end up with a map that can be addressed like this: score = map[ScoreboardName][user]
(that's not the actual syntax, but it does make the structure clear). In the ~scoreboard
command, I change the creation of a new scoreboard to making a new map and the displaying of a scoreboard into a loop of writing down pairs of names and scores. Then, in the ~score
command, I set the score equal to whatever was passed as the score. We'll make it increase the score later on, but this is good enough to start off with.
Throwing some commands at the bot shows me that things work as expected. However, the bot does not respond when you correctly set a score, which means there is no feedback at all, which feels pretty weird and leaves you unsure about whether it was successful. So, I changed it to respond with the score change repeated in natural language. It also stands out that the output for the ~scoreboard
command s a bit weird when the scoreboard exists but has no scores yet, so I craft a special message for that situation.
With all that behind me, it feels like I actually have a first version of this functionality. So, committing time. We're not quite done, yet, though, as there are a few more things to build. So, I quickly get back to my code.
The next thing is to let the user prepend the score with a +
to increase someone's score, a -
to decrease someone's score or a =
to set a score. This is also a good moment to make sure that not prefixing the number means to increase it rather than setting it.
I used a simple regex to extract the prefix. I needed to check what it exactly would do if there was no prefix, but once I found that the method would spit out an undefined in that situation, it was simple enough to make it behave the same as when the prefix was +
.
In this change, there are two things I did that aren't completely obvious at first. The first of those is that I start off by checking if the user is present in the map yet. If it isn't, I set their score to zero. That way, I have entirely handled this case and I don't have to do specific operations for this corner case.
The other thing is that I do not do different operations for the different prefixes. Instead, I always set the value in the map to the score
variable. However, before that, I do use the value currently in the map to adjust that score variable, keeping the operation in mind. This prevents any duplication in the .set()
call. As an added bonus, the response message ("<user> now has <score> points on scoreboard <scoreboard>") is still correct without any further modifications.
Test it. Commit. Next up is printing the scoreboard.
Currently, I simply dump all the users and their scores when one requests an existing scoreboard. It should of course be sorted on scores, contain ranks and have a bit of formatting. Sorting by score was a matter of calling .entries()
on the map, putting the entries in an array and using sort()
on the array, with a comparing function that sorts by the second element (i.e. the value of the entry) in reverse order (from high to low). For the rank, I then used the key of the array, and for the formatting, I just changed things around a bit and added some markdown.
I left it at that. I just committed my most recent changes and pushed the whole thing to GitHub. There are definite improvements to be made. The biggest might be that scoreboards are lost whenever the application is restarted. By saving them in a (SQLite) database instead of a Map in memory, that limitation can be eliminated. It probably should be, as this limitation basically renders the whole thing unusable. The other big thing is that now anyone can give anyone points, even in DMs to the bot. That's probably not what you want either. The quick fix would be to only accept ~scores from the scoreboard's creator. The better fix would probably to let him specify who has access to it. I'm sort of done with the bot for now, especially when also writing everything here. Besides, this post has gotten quite long already.
One of the fun parts of a pointless project like this is that it doesn't matter that the feature isn't entirely usable. I hope you enjoyed me writing about my experience despite how long it got. Of course, if you're even reading this, I guess I must at least have done something right...
me: ~scoreboard Hogwarts
bot: Scoreboard Hogwarts has been created
me: ~score Gryffindor =0
bot: Please follow the correct format to set a score: [+|-|=].
me: ~score Hogwarts Gryffindor =0
bot: Gryffindor now has 0 points on scoreboard Hogwarts.
me: ~score Hogwarts Hufflepuff =0
bot: Hufflepuff now has 0 points on scoreboard Hogwarts.
me: ~score Hogwarts Ravenclaw =0
bot: Ravenclaw now has 0 points on scoreboard Hogwarts.
me: ~score Hogwarts Slytherin =0
bot: Slytherin now has 0 points on scoreboard Hogwarts.
me: ~scoreboard Hogwarts
bot: Scoreboard Hogwarts#1: Gryffindor (0 points)
#2: Hufflepuff (0 points)
#3: Ravenclaw (0 points)
#4: Slytherin (0 points)me: ~score Gryffindor +1000
bot: Please follow the correct format to set a score: [+|-|=].
me: ~score Hogwarts Gryffindor +1000
bot: Gryffindor now has 1000 points on scoreboard Hogwarts.
me: ~score Hogwarts Hufflepuff +650
bot: Hufflepuff now has 650 points on scoreboard Hogwarts.
me: ~score Hogwarts Ravenclaw +700
bot: Ravenclaw now has 700 points on scoreboard Hogwarts.
me: ~score Hogwarts Slytherin +750
bot: Slytherin now has 750 points on scoreboard Hogwarts.
me: ~scoreboard Hogwarts
bot: Scoreboard Hogwarts#1: Gryffindor (1000 points)
#2: Slytherin (750 points)
#3: Ravenclaw (700 points)
#4: Hufflepuff (650 points)me: ~score Hogwarts Slytherin -25
bot: Slytherin now has 725 points on scoreboard Hogwarts.
me: ~score Hogwarts Gryffindor +200
bot: Gryffindor now has 1200 points on scoreboard Hogwarts.
me: ~score Hogwarts Gryffindor =0
bot: Gryffindor now has 0 points on scoreboard Hogwarts.
me: ~score Hogwarts Gryffindor +600
bot: Gryffindor now has 600 points on scoreboard Hogwarts.
me: ~scoreboard Hogwarts
bot: Scoreboard Hogwarts#1: Slytherin (725 points)
#2: Ravenclaw (700 points)
#3: Hufflepuff (650 points)
#4: Gryffindor (600 points)
Top comments (0)