Flappy Bird AI using NEWTL (DEMO)

Demo at the end of the article, be patient

Flappy Bird, this beloved cancelled game

It’s not that hard to make a decent clone, so I went ahead and made one. If you want to follow along, the full code is here
Again, this program is old, don’t mind if it isn’t NASA-Level code here.

The Environment

I went for an object oriented approach, since it has better compatibility with NEWTL.
Every object has a “update” and “show” method, as to write idiomatic p5js code.

The Pipes

The pipes are very simple, basically a rectangle that moves to the left.
Most of the values are arbitrary, to make the game “balanced”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Pipe{

constructor(){
this.w = 100
this.x = width + this.w
this.gap = 125
this.gapY = random(height-this.gap)
while(abs(this.gapY-lastPipeY) > this.gap/3*2)
this.gapY = random(height-this.gap)
lastPipeY = this.gapY
}

update(){
this.x -= speed
}

show(){
fill(40, 200, 40)
rect(this.x, 0, this.w, this.gapY)
rect(this.x, this.gapY+this.gap, this.w, height-(this.gapY+this.gap))
}

}

Now we just have to render them like so:

1
2
3
for(let pipe of pipes){
pipe.show()
}

We’ll take a look at the updating later, since we have to update the env as often as the AI, as opposed to rendering, we can do not-so-frequently

The Agents

Birb class

A Bird class holds again, update and show:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

constructor(){
this.y = height/2
this.x = 1000/6
this.r = 25
this.vel = 0

this.alive = false
}

show(){
tint(255, 180)
image(birdSprite, this.x-this.r, this.y-this.r)
}

update(action){

if(action == "JUMP") this.up()

this.vel += g //gravity that pulls the bird down
this.y += this.vel //the "up" function adds to this

//Check collisions
let r = this.r/2 //radius
//out of screen
if(this.y > height || this.y < 0) this.alive = false
for(let pipe of pipes)
//circle + rectangle collisions
if(this.x>pipe.x-r&&this.x-r<pipe.x+pipe.w)
if(this.y-r<pipe.gapY||this.y+r>pipe.gapY+pipe.gap)
this.alive = false

}

Though, we need to add a score variable, to better be able to handle the Genetic Algorithm.

The NEWTL Requirements

Now we can add logic to our game.
We have to have a reset function that… well… resets the game

1
2
3
4
5
6
const reset = ()=>{
pipes = []
pipeSp = Infinity //time passed >>> the required time => One pipe spawning
tbs = btbs //reset the TimeBetweenSpawn
speed = 1
}

Then, our birds need to have inputs, so that they can “see” the game:

1
2
3
4
5
6
7
8
9
const input = bird=>{
let pipe = bird.findClosestPipe()
return [
bird.y/height,
bird.vel/20,
pipe.x/width,
pipe.gapY/height,
(pipe.gapY+pipe.gap)/height
]}

Here we are feeding it data about itself and the closest pipe.

Finally, we have to evaluate the birds, we’ll simply use their own score
const fitness = bird=> bird.score

The Genetic Algorithm

Okay, now that we have the base game, let’s make it move.
Here’s a function that updates everything:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(let i = 0; i < cps; i++){
//Spawn in a new Pipe
if(pipeSp > tbs) {pipes.push(new Pipe()); pipeSp = 0; tbs-=1.5; if(tbs<200 && speed < 3)speed+=0.01}

//All the computations
for(let pipe of pipes){
pipe.update()
}
//Delete old pipes, going in reverse to be able to splice
for(let i = pipes.length-1; i >= 0; i--){
if(pipes[i].x+pipes[i].w < 0) pipes.splice(i, 1)
}
//Update the pipeSp timer
pipeSp ++
}

That was easy enough. See I haven’t updated the birds. Why ?
I’m not going to do it myself, let the AI do it !

How it works

  • 1. Initialize

    Spawn in A LOT of agents at once.
    Initialize the Birds’ brains with total randomness, so they have no clue of what’s going on !
  • 2. Play

    Let the Birds play the game, and fail miserably
  • 3. Evaluate

    Check the scores of every bird, and make a list of the better performing ones
  • 4. Breed

    Merge the best agents’ brains together, and make a new generation off of these children of champions
  • 5. Adjust

    Some adjustments may be performed to optimize your population.
    • Keep the best ones: shovel their way out of their grave and make them play another game !
    • Mutations: In case of some excellent genes not being in your base population, make them mutate over time to try and unlock them.
  • 2. Play

    And continue doing so until they’re good !

Thankfully, we don’t have to do all that by ourselves, especially the NeuralNetworks parts (tutorials on that coming soon)
We can just initialize a NEWTL object and give it what we just did above

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newt = new Newt({
agentClass: Bird,
popSize: 25,
netConfig: {
layers_num: [5, 4, 2],
layers_act: ["sigmoid", "softmax"],
layers_type: ["dense", "dense"]
},
fitnessFunction: fitness,
inputFetch: input,
actionTable:[
"JUMP",
"STAY"
],
reset: reset,
cps: cps,
mutationRate: 0.1,
textBuffer: textBuffer,
renderBuffer: renderBuffer,
})

Then, in our draw function, we can just call newt.update() and WAM BAM, the game works like a charm !

Here it is below:

Toggle Demo