A bot that moderates a game

Two Rooms One Boom is a game which I learned how to play in Year 9. We called it Communists and Americans but I think the game developers wanted something more PG. To play it, you need to deal each player a card. You can't really do this online, so I built a bot which does it for you.

Discord is like Skype for gaming

Discord is just a place where you can chat with friends and was originally made with gaming in mind. They've now changed their marketing focus but overall, it's still just a place to play games online.

There are audio channels and video sharing, as well as text.

Discord allows you to create bots which do thing automatically based on commands.

So, I decided to make a Discord bot which deals cards out to each player who wants to play, randomly.

Discord bots work using webhooks

The way I understand this works, Discord makes a request to your server every time a message is written (or other events which your bot listens). This request just contains the user, the message content, and maybe some metadata. With this, your bot code needs to figure out what to do, then send a response.

This is much more clever than how I thought it works, which is just constantly making requests to the server, checking for new messages.

How the request to discord is made

There's a Ruby package which does a lot of this for you

Since all Discord bots have the same underlying structure, there are packages for every language which simplifies the process of creating a bot.

Instead of creating a server, doing OAuth with Discord, etc. All you have to do is run:

bot.run

Abstraction is pretty great!

I decided to use discordrb because I wanted to write this in Ruby. The documentation is okay, but I had to look through the source code to understand what's going on. This wasn't as bad as it sounds, though, as the code has great comments which explain everything.

The game itself isn't complicated

The bit I needed to worry about isn't the functionality of the actual game, it's just the automated bits:

  • Let players join the game
  • When the game start, each player gets a card
  • Players need to be able to show each other their card
  • A timer for keeping track of rounds

Let's get started!

Programming is fun

Here's how I went about programming the game.

Letting players join the game

I created an array, joined_player and every time the /join command was run, the author of that message was added to the array:

joined_users = [] bot.command :join do |event| return ":x: can't join the game, it's already running" if is_game_started user = event.user  return ":x: you're already in the game" if joined_users.include? user joined_users.push(user) ":dancer: #{user.name} has joined the 2r1b game."end

When the game starts, give out cards

Cards are called "roles" in this game

When the /start command is run, we need to divide the roles between all players. There's an array called ROLES which contains all the roles available in the game. For each user that has joined, we give them a random role and delete that role from the remaining_rolesarray.

We also split the players into two rooms, and tell them which room they're in (this is another part of the game).

bot.command :start do |event| return ":x: game is already started, `/stop` to stop" if is_game_started  return ":x: not enough players to start" if joined_users.size < 4 remaining_roles = ROLES  is_game_started = true joined_users.each do |u| # Get a random role    role = remaining_roles.sample # Set the user's role    u.role = role # Delete from possible roles array    remaining_roles.delete(role) # Let the user know about it    u.pm(role.to_s)  end # Split into teams  half = joined_users.size/2.to_f room_a = joined_users.sample(half.ceil)  room_b = joined_users - room_a ":rocket: Started the game! I've DM\'d #{joined_users.size} players their"\ " roles. Use `/show @user#1234` and `/swap @user#1234` to show or swap"\ " cards with a user. \n\n"\ " **Room A**: #{room_a.map { |u| u.name }.join(', ')}\n"\ " **Room B**: #{room_b.map { |u| u.name }.join(', ')}"end

Showing your card

Players need a way of showing their card. This is a mechanic in the game. They can either show their entire card (/show_full) or just their team name (/show_team). Here's the code:

Users will enter a reference to the user they want to show their card to. For example, player A might show their card to player B like this: /show B. The library was a bit out of whack on this one, so I had to write my own function which extracts the user from the command arguments (user_from_args)

def user_from_args(args, bot)  user_req = Discordrb::API::User.resolve("Bot #{TOKEN}", args.first[2...-1])  Discordrb::User.new(JSON.parse(user_req.body), bot)end

These commands define /show_full and /show_team:

.pm sends a private message to the user

bot.command :show_full do |event, *args| user_from_args(args, bot).pm(    "**#{event.user.name} has shown you their card**: #{event.user.role.to_s}"  )end bot.command :show_team do |event, *args| user_from_args(args, bot).pm(    "**#{event.user.name} has shown you their team**: #{event.user.role.team}"  )end

Finally, the timer

Each round lasts a few minutes, so I added a timer. This has to be on another thread to make sure that other commands can still be processed.

def start_timer(duration, channel)  Thread.new {    sleep duration    channel.send_message("--- **Timer for #{duration/60} minutes is over!!** ---")  }end timer = nil ### Start a timer of (arg) minutes bot.command :timer do |event, *args| return ":x: no duration specified" unless args.first  duration = args.first.to_f timer = start_timer(duration * 60, event.channel) "starting a timer for #{duration} minutes"end bot.command :stop_timer do |event| return "no timer set." if timer.nil? timer.kill  "stopped the timer"end

Conclusion

This was a nice project, and it only took an afternoon to make. The next steps for this project are to abstract a few things into classes. Also, to automate a few of the commands (like starting a timer for each round). Further, there is a lot of game functionality that isn't added here, so I could add that too.

For an extra challenge, I could also make this work for many different games at the same time.

All the code is on Github

Subscribe to Ori Marash

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe