-
Notifications
You must be signed in to change notification settings - Fork 0
/
AIFlappyBird.py
210 lines (171 loc) · 6.87 KB
/
AIFlappyBird.py
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import pygame
import random
import neat
from sys import exit
# set screen and font
pygame.init()
screen = pygame.display.set_mode((500, 900))
pygame.display.set_caption("AI Flappy Bird")
font = pygame.font.Font("fonts/Minecraft.ttf", 50)
class Bird:
def __init__(self):
# bird images
birdFlap1 = pygame.image.load("images/bird1.png").convert_alpha()
birdFlap2 = pygame.image.load("images/bird2.png").convert_alpha()
self.birdFlaps = [birdFlap1, birdFlap2]
self.img = self.birdFlaps[0]
self.imgIndex = 0
# set initial position and gravity
self.x = 230
self.y = 350
self.gravity = 0
def jump(self):
self.gravity = -15
def applyGravity(self):
self.gravity += 1
self.y += self.gravity
def animate(self, screen):
# rotate through images each frame to get animation effect
self.imgIndex += 0.1
if self.imgIndex >= len(self.birdFlaps):
self.imgIndex = 0
self.img = self.birdFlaps[int(self.imgIndex)]
screen.blit(self.img, (self.x, self.y))
def get_mask(self):
return pygame.mask.from_surface(self.img)
class Pipe():
def __init__(self, x):
# pipe images
self.pipeTop = pygame.image.load("images/tPipe.png").convert_alpha()
self.pipeBottom = pygame.image.load("images/bPipe.png").convert_alpha()
# set pipe positions
self.x = x
self.y = random.randrange(50, 500)
self.top = self.y - self.pipeTop.get_height()
self.bottom = self.y + 200
# boolean to keep track of whether bird has passed the pipe
self.passed = False
def animate(self):
self.x -= 5
def draw(self, screen):
screen.blit(self.pipeTop, (self.x, self.top))
screen.blit(self.pipeBottom, (self.x, self.bottom))
def collide(self, bird):
# check if bird has collided with pipe
birdMask = bird.get_mask()
topMask = pygame.mask.from_surface(self.pipeTop)
bottomMask = pygame.mask.from_surface(self.pipeBottom)
# general apprach inspired by "HKH" from stack overflow
topOffset = (self.x - bird.x, self.top - bird.y)
bottomOffset = (self.x - bird.x, self.bottom - bird.y)
if birdMask.overlap(bottomMask, bottomOffset) or birdMask.overlap(topMask, topOffset):
return True
return False
class Ground:
def __init__(self, y):
self.y = y
self.x = 0
def animate(self):
self.x -= 4
if self.x <= -500:
self.x = 0
def draw(self, screen):
ground = pygame.image.load("images/ground.png").convert_alpha()
screen.blit(ground, (self.x, self.y))
def draw_window(screen, birds, pipes, ground, score, gen):
# draw background, birds, pipes, and ground
backgroundImage = pygame.image.load("images/bg.png").convert_alpha()
screen.blit(backgroundImage, (0, 0))
ground.draw(screen)
for pipe in pipes:
pipe.draw(screen)
for bird in birds:
bird.animate(screen)
# display score and generation
scoreSurface = font.render(f"SCORE: {score}", False, ("White"))
screen.blit(scoreSurface, (10, 60))
genSurface = font.render(f"GEN: {gen-1}", False, "White")
screen.blit(genSurface, (10, 10))
pygame.display.update()
# function ran for each new generation
def eval_genomes(genomes, config):
# set score to 0 and increment generation
global screen, gen
clock = pygame.time.Clock()
gen += 1
score = 0
# create genomes with fitness 0 and add it genomeList, then add them to the neural network, and append the approriate birds to birds list
neuralNetworks = []
birds = []
genomeList = []
for genome_id, genome in genomes:
genome.fitness = 0
net = neat.nn.FeedForwardNetwork.create(genome, config)
neuralNetworks.append(net)
birds.append(Bird())
genomeList.append(genome)
# instanciate ground and pipes
ground = Ground(700)
pipes = [Pipe(700)]
# while at least one bird from a generation is still alive
while len(birds) > 0:
# give player option to quit game
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
# set game to 30 FPS and animate ground
clock.tick(30)
ground.animate()
# determine if bird has passed through pipe if so update pipe
pipeIndex = 0
if len(pipes) > 1 and birds[0].x > pipes[0].x + pipes[0].pipeTop.get_width():
pipeIndex = 1
# give each bird a fitness of 0.1 for each frame it stays alive
for i, bird in enumerate(birds):
genomeList[i].fitness += 0.1
bird.applyGravity()
# send bird location, top pipe location, and bottom pipe location to determine whether to jump or not
output = neuralNetworks[i].activate(
(bird.y, abs(bird.y - pipes[pipeIndex].y), abs(bird.y - pipes[pipeIndex].bottom)))
# since we're using the tanh function(between -1 and 1), if over 0.5 we jump
if output[0] > 0.5:
bird.jump()
# eliminate bird if goes off screen or touches floor
if bird.y + bird.img.get_height() - 10 >= 700 or bird.y < -50:
neuralNetworks.pop(birds.index(bird))
genomeList.pop(birds.index(bird))
birds.pop(birds.index(bird))
for pipe in pipes:
# animate pipe
pipe.animate()
# if collision occurs remove bird
for bird in birds:
if pipe.collide(bird):
neuralNetworks.pop(birds.index(bird))
genomeList.pop(birds.index(bird))
birds.pop(birds.index(bird))
# generate new pipe once pipe is passed and increment score
if not pipe.passed and pipe.x < bird.x-30:
pipe.passed = True
score += 1
pipes.append(Pipe(500))
# remove pipe once it is off screen
if pipe.x <= -100:
pipes.remove(pipe)
draw_window(screen, birds, pipes, ground, score, gen)
if __name__ == '__main__':
# connect neat with configuration file
config_path = 'configFile.txt'
config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
config_path)
# create the population, which is the top-level object for a NEAT run.
population = neat.Population(config)
# add a stdout reporter to show progress in the terminal.
population.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
population.add_reporter(stats)
# run for up to 50 generations starting with gen 0.
gen = 0
population.run(eval_genomes, 50)