During the last semester of my CS degree at Université de Sherbrooke I and three of my classmates started working on a game for JDIS Games, an AI programming competition organized by JDIS, the university's CS club.
As (bad)luck would have it, this semester was the winter semester of the year 2020. This is right when the COVID-19 pandemic started hitting North America. Because of this, the competition was sadly cancelled. It took until the summer of 2022 for the event to be brought back. When talk of a new edition started, it only felt natural to reuse what we had worked on in 2020.
Today, I'd like to do a retrospective of sorts on the project. I'm specifically interested about how using Elixir helped us in some key areas.
First of all, I think it's important to explain a little more about the game and the competition, so everyone can understand what our goals were going into it.
The competition asks participants, usually teams of 2-4 people, to write a smart agent (a.k.a. a bot) whose task is to play a game and score the most points. The game is not revealed until the beginning of the competition and is usually kept pretty simple since this is a single day event. It's also worth noting that in order to make interfacing with it practical, the game is usually written from scratch by the organizers and "starter packs" are provided so the participants don't have to waste time getting networking and the like working.
Finally, the term "AI" is used in its simplest form here. While more advanced techniques are not forbidden, they're usually not practical given the strict time constraints and simplicity of the game. We're mostly talking about a bunch of
ifs and small pieces of logic to calculate pathing, targeting and all kinds of decision making, depending of the game's nature.
For this edition, we decided to built a simplified version of Diep.IO, originally created by Matheus Valarades.
I'm not gonna go over the whole project, but I want to point out a few key areas where Elixir helped us achieve our goals really quickly and efficiently.
The code for the game is open source. Feel free to check it out if you want more details about the things I'm about to talk about below. I'll also be linking interesting files for each feature.
For this competition, we decided that participants would run their bot on their own computers and communicate with the server through Phoenix Channels. Channels gave us a very easy way to set up websocket communication, complete with simple authentication, JSON encoding/decoding, and room isolation. That last point was especially nice because we wanted to offer a secondary, non-ranked game that bots could use to test new strategies, which was very easy to do with topics and pattern matching.
Phoenix also made it very easy to set up different channels for game state updates (used both by the bots and the spectating web view) and bot commands.
One downside of this approach is that while Phoenix Channels have libraries available for a few languages, there isn't one for Python, which we knew would very likely be the first choice of most participants for writing their bots. We ended up writing a partial channels implementation with just enough functionality to meet our needs. I think it was still worth it overall, as the amount of work we would've save on the Python side probably would've come at the cost of a similar amount of work on the Elixir side. Besides it was a nice opportunity to learn a bit more about how Channels work under the hood.
In a competition like JDIS Games, there will inevitably be times when participants break their bot and will need some time in order to bring it back online. To reduce the impact of these events, we opted to store the latest command received from each bot and just keep repeating the same command until a new one is received.
ETS was a very simple and effective way to do this: we wanted something that would support concurrent writes to avoid blocking communication channels and we didn't want to go to a database for this, as there was no need to keep this after a game had ended. We went with a default
set table with
write_concurrency enabled as we had multiple writers (one for each bot) but only one reader (the game loop).
For more information, see the ActionStorage module.
For the game loop itself, we used a simple GenServer. For each iteration of the loop, we would:
- Get bot commands from the action storage
- Run through the game logic (collision detection, upgrades management, updating scores, etc.)
- Broadcast the updated state through Phoenix PubSub. Our
GameStateChannelwould subscribe to the PubSub and send the game state to the clients.
- Calculate how long the iteration took
- If enabled in the game parameters, send performance metrics to another process.
- Either trigger another iteration with
Process.send_afterbased on how long the current iteration took or start a new game altogether if the timer has run its course.
This was honestly where Elixir shined the most for this project. The concurrency model of the BEAM made it super easy to have all these pieces work and communicate together without worrying at all about synchronization, deadlocks, resource starvation or anything else of the sort.
At the same time, this is probably where our lack of experience also shows the most. Our implementation of the game loop wasn't exactly the most consistent: we were sending game state information to clients 3 times per second, but we saw variations in the timings of updates of up to ~20 milliseconds at times. While it wasn't an issue for us, being a single day competition targeting mostly students, it could have been problematic in a higher level setting where participants would be expected to run more complex algorithms, making the exact timing of the game updates (and thus the amount of time available to run said algorithms) more important. If I to guess, I'd say relying on the same process for handling core functionality and timing calculations wasn't the greatest idea. I haven't investigated this at all, but I suspect that scheduling iterations outside of the game loop itself probably would've led to a more stable result.
For more information, see the Gameloop module.
This game was my first big project using Elixir, and two years of on-and-off side projects later, it's still probably the one I had the most fun building. Games have such different requirements compared to the web stuff most of us do day to day, it's a very nice change of pace.
I'm also very happy we used Elixir for it. The language, virtual machine and overall ecosystem make it such a fun and rewarding programming experience. This really is the project that got hooked into the community, and I can confidently say that I'm here to stay!
I couldn't finish this article without saying a big thank you to JDIS for organizing the event, and to my teammates who helped build the game (in alphabetical order):