A while back, I programmed the Game of Life. No, not the board game. The game of life is a cellular simulation, where you have a grid of cells, each of which is either alive or dead. At each turn, the following rules are executed:
- Any live cell with fewer than two live neighbours dies, as if caused by under-population.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overcrowding.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Here is an example of the “glider gun” configuration of the game of life.
It was a fun project, and I learned a lot from it, but I never took the visualization aspect beyond displaying it in the console. Last week, I decided it was time to change that and move it into a Sinatra application, with an AngularJS font-end. I also got to work with the lovely Anisha Ramnani, since she was tackling the same problem.
The Logic of the Game
I kept the game logic the same as when I first built it in Ruby, so all the processing of game moves was going to still take place server-side. I made two routes, “/random_board” and “/tick_board”. random_board created a new game board, initialized a random starting move, and processed the first 50 turns, storing them in an array. I then stored the last board object in a session variable so that tick_board would have access to its last state. My code looked like this:
My tick_board was similar to random_board, except instead of initializing a new board with random new cells, it would take the board object stored in the session. At the end of the 50 moves, it then replaced the session[:board] variable to be the current board state. I also made sure to return the results as JSON strings, so that they would be easily accessible to my Angular app when it received data from the server.
And then I ran into my first major hurdle. random_board was able to process the first 50 moves, and I was able to verify that the correct board state was stored in session[:board]. However, tick_board threw an error. Upon further investigation, I realized that my session wasn’t working. For some reason, tick_board thought session[:board] was nill, when I had verified that I had stored it in random_board.
Sessions, Cookies, and Pool
I investigated my session problem, and soon discovered the cause. Sessions are stored in cookies, and cookies can typically only store a very small amount of data. Most certainly less that my entire game board object! I dug around some more and considered sending the game state from the client to the server, but I preferred to keep this state on the server if possible. I eventually found a solution. In Sinatra, to enable sessions, you normally need to type
enable :sessions, and then you can access them in a hash, which is stored in a cookie. However, Rack has another option, which requires you to instead type
use RACK::SESSION::POOL. Under the hood this works a little different to allow you to store more data in your session. It does this by only storing an id in the cookie. This id references the rest of the session hash, which is stored in @pool instead of a cookie, and can therefore be much larger than what a session cookie could hold. Using Pool is faster than using cookies, but a drawback is that all sessions will be lost if your app restarts. However, for the purpose of the Game of Life I decided that was an acceptable tradeoff. And with that, my game of life could run! On the server at least.
Sinatra? Angular? Wat?
With the server-side components of my Game of Life in a good state, I moved on to Angular. This was my first time using Angular on my own, so there was a lot of learning along the way. When integrating with Sinatra, the entire Angular app will live within your public directory.
In order to make sure my Sinatra app could find my Angular app, I had to make sure two components were in my app.rb file. I needed to first make a configure do block, which would set the new root and public folder for my app. Next I needed to set up a route in Sinatra so that it would look for my Angular app in the public directory.
My next step was to create a basic html file. This is the Angular equivalent of a layout.html file in Sinatra or Rails. The key things to note for Angular are the custom attributes – here they are
After establishing my index.html, I got to work on my app.js file, which contains the routes for the Angular app. I first had to establish what my app was names, based on the ng-app attribute in my index.html. Next I established a route for my Angular app. In a url, anything past a # sound is not sent to the server, and angular routes take advantage of that. I established the route “/random”, and so this would actually be at the url “/#/random”.
Each route in an angular app needs two components. The first is the templateUrl, which is where the partial, or view, for that route is in your public.html directory. The second is the controller, which will be the name of the controller you create in your controller.js file.
All About Control
Moving on to the controller, the controller function takes two arguments. First is the name of the controller targeted int he associated route in app.js, then is an array of dependencies for the controller, ending in an anonymous function. Any arguments you want to pass to your anonymous function should also be in this array. In this case, the dependencies were $scope – which the data served up is given to, $http – which is needed to make requests to the server, and $timeout – which I needed to control the time between ticks of the board.
Inside the controller, I sent an http request to the server to get the initial state of the board. However, because of the way I constructed the random_board route in Sinatra, this request gets more than the initial state of the board. It actually gets the starting state, plus the first 50 moves, all stored in an array. This data was bound to $scope.boards. I then created a recursive function, nextMove. The trick to nextMove is that it returns the item in the array at index 0 each time it runs, and permanently removes that item from the array. This allows me to continually move through the array until the moves are exhausted. The $timeout function calls this function once every second, and to keep the game going continually the controller sends a get request to tick_board every time the array of queued moves dips below 20. Since this information is retrieved asynchronously I made sure I allowed ample time for the new data to be received and I simply add it on to the end of the array. Since the data to calculate the next moves is remaining server-side instead of having to be sent from the client via a post request, my server won’t lose track of where it is in the game and the game will keep running.
One issue I ran into initially was that each time my array of moves dropped below the threshold, a get request was sent to tick_board once a second until the data from a request was received back and the array increased in size. To avoid this, I added in a loading variable, which simply keeps track of if the client is waiting for information. This way the client will not send a new request until the new data is received.
Oh, What a View
Finally, the last key component of my app, the view. In Angular, the views are partials – snippets of html that are switched in and out according to what route you’re at.
Since my game board is a nested array, I wanted to iterate through each board to find and display each row, and then iterate through each row to find and display each cell. Angular has a handy custom attribute specifically used to iterate through a set of data, ng-repeat. Since I only wanted to look at one board at a time, I limited my iteration to only look at board to find the rows. However, once I started trying to iterate over the rows I ran into trouble. In Angular, you cannot have multiple iterations on an item with the same name. So basically, the second row I looked at, when I called
ng-repeat="cell in row", Angular would notice that I had already looked at “row” and throw an error. In order to get around this, I had to add in “track by $index”, which will force Angular to look at the index of the item instead of just the name.
And thus, finally, I had gotten the Game of Life into the browser to come alive in full color. It was a wonderful project, and definitely helped me better understand AngularJS, and how to integrate it with a Sinatra app.