Onepagecode

Onepagecode

Share this post

Onepagecode
Onepagecode
Flappy Bird Which Learn And Play By Itself

Flappy Bird Which Learn And Play By Itself

In this article, we will explore the fascinating world of neural networks and game development by creating a Flappy Bird game that is powered by a neural network.

Onepagecode's avatar
Onepagecode
Mar 10, 2023
∙ Paid

Share this post

Onepagecode
Onepagecode
Flappy Bird Which Learn And Play By Itself
Share

The game will be built using JavaScript, and we will also create a separate file for the neural network code, which we will call neuroevolution.js.

We will start by discussing the basics of neural networks and how they work. We will then delve into the details of the game mechanics and how we can implement them using JavaScript. We will also discuss how we can train the neural network to play the game and keep on playing infinitely, until we manually stop it.

Throughout the article, we will be using a step-by-step approach that will make it easy for readers to follow along, even if they have no prior experience with neural networks or game development. We will also provide explanations and code snippets to help readers understand the concepts and implement the code.

By the end of the article, readers will have a solid understanding of neural networks and how they can be used in game development. They will also have a working Flappy Bird game that is powered by a neural network, which they can customize and experiment with.

First we create and html file, in which we can see what’s happening:

<html>
<head>
 <title>NeuroEvolution : Flappy Bird</title>
 <link href='https://fonts.googleapis.com/css?family=Oswald' rel='stylesheet' type='text/css'>
</head>
<body>
 <canvas id="flappy" width="500" height="512"></canvas> <br/>
 <button onclick="speed(60)">x1</button> 
 <button onclick="speed(120)">x2</button> 
 <button onclick="speed(180)">x3</button> 
 <button onclick="speed(300)">x5</button> 
 <button onclick="speed(0)">MAX</button> 
 <br/>
 <a href="http://onepagecode.substack.com">Substack Repository</a>
 <script src = "Neuroevolution.js"></script>
 <script src = "game.js"></script>
</body>
</html>

Now let’s start coding the game:

Game.js

(function() {
 // Create the timeouts array
 var timeouts = [];

 // Create a random message name
 var messageName = "zero-timeout-message";

 // Create the setZeroTimeout function
 function setZeroTimeout(fn) {
  timeouts.push(fn);
  window.postMessage(messageName, "*");
 }

 // Create the handleMessage function
 function handleMessage(event) {
  // Check if the source of the message is the window
  // and the data matches the messageName
  if (event.source == window && event.data == messageName) {
   // Stop propagation of the event
   event.stopPropagation();
   // Check if there are any timeouts
   if (timeouts.length > 0) {
    // Get the first timeout from the array
    var fn = timeouts.shift();
    // Execute the function
    fn();
   }
  }
 }

 // Add the event listener
 window.addEventListener("message", handleMessage, true);

 // Set the setZeroTimeout function on the window object
 window.setZeroTimeout = setZeroTimeout;
})();

This code defines a setZeroTimeout function that allows a function to be executed in the next event loop iteration, but with zero delay, essentially providing a way to bypass the minimum delay enforced by setTimeout and setInterval in some browsers.

The setZeroTimeout function works by pushing the function to be executed into an array of timeouts, and then posting a message to the window object. When the window receives the message, it checks if there are any timeouts in the array, executes the first one, and removes it from the array.

The code also creates a handleMessage function that handles the message posted by setZeroTimeout, and adds an event listener for the message event to the window object. Finally, the setZeroTimeout function is attached to the window object, making it available for use throughout the script.

var Neuvol;
var game;
var FPS = 60;
var maxScore=0;

var images = {};

var speed = function(fps){
 FPS = parseInt(fps);
}

In the above code, we create some variables which will be useful in building he game.

var loadImages = function(sources, callback){
 // nb is the number of images to load
 var nb = 0;
 // loaded is the number of images that have been loaded so far
 var loaded = 0;
 // imgs is an array of images
 var imgs = {};
 // for each source in sources
 for(var i in sources){
  // increment nb
  nb++;
  // set imgs[i] to a new image
  imgs[i] = new Image();
  // set the source of imgs[i] to the current source
  imgs[i].src = sources[i];
  // add an onload event on imgs[i]
  imgs[i].onload = function(){
   // increment loaded
   loaded++;
   // if loaded == nb
   if(loaded == nb){
    // call callback(imgs)
    callback(imgs);
   }
  }
 }
}

The loadImages function takes two arguments: sources, an object containing the source paths of the images to be loaded, and callback, a function to be called once all images are loaded.

Within the function, nb is set to the number of images to be loaded, which is the length of the sources object. loaded is initialized to 0, which will be incremented each time an image is loaded. imgs is an empty object that will be populated with the loaded images.

A for loop is used to iterate over the sources object. Within each iteration, nb is incremented and a new Image object is created and assigned to imgs[i]. The source of the image is set to the current source in the loop. An onload event listener is added to the image, which will be triggered once the image is loaded. Within the event listener, loaded is incremented and if loaded is equal to nb, the callback function is called with the imgs object as an argument.

Essentially, the loadImages function asynchronously loads multiple images and executes a callback function when all the images are loaded.

Creating the bird object:

// this function creates a new bird object
var Bird = function(json){
 // the position of the bird
 this.x = 80;
 this.y = 250;
 // the size of the bird
 this.width = 40;
 this.height = 30;

 // this variable determines if the bird is alive
 this.alive = true;
 // the gravity of the bird
 this.gravity = 0;
 // the velocity of the bird
 this.velocity = 0.3;
 // the jump of the bird
 this.jump = -6;

 this.init(json);
}

// Make a new Bird object using the data from the JSON object
Bird.prototype.init = function(json){
 // Loop through each property in the JSON object
 for(var i in json){
  // Copy the value from the JSON object to this object
  this[i] = json[i];
 }
}

// Add a new method to the Bird object called flap.
Bird.prototype.flap = function(){
 // Set the gravity speed to the jump speed.
 this.gravity = this.jump;
}

// Add more detailed comments to your code to describe each step
Bird.prototype.update = function(){
 // This function updates the bird's position on the screen
 this.gravity += this.velocity;
 this.y += this.gravity;
}

Bird.prototype.isDead = function(height, pipes){
 // If the bird is off the screen, return true
if(this.y >= height || this.y + this.height <= 0){
  return true;
 }
// For each pipe
for(var i in pipes){
 // If the bird is not touching the pipe
 if(!(
  // If the bird is to the right of the pipe
  this.x > pipes[i].x + pipes[i].width ||
  // If the bird is to the left of the pipe
  this.x + this.width < pipes[i].x || 
  // If the bird is below the pipe
  this.y > pipes[i].y + pipes[i].height ||
  // If the bird is above the pipe
  this.y + this.height < pipes[i].y
  )){
  // Return true
  return true;
 }
}
}
var Pipe = function(json){
 // Define the default values for our pipe
 this.x = 0;
 this.y = 0;
 this.width = 50;
 this.height = 40;
 this.speed = 3;

 // Initialize the pipe with the values passed in
 this.init(json);
}

This is a constructor function for a Pipe object in JavaScript. The Pipe function takes in a single argument, json, which is expected to be a JSON object containing properties to initialize the Pipe object with.

Inside the Pipe function, there are several default properties being defined for the Pipe object, i ncluding x, y, width, height, and speed. These properties are initialized with default values.

After defining the default properties, the init method is called on the Pipe object with the json argument passed in. The purpose of this method is to update the default properties with any values passed in via the json argument.

So essentially, this constructor function creates a Pipe object with default properties, and then initializes those properties with any values passed in via the json argument.

Pipe.prototype.init = function(json){
 // iterate through each property in the json object
 for(var i in json){
  // add the property and value to the Pipe object
  this[i] = json[i];
 }
}

This is the implementation of the init method of the Pipe object. As mentioned in the previous explanation, this method is responsible for updating the default properties of the Pipe object with any values passed in via the json argument.

The init method takes in the json argument, which is expected to be a JSON object containing properties to update the Pipe object with.

Inside the method, a for loop is used to iterate through each property in the json object. For each property, the method sets the corresponding property of the Pipe object to the value of the property in the json object. This is achieved by using bracket notation to access the property of the Pipe object with the same name as the property being iterated over in the json object.

In essence, this method allows you to initialize or update the properties of a Pipe object by passing in a JSON object containing key-value pairs of properties and their corresponding values.

// this function will be called every frame
Pipe.prototype.update = function(){
 // move the pipe to the left
 this.x -= this.speed;
}

This is the implementation of the update method of the Pipe object. This method is responsible for updating the position of the Pipe object.

The update method doesn’t take in any arguments, it simply updates the x property of the Pipe object by subtracting the speed property from it. This moves the Pipe object to the left.

In summary, the update method is called every frame (or at a specified interval) to update the position of the Pipe object by moving it to the left by an amount determined by the speed property.

Pipe.prototype.isOut = function(){
 // if the pipe is out of the canvas, return true
 // if the pipe is in the canvas, return false
 if(this.x + this.width < 0){
  return true;
 }
}

This is the implementation of the isOut method of the Pipe object. This method is responsible for determining whether the Pipe object is outside of the visible area of the canvas.

The isOut method doesn’t take in any arguments, it simply checks whether the sum of the x and width properties of the Pipe object is less than 0. If this condition is true, then the Pipe object is outside of the visible area of the canvas and the method returns true. Otherwise, the Pipe object is still visible and the method returns false.

In essence, the isOut method is called every frame (or at a specified interval) to check whether the Pipe object has moved outside of the visible area of the canvas. If it has, then the game can take appropriate action, such as removing thePipe object or resetting its position.

var Game = function(){
 //create an array of pipes
 this.pipes = [];
 //create an array of birds
 this.birds = [];
 //initial score is 0
 this.score = 0;
 //canvas is the flappy canvas
 this.canvas = document.querySelector("#flappy");
 //the context of the canvas is 2d
 this.ctx = this.canvas.getContext("2d");
 //width of the canvas
 this.width = this.canvas.width;
 //height of the canvas
 this.height = this.canvas.height;
 //spawn interval is 1.5 seconds
 this.spawnInterval = 90;
 //interval is 0
 this.interval = 0;
 //generation is 0
 this.generation = 0;
 //create an array of generations
 this.gen = [];
 //number of birds alive is 0
 this.alives = 0;
 //background speed is 0.5
 this.backgroundSpeed = 0.5;
 //background x is 0
 this.backgroundx = 0;
 //max score is 0
 this.maxScore = 0;
}

This is the implementation of the Game object constructor. This constructor initializes a new Game object with default values for its properties.

The Game object has several properties:

pipes: an array that will contain Pipe objects.

birds: an array that will contain Bird objects.

score: the current score of the game, which is initially set to 0.

canvas: a reference to the HTML canvas element that the game will be rendered on.

ctx: the 2D rendering context of the canvas.

width: the width of the canvas.

height: the height of the canvas.

interval: a counter to keep track of the number of frames since the last pipe was spawned.

generation: the current generation of birds.

gen: an array that will contain all of the generations of birds.

alives: the number of birds that are still alive.

backgroundSpeed: the speed at which the background will move.

backgroundx: the x position of the background.

maxScore: the highest score achieved in the game.

In summary, the Game object constructor sets up the initial state of the game by initializing default values for its properties.

Game.prototype.start = function(){
 // reset the game
 this.interval = 0;
 this.score = 0;
 this.pipes = [];
 this.birds = [];

 // get the new generation from Neuvol
 this.gen = Neuvol.nextGeneration();
 for(var i in this.gen){
  var b = new Bird();
  this.birds.push(b)
 }
 this.generation++;
 this.alives = this.birds.length;
}

This is the implementation of the start method of the Game object. This method is responsible for resetting the game and starting a new generation of birds.

When the start method is called, it resets the interval, score, pipes, and birds properties of the Game object to their default values. It then retrieves a new generation of birds from the Neuvol object (presumably an external neural network library), and creates a new Bird object for each bird in the generation. These Bird objects are then added to the birds array of the Game object.

After creating the new generation of birds, the generation property of the Game object is incremented, and the alives property is set to the number of birds in the birds array.

In summary, the start method of the Game object resets the game state and starts a new generation of birds, which will be used to play the game.

Game.prototype.update = function(){
 // Move the background
this.backgroundx += this.backgroundSpeed;

// Keep track of the distance to the next pipe
var nextHoll = 0;

// Make sure there is at least one bird alive
if(this.birds.length > 0){
 // Loop over all pairs of pipes
 for(var i = 0; i < this.pipes.length; i+=2){
  // Check if the next pipe is after the bird
  if(this.pipes[i].x + this.pipes[i].width > this.birds[0].x){
   // Calculate the distance to the next pipe
   nextHoll = this.pipes[i].height/this.height;
   // Stop looping
   break;
  }
 }
}

This is the implementation of the update method of the Game object. This method is responsible for updating the game state during each frame of the game.

Firstly, the method moves the background by updating the backgroundx property of the Game object by the backgroundSpeed value.

Next, the method checks if there is at least one bird alive. If there is, the method loops over all pairs of pipes (presumably representing the obstacles in the game). For each pair of pipes, the method checks if the next pipe is after the bird. If it is, the method calculates the distance to the next pipe by dividing the height of the pipe by the height of the canvas. Finally, the loop is stopped using the break statement.

The purpose of the nextHoll variable is to keep track of the distance to the next pipe, which will be used later to determine the fitness of each bird in the game.

for(var i in this.birds){
  // if the bird is alive
  if(this.birds[i].alive){

   // get the inputs for the bird
   var inputs = [
   this.birds[i].y / this.height,
   nextHoll
   ];

   // compute the output of the neural network
   var res = this.gen[i].compute(inputs);
   // if the output is greater than 0.5, flap
   if(res > 0.5){
    this.birds[i].flap();
   }

   // update the bird
   this.birds[i].update();
   // if the bird is dead (out of the screen or collided with a pipe)
   if(this.birds[i].isDead(this.height, this.pipes)){
    // the bird is dead
    this.birds[i].alive = false;
    // decrease the number of birds alive
    this.alives--;
    // compute the score of the bird
    Neuvol.networkScore(this.gen[i], this.score);
    // if this was the last bird alive
    if(this.isItEnd()){
     // start a new generation
     this.start();
    }
   }
  }
 }

 // Update the pipes
for(var i = 0; i < this.pipes.length; i++){
 this.pipes[i].update();
 // If the pipe is out of the screen
 if(this.pipes[i].isOut()){
  // Remove it from the list
  this.pipes.splice(i, 1);
  // Move one step back in the list
  i--;
 }
}

This block of code is responsible for updating the state of the game. It loops through each bird and checks if it is alive. If it is, it gets the inputs for the bird, which includes its current y position and the distance to the next hole in the pipes. It then computes the output of the neural network and flaps the bird if the output is greater than 0.5. The bird’s position is then updated, and if it is dead (i.e., out of the screen or collided with a pipe), it is marked as dead, its score is computed, and the number of birds alive is decreased. If there are no more birds alive, the game starts a new generation.

After updating the state of the birds, the code updates the state of the pipes. It loops through each pipe, updates its position, and removes it from the list if it is out of the screen.

 if(this.interval == 0){
  // Create a new pipe each 100 frames
  var deltaBord = 50;
  var pipeHoll = 120;
  // Compute a random position for the pipe holl
  var hollPosition = Math.round(Math.random() * (this.height - deltaBord * 2 - pipeHoll)) +  deltaBord;
  // Create the two pipes
  this.pipes.push(new Pipe({x:this.width, y:0, height:hollPosition}));
  this.pipes.push(new Pipe({x:this.width, y:hollPosition+pipeHoll, height:this.height}));
 }

 // Add one to the interval.
this.interval++;

// If the interval has reached the spawn interval.
if(this.interval == this.spawnInterval){

 // Reset the interval.
 this.interval = 0;
}

 // Add 1 to the current score
this.score++;
// Update the maximum score if the current score is greater than the max score
this.maxScore = (this.score > this.maxScore) ? this.score : this.maxScore;

// Store the current object in the variable self - this is needed to avoid a conflict
// with the keyword "this" in the setTimeout function
var self = this;

// If the FPS is set to 0, call the update function using setZeroTimeout
if(FPS == 0){
 setZeroTimeout(function(){
  self.update();
 });
// Otherwise, call the update function using setTimeout
}else{
 setTimeout(function(){
  self.update();
 }, 1000/FPS);
}
}

This is the end of the code for the Flappy Bird game. The update function updates the state of the game and calls itself again after a certain amount of time using either setTimeout or setZeroTimeout.

The first section of the update function moves the background, computes the distance to the next pipe, and checks if there is at least one bird alive. Then, for each bird that is alive, it computes the inputs for the neural network (the y-position of the bird and the distance to the next pipe), computes the output of the neural network (flap or not), updates the bird’s position, and checks if the bird is dead. If a bird is dead, its score is computed using the Neuvol object, and if all birds are dead, a new generation is created.

The second section of the update function updates the position of each pipe, removes pipes that are out of the screen, and creates new pipes every certain number of frames.

The last section of the update function updates the score and calls the update function again after a certain amount of time. If FPS is set to 0, it uses setZeroTimeout, which is a more accurate version of setTimeout that uses the browser’s event loop to avoid long-running JavaScript code from blocking the UI.

Game.prototype.isItEnd = function(){
 // loop through all the birds
 for(var i in this.birds){
  // if any of the birds are still alive
  if(this.birds[i].alive){
   // then the game is not over
   return false;
  }
 }
 // if we got here then all the birds are dead
 // so the game is over
 return true;
}

The isItEnd() function in the Game class checks if any bird is still alive in the game. It loops through all the birds and checks if any of them have the alive property set to true. If it finds any such bird, it returns false, indicating that the game is not over yet. If it loops through all the birds and none of them are alive, it returns true, indicating that the game is over. This function is used in the update() function to determine if a new generation needs to be started.


Game.prototype.display = function(){
 this.ctx.clearRect(0, 0, this.width, this.height);
 for(var i = 0; i < Math.ceil(this.width / images.background.width) + 1; i++){
  this.ctx.drawImage(images.background, i * images.background.width - Math.floor(this.backgroundx%images.background.width), 0)
 }

 for(var i in this.pipes){
  if(i%2 == 0){
   this.ctx.drawImage(images.pipetop, this.pipes[i].x, this.pipes[i].y + this.pipes[i].height - images.pipetop.height, this.pipes[i].width, images.pipetop.height);
  }else{
   this.ctx.drawImage(images.pipebottom, this.pipes[i].x, this.pipes[i].y, this.pipes[i].width, images.pipetop.height);
  }
 }

 this.ctx.fillStyle = "#FFC600";
 this.ctx.strokeStyle = "#CE9E00";
 for(var i in this.birds){
  if(this.birds[i].alive){
 //save the current drawing state
 this.ctx.save(); 
 //translate the canvas origin to the center of the bird
 this.ctx.translate(this.birds[i].x + this.birds[i].width/2, this.birds[i].y + this.birds[i].height/2);
 //rotate the canvas origin around the center of the bird
 this.ctx.rotate(Math.PI/2 * this.birds[i].gravity/20);
 //draw the bird, offset so that the canvas origin is at the top left corner of the image
 this.ctx.drawImage(images.bird, -this.birds[i].width/2, -this.birds[i].height/2, this.birds[i].width, this.birds[i].height);
 //restore the previous drawing state
 this.ctx.restore();
}
 }

 // Set the text color
this.ctx.fillStyle = "white";
// Set the font
this.ctx.font="20px Oswald, sans-serif";
// Draw the score
this.ctx.fillText("Score : "+ this.score, 10, 25);
// Draw the max score
this.ctx.fillText("Max Score : "+this.maxScore, 10, 50);
// Draw the generation
this.ctx.fillText("Generation : "+this.generation, 10, 75);
// Draw the population
this.ctx.fillText("Alive : "+this.alives+" / "+Neuvol.options.population, 10, 100);

 var self = this;
 requestAnimationFrame(function(){
  // 1. Request the next animation frame for the display function
  self.display();
 });
}

This function is responsible for displaying the game on the canvas. It first clears the canvas and draws the background, pipes, and birds on the canvas. It then displays the score, maximum score, generation, and population information on the canvas. The birds are drawn rotated based on their gravity value. If the bird is alive, it is drawn with its center at its (x,y) position. The canvas origin is first translated to the center of the bird and then rotated around that point. The bird image is then drawn with its top left corner at the new canvas origin. Finally, the previous drawing state is restored. The function uses the requestAnimationFrame() method to request the next animation frame for the display function, which allows for smooth animation.

window.onload = function(){
 var sprites = {
  bird:"./img/bird.png",
  background:"./img/background.png",
  pipetop:"./img/pipetop.png",
  pipebottom:"./img/pipebottom.png"
 }

 var start = function(){
  Neuvol = new Neuroevolution({
   population:50,
   network:[2, [2], 1],
  });
  game = new Game();
  game.start();
  game.update();
  game.display();
 }


 loadImages(sprites, function(imgs){
  images = imgs;
  start();
 })

}

This code sets up the game by loading images and creating a new instance of the Neuroevolution and Game objects. The code is run when the window finishes loading. The sprites object defines the file paths for the images used in the game. The start function creates a new instance of Neuroevolution with a population of 50 and a neural network with 2 input neurons, 2 hidden neurons, and 1 output neuron. It also creates a new instance of the Game object, starts the game, updates it, and displays it. The loadImages function loads the images defined in the sprites object and passes them to the start function when they are finished loading. Overall, this code sets up the basic infrastructure for the game and initializes the necessary objects to run it.

Now let’s start working on our neural network. The file is called neuroevolution.js

First we declare the the variable Neuronevolution.

/**
 * Provides a set of classes and methods for handling Neuroevolution and
 * genetic algorithms.
 *
 * @param {options} An object of options for Neuroevolution.
 */
var Neuroevolution = function (options) {
 var self = this; // reference to the top scope of this module
// Declaration of module parameters (options) and default values
 self.options = {
  /**
   * Logistic activation function.
   *
   * @param {a} Input value.
   * @return Logistic function output.
   */
  activation: function (a) {
   ap = (-a) / 1;
   return (1 / (1 + Math.exp(ap)))
  },

  /**
   * Returns a random value between -1 and 1.
   *
   * @return Random value.
   */
  randomClamped: function () {
   return Math.random() * 2 - 1;
  },

  // various factors and parameters (along with default values).
  network: [1, [1], 1], // Perceptron network structure (1 hidden
  // layer).
  population: 50, // Population by generation.
  elitism: 0.2, // Best networks kepts unchanged for the next
  // generation (rate).
  randomBehaviour: 0.2, // New random networks for the next generation
  // (rate).
  mutationRate: 0.1, // Mutation rate on the weights of synapses.
  mutationRange: 0.5, // Interval of the mutation changes on the
  // synapse weight.
  historic: 0, // Latest generations saved.
  lowHistoric: false, // Only save score (not the network).
  scoreSort: -1, // Sort order (-1 = desc, 1 = asc).
  nbChild: 1 // Number of children by breeding.

 }

This code block initializes an object called options with various properties and their default values that are used to configure and control the behavior of the neural network.

The activation property specifies a logistic activation function, which takes an input value a and returns the output value of the logistic function applied to a.

The randomClamped property returns a random value between -1 and 1, which is used to initialize the weights of the synapses in the neural network.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 Onepagecode
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share