I am going to assume that you have prior Python knowledge. Full code → linked here. You can also check out TechwithTim on YouTube.
Let’s start by importing necessary libraries and assets. Since all the image assets found online were a bit small, we will multiply their size by using pygame.transform.scale2x()
. Here’s the code:
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 is all that’s moving. Here we will create a physics mechanism where we’ll adjust the bird’s displacement from they
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 itsy
-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’s would look very 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 getting confused on how we can implement a bird’s velocity. Well, for this the solution is to set velocity to some value and using it, implement a formula to calculate a displacement d
. Then to run the game at 60fps, we call the main( )
60 times a second, which will also invoke the method containing the displacement and hence 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. Here’s the code:
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 windowcollide(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 returnTrue
(which will be used later), otherwise, returnFalse
.
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 the diagram below that explains this.
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.
The next step is to actually draw everything on the window, like all the pipes, the score, other stats, etc. The important thing to do here is to make sure that you are writing this outside the classes, i.e, starting with zero indentation. The code below is pretty self-explanatory:
Now its 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 would be 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 therip_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).
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 → this link.
Having the ‘neat.StdOutReporter(True)’
would give us detailed statistics in the terminal. Something like this:
#========== assets/flappy-config.txt ==========
[NEAT]
# determines the fact that we need the best birds
fitness_criterion = max
# the fitness level we want before we terminate the program
fitness_threshold = 1000
pop_size = 15
# even if all the species become simultaneously extinct due to stagnation, no new random population will be created
reset_on_extinction = False[DefaultGenome]
activation_default = tanh
# does not change to another activation function when a new population is created
activation_mutate_rate = 0.0
activation_options = tanh
aggregation_default = sum
aggregation_mutate_rate = 0.0
aggregation_options = sum
bias_init_mean = 0.0
bias_init_stdev = 1.0
bias_max_value = 30.0
bias_min_value = -30.0
bias_mutate_power = 0.5
bias_mutate_rate = 0.7
bias_replace_rate = 0.1
compatibility_disjoint_coefficient = 1.0
compatibility_weight_coefficient = 0.5
conn_add_prob = 0.5
conn_delete_prob = 0.5
enabled_default = True
enabled_mutate_rate = 0.01
feed_forward = True
initial_connection = full
node_add_prob = 0.2
node_delete_prob = 0.2
num_hidden = 0
num_inputs = 3
num_outputs = 1
response_init_mean = 1.0
response_init_stdev = 0.0
response_max_value = 30.0
response_min_value = -30.0
response_mutate_power = 0.0
response_mutate_rate = 0.0
response_replace_rate = 0.0
weight_init_mean = 0.0
weight_init_stdev = 1.0
weight_max_value = 30
weight_min_value = -30
weight_mutate_power = 0.5
weight_mutate_rate = 0.8
weight_replace_rate = 0.1
[DefaultSpeciesSet]
compatibility_threshold = 3.0
[DefaultStagnation]
species_fitness_func = max
max_stagnation = 20
species_elitism = 2
[DefaultReproduction]
elitism = 2
survival_threshold = 0.2
That’s It! We have programmed the neural network to learn how to play ‘Flappy Bird.’ Give it a try yourself!
Thanks for reading, I hope you learned something! Any comments, doubts, or suggestions are highly valuable to me.
A massive thanks to YouTuber TechwithTim.