Minotaur

I started a project nearly 4 years ago to try out a simple turn-based game concept using Phoenix LiveView. I named the project Minotaur based on an initial idea of the game direction, but it’s just a code name or placeholder until I know what this thing might end up being. I spent a few weeks working on Minotaur until I got busy with something else and I didn’t come back to it for a few months. I’ve had cycles of working on it just long enough to upgrade the dependencies, relearn the technologies involved, and add a couple of features before setting it down again for a few months at a time. A few months ago, I picked up the project again and have been able to keep my momentum and spend more of my free time adding features.

I’ve finally got to a point in development where I can say there is a working MVP that could technically be considered to be a “complete” game. The game is a very barebones prototype of a turn-based dungeon crawler where two or more players can join online through a browser and have their character either Move or Attack by spending Action Points which are gained once per turn. There aren’t any computer controlled enemies or items or cool artwork at this point, but I wanted to get to this stage so I can publish a development copy that my friends can try out as I iterate on the project.

Deploying updates in Elixir

I want to create a workflow as I develop the game where I can make small changes at a time and push these updates to the development server. My vision is to automatically deploy a rolling update with the new version of the application without users losing any of their active game progress. I’ll have to consider how state is managed in the application for active games and what options I can leverage with the Erlang VM to achieve my goal.

I don’t have any persisted storage at this point in my project so every time I shut down the application, all of the in-memory state is lost. I chose to defer the decision of where to persist data to storage until it becomes a concern I need to address. Up until now, I haven’t cared about keeping user records saved longer than my development sessions. I will need to think about storage soon, but it isn’t the problem I want to solve next. What I really want to solve is the problem of losing the in-memory state when I need to deploy a new version of the applciation.

When a user joins a game, their browser is opening up a web socket connection with the Phoenix LiveView backend. The clientside Phoenix socket code already includes logic for re-establishing with the backend if the socket connection is lost such as when the LiveView process crashes. This layer of the application should be fairly straight forward to rebuild state after a deploy as long as the LiveView process can recover necessary session and game state on initialization. However, keeping the game state alive between deployments is something I need to handle myself.

The state for each active game is kept in its own generic server process (GenServer) with which LiveView processes can interact through a simple API. When a new version of the application is deployed, the game state needs to be transferred to a new process on the newly deployed application before the previously deployed version terminates.

Fortunately, there are a few tools that can be combined with native Elixir features to achieve my goal. I found a great talk by Daniel Azuma that covers the exact problem I need to solve.

Daniel’s example uses Kubernetes to deploy new versions of the application, but I decided to use Docker Swarm instead which is simpler and sufficient for my case. I’ve used Docker Swarm at work a few years ago before we migrated to Elastic Container Service in AWS so it didn’t take long to initialize a simple single node swarm on my dev server and a new service for the Minotaur application using a compose file. I configured the ports for the server host machine to communicate to the Docker service and updated Nginx to proxy requests to that port.

Next Steps

The dev server is now serving the game through a Docker Swarm service, but I still have these problems to solve:

  • Launch the application as nodes in an Erlang Cluster. This can be handled by the libcluster library.
  • Intercept terminate signals and stash game state when shutting down after new version deployment. May save to a DB or maybe CRDT library?
  • Start new processes for active games which can recover from stashed state. Horde Distributed Supervisors and Registry seem like great options here.
  • Create a deploy process that builds an image of the new version of application code and updates the service to use the new version. Some configuration with Docker Swarm may be needed to ensure the state hand off completes before the old application version is terminated.