
If you want to replicate this project please make sure the following libraries match
neat-python 0.92
pip 21.2.3
pygame 2.0.1
setuptools 57.4.0
Creating the Dinosaur
class Dinosaur:
X_POS = 80
Y_POS = 310
JUMP_VEL = 8.5
def __init__(self, img=RUNNING[0]):
self.image = img
self.dino_run = True
self.dino_jump = False
self.jump_vel = self.JUMP_VEL
self.rect = pygame.Rect(self.X_POS, self.Y_POS, img.get_width(), img.get_height())
self.color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
self.step_index = 0
def update(self):
if self.dino_run:
self.run()
if self.dino_jump:
self.jump()
if self.step_index >= 10:
self.step_index = 0
def jump(self):
self.image = JUMPING
if self.dino_jump:
self.rect.y -= self.jump_vel * 4
self.jump_vel -= 0.8
if self.jump_vel <= -self.JUMP_VEL:
self.dino_jump = False
self.dino_run = True
self.jump_vel = self.JUMP_VEL
def run(self):
self.image = RUNNING[self.step_index // 5]
self.rect.x = self.X_POS
self.rect.y = self.Y_POS
self.step_index += 1
def draw(self, SCREEN):
SCREEN.blit(self.image, (self.rect.x, self.rect.y))
pygame.draw.rect(SCREEN, self.color, (self.rect.x, self.rect.y, self.rect.width, self.rect.height), 2)
To initialize the dinosaur, we need to give it some attributes. We need to give it an image, two Booleans to determine whether we are jumping or running, a jumping velocity, a rectangle around the dinosaur for a hitbox. We then need to update the current state of the dinosaur, in the update function, if the running is true, we run self.run(), we similarly do this to jump. The step-index is to help us run through our array of images for the dinosaur giving it the appearance of running. Defining jump was changing the dinosaur’s position lower for our y-axis. After the jump, we set to make sure to update our dinosaur telling it that it is no longer jumping and now running. Similarly, in the run function, we are moving inside the array of running images and setting the x and y coordinates back to their initial position. Just changing the images will create the run effect we need. This is because the dinosaur stays in place with an exception of a y-axis change during jumping.
Creating the Obstacles
class Obstacle:
def __init__(self, image, number_of_cacti):
self.image = image
self.type = number_of_cacti
self.rect = self.image[self.type].get_rect()
self.rect.x = SCREEN_WIDTH
def update(self):
self.rect.x -= game_speed
if self.rect.x < -self.rect.width:
obstacles.pop()
def draw(self, SCREEN):
SCREEN.blit(self.image[self.type], self.rect)
To initialize the obstacles we will have we will assign them an image, and an integer that will represent the number of cacti. We create the rectangle around the images allowing for a hitbox and then create an update function to allow these images to move and be removed from the game. Creating two separate classes for the small Cactus and Large cactus, we initialize these with their image, and the number of cacti. The smaller cactus needs to be placed high as the coordinates go from the top left of a window/image.
Main Game Loop
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
SCREEN.fill((255,255,255)) #Screen Filled White
for dinosaur in dinosaurs: #For all dinosaurs in the array update and draw them to the screen
dinosaur.update()
dinosaur.draw(SCREEN)
if len(dinosaurs) == 0: #If there are no more dinosaurs break out of the loop
break
if len(obstacles) == 0: #If there are no obsticals, spawn new one | type is random | Amount of obstical random
rand_int = random.randint(0, 1)
if rand_int == 0:
obstacles.append(SmallCactus(SMALL_CACTUS, random.randint(0 , 2)))
elif rand_int == 1:
obstacles.append(LargeCactus(LARGE_CACTUS, random.randint(0, 2)))
for obstacle in obstacles: #For all the obstacles draw and update them, check for collision with dinosaur
obstacle.draw(SCREEN)
obstacle.update()
for i, dinosaur in enumerate(dinosaurs):
if dinosaur.rect.colliderect(obstacle.rect): #Decrease fitness when collision
ge[i].fitness -= 1
remove(i)
for i, dinosaur in enumerate(dinosaurs): #Passing inputs to the dinosaurs, using distance function to determine when to jump
output = nets[i].activate((dinosaur.rect.y, distance((dinosaur.rect.x, dinosaur.rect.y), obstacle.rect.midtop)))
if output[0] > 0.5 and dinosaur.rect.y == dinosaur.Y_POS:
dinosaur.dino_jump = True
dinosaur.dino_run = False
statistics()
score()
background()
clock.tick(45)
pygame.display.update()
To start the game loop we first must enable a way to quit out of the game. This way we don’t get stuck in our code…trust me you always need a emergency escape. Loading our array of players, or dinosaurs in this instance, we draw them to the screen. Checking if there are no obstacles, we then randomize the type and amount of obstacles coming into the dinosaur. After setting up collisions by checking the “hitbox” around the dinosaur and checking for an intersection of the “hitbox” of the cactus. The most important line here if you are looking for where the NEAT library comes into play is
ge[i].fitness -= 1
Here upon the collision of one of the dinosaurs, they will be removed and the fitness score will decrease by 1 to try to promote others to stay away from the cactus. In order to append each player as a genome within the NEAT library we will need the following code above the main loop
for genome_id, genome in geonomes:
dinosaurs.append(Dinosaur())
ge.append(genome)
net = neat.nn.FeedForwardNetwork.create(genome, config)
nets.append(net)
genome.fitness = 0
Notice that the Feedforward network required our players and a config file. What is this config file and what does it do. Above we used the fitness score and decreased it on collision to “teach” the dinosaurs to avoid the cactus. Within the config file holds the parameters the NEAT library will use.
[NEAT]
fitness_criterion = max
fitness_threshold = 10000
pop_size = 15
reset_on_extinction = False
[DefaultGenome]
# node activation options
activation_default = tanh
activation_mutate_rate = 0.0
activation_options = tanh
# node aggregation options
aggregation_default = sum
aggregation_mutate_rate = 0.0
aggregation_options = sum
# node bias options
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
# genome compatibility options
compatibility_disjoint_coefficient = 1.0
compatibility_weight_coefficient = 0.5
# connection add/remove rates
conn_add_prob = 0.5
conn_delete_prob = 0.5
# connection enable options
enabled_default = True
enabled_mutate_rate = 0.01
feed_forward = True
initial_connection = full
# node add/remove rates
node_add_prob = 0.2
node_delete_prob = 0.2
# network parameters
num_hidden = 0
num_inputs = 2
num_outputs = 1
# node response options
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
# connection weight options
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