The Chrome Dino Game

We’ll be trying to create an AI system that’s able to play this game like any human would.

Our first task was to simulate the Chrome Dinosaur game (this is where GitHub helped :P). For this we found an open-source repo that had the complete code of the Chrome Dino game. We’ve formatted it to make the code more readable and organized.

Let’s work through it step by step :)

The main class that contains the dinosaurs and events related to playing the game is called Runner.js . It’s designed to allow us to use multiple dinos simultaneously (We’ll be using this feature in the next blog post).

We’ve also added three events to this class :

onCrash onReset onRunning

These are the three primary events into which the game can be divided. The onCrash method is called when the dino crashes, onReset is called after onCrash to reset the game, and onRunning is called at every instance of movement to decide whether the dino should jump or not.

You can check the reference code here:

The src/game folder contains the files used to replicate the Chrome Dino game. runner.js is exported from here so that nn.js , where the main artificial intelligence code is written, can access the above-mentioned functions. The index.html file creates HTML divs , where we’ll inject the game and add the scripts.

Brilliant!! Hope you’re with me so far :p Let’s see how to set up our project.

Setting up

src/nn.js

The first step is to define imports.

We’re using babel-polyfill , which allows us to use the full set of ES6 features beyond syntax changes. It also includes features such as new built-in objects like WeakMaps.

Now we can import the tensorflow library as a tf object.

We’ll also import canvas width and canvas height to use feature scaling, and obviously the Runner class.

After that, we can initialize the instance of runner class as null.

We’ve created a function named setup that will be called after the DOM content is loaded.

In the setup function, we initialize the runner instance with a DINO_COUNT of 1 and assign functions to onReset , onCrash , and onRunning events. As mentioned earlier, the Runner class is designed so we can have multiple dinos play the game simultaneously. Here, DINO_COUNT signifies the number of dinos we want to run the current simulation with.

Assign the runner object to window.runner for global access and call the runner.init() function, which starts the game.

Seems like we are “set up” and ready to go :)

Handling Resets

We create a variable named firstTime , which tells us whether the Dino game is being played for the first time or the current game is a reset. This way, we can use the same function with an if condition inside it to handle resets.

The handleReset method takes an array of dinos as an argument. The runner class creates an array of dinosaurs that can be used for playing the game with multiple dinosaurs. For the purposes of part one of this series, we’re using only one dino.

Since we’re only using one dino, let’s just take the 0th element of the dinos array.

If it’s the first time this function is called, we initialize the model in the dino.model object. We create the model using the tf.sequential() call which returns a sequential model. Then, we’ll be adding two layers to the model.

The neural net will take three inputs in the beginning—namely, the parameters that define the state of the dino, i.e. the speed of the game, the width of the oncoming obstacle and it’s distance from our dino.

Therefore the first layer has an input shape of [3] that’s a 2D tensor array, such as [ [1 , 1 , 0] ], to account for the three inputs. The activation function we’ve used is the basic sigmoid function that will output six units for the next output layer.

This is the second output layer with six inputs coming from the previously hidden layer.

The Activation function is again sigmoid . What do you think? How many units do we need in the output layer now? It will be two units, right? One for a dino to jump [0,1] and one for a dino to not jump [1,0].

We finally compile the model using meanSquaredError loss function and adam optimizer with a learning rate of 0.1. Feel free to play around with this learning rate :)

We’ve also created two arrays inside a dino.training object that will keep our training set as 2 arrays named inputs and labels .

Now, if this isn’t the first time reset has been called, we’ll train our neural network using the model.fit function of TensorFlow models. This function takes two tensors as vectors, where the first argument is the input tensor with the shape of the first layer’s input, and the next argument is the appropriate output tensor, which again is of the shape specified in the arguments of the model’s last layer. We’ll be using the tensor2d function of the TensorFlow api to convert these normal 2D arrays to tensors.

Awesome, we’ve created our model and written code for training it.

So now…

It’s Prediction Time!

The prediction part of our model will obviously be used in handleRunning , as that’s where we’ll decide what to do next.

The handleRunning method takes dino and state as arguments. The state is the current condition of the Runner — it contains the distance of the next object, its width, and the speed of the game. It returns a promise that’s resolved using the action the dino is required to take.

In the callback for the promise, we’ll give one argument to the arrow function, which is the resolve callback. If the dino is currently not jumping, we’ll predict the next action using the model.predict method, which in turn calls the ConvertStateToVector method, taking a state object as an input and returning a feature scaled vector. Then we’ll call the tf.tensor2d method to convert this array to a tensor and will call the predict function on it.

The model.predict method returns an object. That object has a data method that returns a promise. The then function of that promise takes a callback with a result as an argument. It’s this result that contains the prediction in the form of a simple array.

Since we defined [0,1] as the jump output, we compare result[1] and result[0]: if result[1] is greater than result[0], the dino should jump; otherwise, the dino should keep running.

If it chooses to jump, we’ll set the action as 1 and set the lastJumpingState as a current state, as we should choose to jump at this state.

If it chooses to not jump we’ll set the lastRunningState as current state, as we chose to run at this state.

In the end, we’ll resolve the promise with the action required of the dino (0 already as it was not jumping, 1 if it predicts a jump)

resolve(action);

If the dino was already jumping in the current state, we resolve with the code for running (0)

resolve(0);

Phew! We’ve finally decided how we’re going to act. Now let’s handle failures i.e. the crash. This is also where we’ll create our training data.