While I might not be good at playing ‘Flappy Bird’, I am good at programming. Which technically means…I am really good at Flappy Bird.

So, I built a Flappy Bird game along with an A.I for it that is capable of learning and playing it in a day. Here’s how I did it.

If you want to check out the source code, here's a link to my GitHub.

My Approach to This Project

To create this project, I used Neuroevolution of Augmenting Topologies (NEAT) and Python to program a neural network capable of teaching itself to understand and survive simulated scenarios.

I programmed classes for Flappy Bird in Python, then implemented some physics and death mechanics along with pixel precision collision accuracy to it.

I implemented a Neural Network using NEAT and bred birds every generation until a certain fitness threshold was obtained and the Neural Network was capable of figuring out a solution to the simulated scenario.

I used the following technologies and tools to complete this project:

  • Python 3.8.5 and MacOS Terminal as its interpreter
  • Vim and Jupyter Notebook as primary IDE's (Personal Preference)
  • Python NEAT Library ()
  • Other common libraries such as pygame, time, random, os

Prerequisites:

  • Basic Python knowledge about classes, functions & objects
  • How the game Flappy Bird works (to comprehend the Physics and the Death Mechanics implemented in this project).

What Is NEAT and How Can We Implement it Here?

NEAT stands for NeuroEvolution of Augmenting Topologies. In basic terms, it is essentially an attempt to copy evolution in nature.

What we'll do here is generate a bunch of random players and test them. The players that do the best will be sent for breeding and the others will be mutated.

This means that the next generation will be filled with the best players. And the process repeats until we have a network capable of doing what we want.


How Do Neural Networks Work?

Before explaining NEAT’s implementation in our project, I first want to start off by explaining how Neural Networks work.

Essentially Neural Networks come in layers, with the first layer being the input layer.

Input layers are the information passed to our network, like what the network actually knows and sees. For our project, we could tell the bird about the distance between the bird and the top/bottom pipes as the input layer.

The last layer is the output layer, which will tell the A.I. what it should do. In our case, it will tell the bird whether it should jump or not.

A Deeper Explanation of Neural Networks – a Mathematical Approach

While working with neural networks, we start off by passing some values into the input layer neurons which will then pass them to the output neuron using Connections.

All Connections will have something called Weights, which is a number representing how strong or weak the connection is. We will tweak these numbers to make our A.I. perform better.

After this, we will calculate the Weighted Sum. This means that we are going to take each weight and multiply it by its corresponding input value and then add them.

Feed-Forward Neural Network

Then to the weighted sum, we add two things:

  • (i) Bias: A number that lets us tweak our Neural Network to allow us to move our network a little bit up and down and shift it to the best possible position.
  • (ii) Activation Function: This function will help us get the value for our neuron between a range of two numbers. This is very useful, as in this case we can tell the A.I to jump or not jump depending upon the value returned by the activation function (bird jumps if output > 0 in our case).

For this project, we will use TanH as our activation function.

TanH function compressing the output values between -1 and 1

So, you might be wondering – how do we get the correct weights and biases for our bird here?

Well, we actually don't. We tell the computer to do it for us. This is where NEAT comes into play!

Here we start off by creating a population of birds that is completely random. Every bird of the population will have a different neural network controlling it.

These neural networks will all start with random weights and biases. Then, we put all of them on our level and see how well they perform. We relatively tweak their fitness to encourage them. Fitness is essentially how “fit” or how “good” the solution is with respect to the problem in consideration, and is different depending on the task.

So when do we increase the bird's Fitness value?

In our case, we will increment the fitness of the birds that have successfully passed a pipe and reduce the fitness of those birds that have failed.

To encourage the bird to stay alive and move, we will also increase its fitness if it has moved forward every frame without dying.

Finally, at the end of the simulation, the birds that have performed the best will be bred to form a new population (generation +=1). This generation of birds will perform better and the cycle continues until we obtain our desired fitness level.

There's also an ‘n’ number of Hidden Layers between the input and the output layer. Hidden layers capture more and more complexity with every layer by discovering relationships between features in the input.

The NEAT Cycle

To summarize, we are using a ‘feed-forward’ neural network, where the input is multiplied by the weights, summed, and passed through non-linear equations of choice, that is activation functions (eg. Sigmoid, ReLU, TanH).

Hidden layers apply a non-linearity to the neural network’s input, and the more hidden layers you stack together, the more complex functions you will be able to model.

How to Program Flappy Bird and Apply NEAT

I am going to assume that you have prior Python knowledge.

You can find the full code → here.

Let’s start by importing the necessary libraries and assets. Since all the image assets found online were a bit small, we will multiply their size using pygame.transform.scale2x().

import os
import neat
import time
import random
import pygame
pygame.font.init()
pygame.display.set_caption('Flappy Bird AI')
window_font = pygame.font.SysFont("arial", 18)
flappy_font = pygame.font.Font("assets/flappy-font.ttf", 50)
window_width = 500
window_height = 800
generations = 0
all_bird_imgs = [pygame.transform.scale2x(pygame.image.load(os.path.join("assets", "bird1.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join(
"assets", "bird2.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join("assets", "bird3.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join("assets", "bird4.png")))]
img_pipe = pygame.transform.scale2x(
pygame.image.load(os.path.join("assets", "pipe.png")))
img_base = pygame.transform.scale2x(
pygame.image.load(os.path.join("assets", "base.png")))
img_background = pygame.transform.scale2x(
pygame.image.load(os.path.join("assets", "background.png")))

Now that all our assets are loaded, we will start working on the class Bird(): In this class, we will define the following methods:

  • rip_animation(self): This is the method that will be called if a bird hits a pipe. The bird will go vertically downwards until it hits the ground.
  • move(self): To make things clear, the bird actually doesn't have to move forward, but rather just up and down because the pipes and the ground are all that's moving. Here we will create a physics mechanism where we’ll adjust the birds' displacement from the y axis depending upon the time passed by calculating the number of ticks.
  • jump(self): We will add -ve velocity to the bird here because the top left corner of the pygame window has coordinates (0,0) and the bottom right will have coordinates (500,800). Thus to make the bird go upwards, we will have to reduce its y-coordinate.
  • draw(self, win): In this method, we will tilt the bird according to where it's going. One more thing we need to do is tell the bird not to play the flapping images if it's going down since that would look kinda dumb.
  • get_mask(self): Here we will use an inbuilt pygame function to mask every pixel of the bird since we desire pixel-perfect collisions.

You also might be wondering how we can implement a bird’s velocity. Well, the solution is to set velocity to some value and, using it, implement a formula to calculate a displacement ‘d’.

Then we run the game at 60fps, and we call the main( ) 60 times a second. This will also invoke the method containing the displacement and so move the bird a certain distance ‘d’ every frame (1/60th of a second). This will make it look very smooth while moving with some velocity.

"""
Code for the Bird Class
"""
class Bird():
animation_imgs = all_bird_imgs
# while the bird moves up and down
maximum_rotation = 24
# velocity with which we rotate the bird
rotation_velocity = 18
# flap animation duration
flap_animation = 8
def __init__(self, x, y, rip):
# x, y are the starting coordinates, rip (rest in peace) is the boolean value that checks if the bird is alive or not
self.x = x
self.y = y
self.rip = rip
self.tilt = 0
self.ticks = 0
self.vel = 0
self.height = self.y
self.img_count = 0
self.img = self.animation_imgs[0]
def rip_animation(self):
# positive velocity will make it go in down as the y coordinate of the pygame window increases as we go down
self.vel = 10
self.ticks = 0
# if bird is rip (by hitting a pipe) then it will turn the bird red and move it to the ground where we will remove it from the list of birds
self.height = window_height
def move(self):
self.ticks = self.ticks + 1
# d stands for displacement
d = self.vel * (self.ticks) + 1.5 * self.ticks**2
if d >= 14:
d = 14
if d < 0:
d -= 2
self.y = self.y + d
if d < 0 or self.y < self.height + 50:
if self.tilt < self.maximum_rotation:
self.tilt = self.maximum_rotation
else:
if self.tilt > -90:
self.tilt -= self.rotation_velocity
def jump(self):
# since top left corner of pygame window has coordinates (0,0), so to go upwards we need negative velocity
self.vel = -10
self.ticks = 0
self.height = self.y
def draw(self, win):
# img_count will represent how many times have we already shown image
self.img_count = self.img_count + 1
# condition to check if the bird is alive
if self.rip == False:
# checking what image of the bird we should show based on the current image count
if self.img_count <= self.flap_animation:
self.img = self.animation_imgs[0]
elif self.img_count <= self.flap_animation * 2:
self.img = self.animation_imgs[1]
elif self.img_count <= self.flap_animation * 3:
self.img = self.animation_imgs[2]
elif self.img_count <= self.flap_animation * 4:
self.img = self.animation_imgs[1]
elif self.img_count == self.flap_animation * 4 + 1:
self.img = self.animation_imgs[0]
self.img_count = 0
# this will prevent flapping of the birds wings while going down
if self.tilt <= -80:
self.img = self.animation_imgs[1]
self.img_count = self.flap_animation * 2
# condition if the bird is rip
elif self.rip == True:
self.tilt = -90
self.img = self.animation_imgs[3]
self.rip_animation()
# to rotate image in pygame
rotated_image = pygame.transform.rotate(self.img, self.tilt)
new_rect = rotated_image.get_rect(
center=self.img.get_rect(topleft=(self.x, self.y)).center)
# blit means draw, here we will draw the bird depending upon its tilt
win.blit(rotated_image, new_rect.topleft)
# since we want pixel perfect collision, and not just have a border around the bird, we mask the bird
def get_mask(self):
return pygame.mask.from_surface(self.img)

The next class is Pipe():. In this class, we will define the following methods:

  • set_height(self): This method randomly sets the heights of the top and bottom pipes and makes sure that the gap between them stays constant.
  • draw(self, win): Draws the top and bottom pipes on the window
  • collide(self, bird, win): This method will get the bird’s mask and check if it overlaps with the top or bottom pipe’s mask. If yes, then it will return True (which will be used later), otherwise, return False.
"""
Code for the Pipe Class
"""
class Pipe:
gap = 220
# velocity of pipes, since the pipes move and the bird does not
vel = 4
def __init__(self, x):
# x because the pipes are going to be random
self.x = x
self.height = 0
# creating varibles to keep track of where the top and bottom of the pipe are going to be drawn
self.top = 0
self.bottom = 0
self.top_pipe = pygame.transform.flip(img_pipe, False, True)
self.bottom_pipe = img_pipe
# if the bird has passed the pipe
self.passed = False
# this method will show where our pipes are and what the gap between them is
self.set_height()
def set_height(self):
# randomizes the placement of pipes
self.height = random.randrange(40, 450)
self.top = self.height - self.top_pipe.get_height()
self.bottom = self.height + self.gap
def move(self):
self.x = self.x - self.vel
def draw(self, win):
win.blit(self.top_pipe, (self.x, self.top))
win.blit(self.bottom_pipe, (self.x, self.bottom))
def collide(self, bird, win):
bird_mask = bird.get_mask()
top_mask = pygame.mask.from_surface(self.top_pipe)
bottom_mask = pygame.mask.from_surface(self.bottom_pipe)
top_offset = (self.x - bird.x, self.top - round(bird.y))
bottom_offset = (self.x - bird.x, self.bottom - round(bird.y))
# finding the point of collision
bottom_point = bird_mask.overlap(bottom_mask, bottom_offset)
top_point = bird_mask.overlap(top_mask, top_offset)
if top_point or bottom_point:
return True
return False

The next class is Base(): In this class, we will make the base appear to be moving infinitely. But in theory, we are actually looping two pictures of the base one behind another. Check out the diagram below that explains this.

Looping the base to make it look infinitely long

The methods we will define in this class are:

  • move(self): This method will move the images by some distance to the left per frame. Then, when the first image is completely off the screen, it quickly goes behind the second image, and this loops until termination.
  • draw(self, win): Draws both the base images on the window.
"""
Code for the Base Class
"""
class Base():
vel = 5
width = img_base.get_width()
img = img_base
def __init__(self, y):
self.y = y
self.x1 = 0
self.x2 = self.width
def move(self):
self.x1 -= self.vel
self.x2 -= self.vel
if self.x1 + self.width < 0:
self.x1 = self.x2 + self.width
if self.x2 + self.width < 0:
self.x2 = self.x1 + self.width
def draw(self, win):
win.blit(self.img, (self.x1, self.y))
win.blit(self.img, (self.x2, self.y))

The next step is to actually draw everything on the window, like all the pipes, the score, and other stats.

The important thing to do here is to make sure that you are writing this outside the classes, that is starting with zero indentation. The code is pretty self-explanatory.

"""
Code to Render Pipes, Birds, Stats, etc. on the Window
"""
def draw_window(win, birds, pipes, base, score, gen, pipe_number, fitness):
if gen == 0:
gen = 1
win.blit(img_background, (0, 0))
for pipe in pipes:
pipe.draw(win)
# showing all the text labels
score_text = flappy_font.render(str(score), 0, (255, 255, 255))
win.blit(score_text, (window_width - score_text.get_width() - 230, 100))
# showing the generation number
gen_text = window_font.render(
"Species Generation Num: " + str(gen), 1, (0, 0, 0))
win.blit(gen_text, (10, 5))
# showing the number of birds that are alive in the provided frame
alive_text = window_font.render("Alive: " + str(len(birds)), 1, (0, 0, 0))
win.blit(alive_text, (10, 25))
# showing the total number of birds that have been mutated in the current frame
mutated_text = window_font.render(
"Mutated: " + str(15 - len(birds)), 1, (231, 84, 128))
win.blit(mutated_text, (10, 45))
# showing the fitness value of the birds
fitness_text = window_font.render(
"Fitness: " + str(fitness), 1, (0, 255, 0))
win.blit(fitness_text, (10, 65))
# showing the fitness threshold that should be reached before automatically terminating the program
fitness_t_text = window_font.render(
"Fitness Threshold: 1000", 1, (0, 0, 0))
win.blit(fitness_t_text, (window_width - fitness_t_text.get_width() - 10, 5))
# showing the population of the birds that will be bred every generation
population_text = window_font.render("Population: 15", 1, (0, 0, 0))
win.blit(population_text, (window_width -
population_text.get_width() - 10, 25))
base.draw(win)
for bird in birds:
bird.draw(win)
pygame.display.update()

Now it's time to program our main(). It will also behave as the fitness function for our project. In this, we will do the following:

  • Setup a FeedForward Neural Network for the genomes using the config file located in assets (imported after main).
  • Then we will place the initial pipes and the base and set a clock to tick 60 times a second.
  • Our next goal is to make sure the bird looks at the pipes that are in front of it and not the pipes that it has already passed.
  • Then, we will instruct the bird to jump if the output returned by the neural network is > 0. Now, we will increment the fitness of the bird if it either goes in between the pipes or if the bird is alive for a given frame (this will encourage it to stay alive and flap its wings)
  • If a bird does hit a pipe, we will reduce its fitness (so it doesn't breed to form the next generation) and set the bird’s ‘rip’ attribute to True, which will turn the bird red and trigger the rip_animation to make it fall to the ground.
  • Whenever a bird hits the ground or tries to trick the system by going above the screen to cross the pipes, we will remove it from the birds’ list by using the pop() function (Also make sure to pop the network and genomes associated with that bird before popping the actual bird).
"""
Code for main()
This will also behave as a fitness function for the program
"""
# the main() will also act as a fitness function for the program
def main(genomes, config):
global generations
generations += 1
nets = []
ge = []
birds = []
# setting neural network for genome, using _, g as 'genomes' is a tuple that has the genome id as well as the genome object
for _, g in genomes:
net = neat.nn.FeedForwardNetwork.create(g, config)
nets.append(net)
birds.append(Bird(210, 320, rip=False))
g.fitness = 0
ge.append(g)
base = Base(730)
# first pipe will be a little away that other pipes so the birds knows that they can gain fitness by staying alive
pipes = [Pipe(600)]
win = pygame.display.set_mode((window_width, window_height))
clock = pygame.time.Clock()
score = 0
run = True
while run and len(birds) > 0:
clock.tick(60)
# keeps track if something happens like whenever user clicks keys etc.
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
# quits the program when user hits the cross
pygame.quit()
quit()
# making sure the bird looks only at first pipe and not the second pipe, since there can be multiple pipes generated
pipe_number = 0
if len(birds) > 0:
# if we pass the first pipe, then look at the next pipe
if len(pipes) > 1 and birds[0].x > pipes[0].x + pipes[0].top_pipe.get_width():
pipe_number = 1
for bird in birds:
bird.move()
# adding fitness since it has come to this level, also giving such little fitness value because the for loop will run 60 times a second so every second our bird stays alive, it will give it some fitness point, so this encourages the bird to stay alive
ge[birds.index(bird)].fitness += 0.2
output = nets[birds.index(bird)].activate((bird.y, abs(
bird.y - pipes[pipe_number].height), abs(bird.y - pipes[pipe_number].bottom)))
if output[0] > 0:
bird.jump()
base.move()
# list to remove pipes
add_pipe = False
rem = []
for pipe in pipes:
pipe.move()
# checking for collision
for bird in birds:
if pipe.collide(bird, win):
# every time a bird hits a pipe, we will reduce its score, so we are encouraging the bird to go between the pipes
ge[birds.index(bird)].fitness -= 1
birds[birds.index(bird)].rip = True
# no need to add the below lines to delete the bird as, on hitting a pipe, we play the rip animation which turns the bird red and moves it to the ground and we already will pop the bird if it hits the ground so we dont need this.
# nets.pop(birds.index(bird))
# ge.pop((birds.index(bird)))
# birds.pop(birds.index(bird))
# this is checking if our pipe is off the screen so we can generate another pipe
if pipe.x + pipe.top_pipe.get_width() < 0:
rem.append(pipe)
if not pipe.passed and pipe.x < bird.x:
pipe.passed = True
add_pipe = True
if add_pipe:
score += 1
for g in ge:
g.fitness += 4
# distance between pipes
pipes.append(Pipe(550))
for r in rem:
pipes.remove(r)
# checks if the bird hits the ground or goes above the screen and avoids the pipe
for bird in birds:
if bird.y + bird.img.get_height() - 10 >= 730 or bird.y < -50:
# mutate the bird if it hits the ground
nets.pop(birds.index(bird))
ge.pop((birds.index(bird)))
birds.pop(birds.index(bird))
draw_window(win, birds, pipes, base, score,
generations, pipe_number, g.fitness)

Finally, we will import the NEAT config file located at assets/flappy-config.txt. This file contains the tweaks and values that the A.I. will use.

The configuration file is in the format described in the Python ConfigParser documentation. Currently, all values must be explicitly enumerated in the configuration file. This makes it less likely that code changes will result in your project silently using different NEAT settings.

To learn in detail about what configuration files are, visit → here.

Having ‘neat.StdOutReporter(True)’ would give us detailed statistics in the terminal. Something like this:

Detailed NEAT Neural Network Statistics
"""
Code for importing and running the config file located in the assets directory
"""
def run(config_path):
config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation, config_path)
p = neat.Population(config)
# now we will add stats reporters which will give us some outputs in the console where we will see the detailed statistics of each generation and the fitness etc.
p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)
winner = p.run(main, 100)
if __name__ == "__main__":
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, "assets/flappy-config.txt")
run(config_path)
# Thank you! If you liked this project, you could show me support by following me or starring my repos --> github.com/yaashwardhan

That's It! We have programmed the Neural Network to learn how to play Flappy Bird. Give it a try yourself :)