AI learns to play Flappy Bird using Genetic Algorithms
You can test it in your browser here. Note that there is a problem with how the font is exported, I guess. It shouldn't be a big deal, though.
recording.mp4
Almost 6 months ago, I got interested in genetic algorithms and the simulations people were making using them. I wanted to try it out in Flappy Bird, which was done by many people before :) At the time, I had used C++ directly and the official godot-cpp bindings, and I hadn't touched it after that until now.
I wanted to test it out with godot-rust/gdext which is the Rust bindings for Godot. It was pretty great, and I was able to build for web as well, which was pretty amazing.
For ordinary builds, we just need to run
cd ./rust/godot-neural-networks/ && cargo build
I didn't test with Mac or Windows, but it should hopefully be fine. Then, we need to copy the resulting target/debug/libgodot_neural_networks.so
to the game/extern
directory. Alternatively, you can run
nix build .
It should result in result/lib/libgodot_neural_networks.so
, which you can copy to the game/extern
directory and follow the Godot instructions.
It is a bit more complex. Here's how I did it, and you can reproduce if you are a Nix user.
-
First, you will need to go to the
rust/godot-neural-networks
directory. -
If you are using
direnv
, you are going to end up in a shell with nightly rust installed along withemscripten
which is needed to link towasm32-unknown-unknown
. If you do not have a nightly rust, you can follow the instructions in rustup.rs. -
Now, we need to build the wasm. If you use Nix, and therefore have the nightly rust, you can just run
cargo build -Zbuild-std=std,panic_abort --target wasm32-unknown-emscripten
If you are not using Nix, but have the nightly build, you should run:
rustup target add wasm32-unknown-emscripten
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
rustup target add wasm32-unknown-emscripten --toolchain nightly
cargo +nightly build -Zbuild-std --target wasm32-unknown-emscripten
Then, you should copy the resulting target/wasm32-unknown-emscripten/release/libgodot_neural_networks.wasm
to the game/extern
directory and run:
cd game
mkdir -v -p ../exports/web
godot --headless --verbose --export-release "Web" ../exports/web/index.html
Here is how the training process works in steps:
-
Create
num_players
random agents (random agent refers to an agent whose neurons' weights and biases are completely random). -
Run the simulation and score all the agents.
-
Choose the best performing
survival_percentage
percent of the agents - they are going to survive to the next generation without any mutation. -
Generate new agents in two distinct ways (depending on the
exploration_rate
):- Create a new random agent
- Pick two random agents that survived from the previous generation and make a new agent out of them (crossover) along with some mutations.
-
Repeat the process (from step 2).
There are 4 parameters that are controlling the entire simulation:
-
num_players
(default: 100): The number of agents in the simulation (birds in this context). -
survival_percentage
(default: 0.2): The percentage of the population that is going to survive unmodified to the next generation. With our default parameters, 20 agents that have performed the best will survive to the next round. -
mutation_probability
(default: 0.5): The probability of a gene mutating. This parameter is used to determine if a neuron's weight is going to be randomized or is going to be set using the either parents' neurons. -
exploration_rate
(default: 0.5): The probability of the generation of a new random agent. Once the agents that are going to survive the next round are decided, for each agent, we will poll, and see if we need to generate a completely new random agent with completely random weights.
It is a bit tricky, but the way we handle depends on two things:
-
score
: The score of the agent in the current round/generation. -
collective_score
: The sum of the scores of the agents in all the previous generations.
We prioritize score
over collective_score
. You can see in the source code how the comparison is held:
impl Ord for Agent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.score, self.collective_score).cmp(&(other.score, other.collective_score))
}
}
However, I think this comparison is far from optimal... the newer agents are less likely to survive the next round even though they are better compared to the older ones.