Implementing NEAT for a vey simple endless runner in Pygame

I am trying to implement NEAT into a very basic endless runner game. I made the game following a Pygame tutorial (https://www.youtube.com/watch?v=ZV8TNrwqG1Y). Though I have made quite a few changes, the core game has remained the same. It is a simple 2D endless runner game, where you can control the player (a square), move it left or right, and jump. A series of other squares come in from the right and you get a point each time you jump over an obstacle for the first time.

Apologies if the format of the question isn’t how it is supposed to be, I’m quite new to this platform.

After implement NEAT

The fitness goes up by a very small amount after the first generation, but from there on out its almost completely random. I have run multiple sessions of 50 generations (each of 50 population size), and I have noticed that by the end, the average fitness of the population seems to be increasing but here’s the thing: its completely random. The NN evolves to perform these random actions in a loop and it finds some success from it, and it thinks thats the right thing to do. It doesnt understand that its supposed to avoid the squares(obstacles). Some genomes look like they understand that they need to avoid the obstacles, but they are not chosen as the more random pattern repeating genomes are chosen as they, by sheer luck?, end up getting a better fitness score.

Here is the entire code so far:

import pygame
import random
import neat
import pickle
import time

pygame.init()

# Colors
white = (255, 255, 255)
black = (0, 0, 0)
green = (0, 255, 0)
red = (255, 0, 0)
orange = (255, 165, 0)
yellow = (255, 255, 0)

# Screen size
WIDTH = 450
HEIGHT = 300

# Screen
screen = pygame.display.set_mode([WIDTH, HEIGHT])
pygame.display.set_caption("ML project")
background = black
fps = 60

font = pygame.font.Font('freesansbold.ttf', 16)
timer = pygame.time.Clock()

# NEAT configuration
def eval_genomes(genomes, config):
    global running
    for genome_id, genome in genomes:
        net = neat.nn.FeedForwardNetwork.create(genome, config)
        genome.fitness = 0

        player_x = 50
        player_y = 200
        y_change = 0
        x_change = 0
        gravity = 1
        # obstacles = [300, 450, 600]
        ranges = [(300, 320), (400, 450), (500, 600)]
        obstacles = [random.randint(start, end) for start, end in ranges]
        obstacles_speed = 3
        active = True
        score = 0
        genome_running = True
        jump_penalty = 0
        last_jump_time = time.time()  
        jump_delay = 0.35
        start_time = time.time()
        move_reward = 0
        once_only = [True, True, True]
        inAir = False
        jump_once = True
        collision_logged = True


        while genome_running:
            survival_time = time.time() - start_time
            bonus = survival_time * 0.5  

            timer.tick(fps)
            screen.fill(background)

            distance_bw0 = obstacles[0] - player_x
            distance_bw1 = obstacles[1] - player_x
            distance_bw2 = obstacles[2] - player_x

            # normalized_neg1_to_1 = 1 - (distance_bw0 / WIDTH)
            # normalized_neg1_to_1 = normalized_neg1_to_1 * 2 - 1

            # twonormalized_neg1_to_1 = 1 - (distance_bw1 / WIDTH)
            # twonormalized_neg1_to_1 = twonormalized_neg1_to_1 * 2 - 1

            # threenormalized_neg1_to_1 = 1 - (distance_bw2 / WIDTH)
            # threenormalized_neg1_to_1 = threenormalized_neg1_to_1 * 2 - 1
                        
            distance_bw0y = 200 - player_y
            distance_bw1y = 200 - player_y
            distance_bw2y = 200 - player_y

            if not active:
                instruction_text = font.render('SpaceBar to Start', True, white, black)
                screen.blit(instruction_text, (150, 50))
                instruction_text = font.render('SpaceBar to jump, left and right keys to move', True, white, black)
                screen.blit(instruction_text, (65, 90))

            score_text = font.render(f'Score: {score}', True, white, black)
            screen.blit(score_text, (180, 240))

            score_text = font.render(f'Fitness: {int(genome.fitness)}', True, white, black)
            screen.blit(score_text, (180, 260))

            score_text = font.render(f'Bonus for time: {round(bonus, 3)}', True, white, black)
            screen.blit(score_text, (180, 280))
       
            # score_text = font.render(f'hmm : {obstacles[0]}', True, white, black)
            # screen.blit(score_text, (180, 270))

            pos_text = font.render(f'Obstacle1x: {distance_bw0} Obstacle2x: {distance_bw1} Obstacle3x: {distance_bw2} ', True, white, black)
            screen.blit(pos_text, (10, 50))

            pos_text = font.render(f'Obstacle1y: {distance_bw0y} Obstacle2y: {distance_bw1y} Obstacle3y: {distance_bw2y} ', True, white, black)
            screen.blit(pos_text, (10, 30))

            pos_text = font.render(f'Safe: {"Yes" if inAir else "No"}', True, white, black)
            screen.blit(pos_text, (10, 10))



            floor = pygame.draw.rect(screen, white, [0, 220, WIDTH, 5])
            player = pygame.draw.rect(screen, green, [player_x, player_y, 20, 20])

            obstacle0 = pygame.draw.rect(screen, red, [obstacles[0], 200, 20, 20])
            obstacle1 = pygame.draw.rect(screen, yellow, [obstacles[1], 200, 20, 20])
            obstacle2 = pygame.draw.rect(screen, orange, [obstacles[2], 200, 20, 20])

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                    genome_running = False

            if active:
                output = net.activate([player_x, player_y, distance_bw0, distance_bw1, distance_bw2, distance_bw0y,
                                    distance_bw1y, distance_bw2y, inAir])
                # output = net.activate([player_x, player_y, distance_bw0, distance_bw1, distance_bw2])

                current_time = time.time()
                
                if output[0] > 0.5:
                    if y_change == 0 and (current_time - last_jump_time) > jump_delay:
                        y_change = 18
                        last_jump_time = current_time

                if output[1] > 0.5 and output[2] < 0.5:
                    x_change = -4
                    # genome.fitness += 0.25
                    
                elif output[2] > 0.5 and output[1] < 0.5:
                    x_change = 4
                    # genome.fitness += 0.25
                
                elif output[3] > 0.5 and output[1] < 0.5 and output[2] < 0.5:
                    pass
                
                for i in range(len(obstacles)):
                    obstacles[i] -= obstacles_speed
                    if obstacles[i] < player_x and once_only[i]:
                        score += 1
                        genome.fitness += 1
                        once_only[i] = False

                    if obstacles[i] < -20:
                        obstacles[i] = random.randint(470, 570)
                        once_only[i] = True

                if player.colliderect(obstacle0) or player.colliderect(obstacle1) or player.colliderect(obstacle2) or player_x == 0 or player_x == 430 and collision_logged:
                    active = False
                    genome.fitness -= 1
                    genome.fitness += bonus
                    genome_running = False 
                    print(f"Collision! Genome {genome_id} fitness: {round(genome.fitness, 3)} Score {score}")
                    collision_logged = False

            # if output[0] < 0.5:
            #     x_change = 0

            # if output[1] < 0.5:
            #     x_change = 0

            if output[0] <= 0.5 and output[1] <= 0.5:
                x_change = 0

            if 0 <= player_x <= 430:
                player_x += x_change
                # if x_change != 0 and player_x > 0:
                #     genome.fitness += 0.25

            if player_x < 0:
                player_x = 0

            if player_x > 430:
                player_x = 430

            
            if y_change > 0 or player_y < 200:
                player_y -= y_change
                y_change -= gravity
                if jump_once:
                    # genome.fitness -= 10
                    jump_once = False


            if player_y > 200:
                player_y = 200
                jump_once = True

            if player_y == 200 and y_change < 0:
                y_change = 0
                jump_once = True

            if player_y > 170:
                inAir = False
            else:
                inAir = True
            

            pygame.display.flip()

        
            
        # genome.fitness += jump_penalty
        # genome.fitness += move_reward


config_path = "file.txt"
config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction,
                            neat.DefaultSpeciesSet, neat.DefaultStagnation,
                            config_path)

p = neat.Population(config)

p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)

winner = p.run(eval_genomes, 50)

with open('winner.pkl', 'wb') as f:
    pickle.dump(winner, f)
print("Training completed. Best genome saved.")

pygame.quit()

And here is the Config file:

[NEAT]
fitness_criterion     = max
fitness_threshold     = 400
pop_size              = 20
reset_on_extinction   = False

[DefaultStagnation]
species_fitness_func = max
max_stagnation       = 20
species_elitism      = 2

[DefaultReproduction]
elitism            = 2
survival_threshold = 0.2

[DefaultGenome]
# node activation options
activation_default      = relu
activation_mutate_rate  = 1.0
activation_options      = relu

# node aggregation options
aggregation_default     = sum
aggregation_mutate_rate = 0.0
aggregation_options     = sum

# node bias options
bias_init_mean          = 3.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_direct

# node add/remove rates
node_add_prob           = 0.2
node_delete_prob        = 0.2

# network parameters
num_hidden              = 1
num_inputs              = 9
num_outputs             = 4

# 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

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật