How do I have the NEAT algorithm force a ship to shoot bullets?

During coding, I found that I was unable to have a ship fire a ‘bullet’ using NEAT, and even worse, I found that the bullet class required several things that I have no idea how to implement.


main.py:

import pygame
import math
import sys
import neat

pygame.init()

# Define Constants
SCALE = 1/2
SCREEN_WIDTH = 1634*2
SCREEN_HEIGHT = 842*2
SCALED_SCREEN = pygame.display.set_mode(
    (SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE))
SCREEN = pygame.surface.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
TEXT_SCREEN = pygame.surface.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),pygame.SRCALPHA, 32)
FONT = pygame.font.Font('img/FreeMono.ttf', 25)
DRAWING = SCREEN.copy()
show_debug = True
pressed = True
pygame.display.set_caption('Naval Simulation')

BULLET_SPEED = 500
BULLET_LIFETIME = 1000
BULLET_RATE = 150

class Bullet(pygame.sprite.Sprite):
    def __init__(self, game, pos, dir):
        super().__init__()
        self.groups = all_sprites
        pygame.sprite.Sprite.__init__(self, self.groups)
        self.image = pygame.image.load('img/bullet.png')
        self.pos = pos
        self.rect = self.image.get_rect(center = (590, 670))
        self.vel_vector = pygame.math.Vector2(5, 0)
        self.spawn_time = pygame.time.get_ticks()

    def update(self):
        self.pos += self.vel * (pygame.time.Clock(FPS) / 1000)
        self.rect.center = self.pos
        if pygame.time.get_ticks() - self.spawn_time > BULLET_LIFETIME:
            self.kill()
        
class Ship(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.original_image = pygame.image.load('img/Fletcher-class DD.png')
        self.original_image = pygame.transform.rotozoom(self.original_image, 0, 0.1)
        self.image = self.original_image
        self.rect = self.image.get_rect(center=(590, 670))
        self.vel_vector = pygame.math.Vector2(0.8, 0)
        self.angle = 0
        self.roatation_vel = 5
        self.direction = 0
        self.alive = True
        self.radars = []
        self.last_shot = 0

    def update(self):
        self.radars.clear()
        self.drive()
        self.rotate()
        for radar_angle in (-60, -30, 0, 30, 60):
            self.radar(radar_angle)
        self.collision()
        self.data()

    def drive(self):
        self.rect.center += self.vel_vector * 3

    def rotate(self):
        if self.direction == 1:
            self.angle -= self.roatation_vel
            self.vel_vector.rotate_ip(self.roatation_vel)
        elif self.direction == -1:
            self.angle += self.roatation_vel
            self.vel_vector.rotate_ip(-self.roatation_vel)

        self.image = pygame.transform.rotate(self.original_image,self.angle)
        self.rect = self.image.get_rect(center=self.rect.center)

    def radar(self, radar_angle):
        length = 0
        x = int(self.rect.center[0])
        y = int(self.rect.center[1])
        try:
            while not SCREEN.get_at((x, y)) == pygame.Color(2, 105, 31, 255) and length < 200:
                length += 1
                x = int(self.rect.center[0] +
                        math.cos(math.radians(self.angle + radar_angle)) * length)
                y = int(self.rect.center[1] -
                        math.sin(math.radians(self.angle + radar_angle)) * length)
        except IndexError:
            pass
        
        if show_debug:
            pygame.draw.line(SCREEN, (225, 225, 225, 225), self.rect.center,
                            (x, y), 1)
            pygame.draw.circle(SCREEN, (0, 225, 0, 0), (x, y), 3)

        dist = int(
            math.sqrt(
                math.pow(self.rect.center[0] - x, 2) +
                math.pow(self.rect.center[1] - y, 2)))

        self.radars.append([radar_angle, dist])

    def collision(self):
        length = 40
        collision_point_right = [
            int(self.rect.center[0] +
                math.cos(math.radians(self.angle + 18)) * length),
            int(self.rect.center[1] -
                math.sin(math.radians(self.angle + 18)) * length)
        ]
        collision_point_left = [
            int(self.rect.center[0] +
                math.cos(math.radians(self.angle - 18)) * length),
            int(self.rect.center[1] -
                math.sin(math.radians(self.angle - 18)) * length)
        ]
        try:
            coll_right = SCREEN.get_at((collision_point_right))
        except IndexError:
            coll_right = pygame.Color(2, 105, 31,255)
        try:
            coll_left = SCREEN.get_at((collision_point_left))
        except IndexError:
            coll_left = pygame.Color(2, 105, 31,255)

        if coll_right == pygame.Color(2, 105, 31,255) or coll_left == pygame.Color(2, 105, 31, 255):
            self.alive = False

        if show_debug:
            pygame.draw.circle(SCREEN, (0, 255, 255, 0), collision_point_right, 4)
            pygame.draw.circle(SCREEN, (0, 255, 255, 0), collision_point_left, 4)
            pygame.draw.circle(SCREEN, (0, 0, 0, 0), self.rect.center, 400, width=5)

    def data(self):
        input = [0, 0, 0, 0, 0]
        for i, radar in enumerate(self.radars):
            input[i] = int(radar[1])
        return input

def remove(index):
    ships.pop(index)
    ge.pop(index)
    nets.pop(index)

def eval_genomes(genomes, config):
    global ships, ge, nets, bullets, show_debug, pressed, SCALED_SCREEN

    ships = []
    ge = []
    nets = []
    bullets = []

    for genome_id, genome in genomes:
        ships.append(pygame.sprite.GroupSingle(Ship()))
        ge.append(genome)
        net = neat.nn.FeedForwardNetwork.create(genome, config)
        nets.append(net)
        bullets.append(pygame.sprite.GroupSingle(Bullet()))
        genome.fitness = 0


    run = True
    while run:
        text1 = FONT.render("Training...", True, (225, 225, 225))
        text2 = FONT.render(f"Generation: {pop.generation+1}", True, (225, 225, 225))
        text3 = FONT.render(f"Toggle Debug lines with \"H\"", True, (225, 225, 225))
        TEXT_SCREEN.fill((0,0,0,0))
        TEXT_SCREEN.blit(text1, (5,0))
        TEXT_SCREEN.blit(text2, (5,25))
        TEXT_SCREEN.blit(text3, (5,50))

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit(1)
        keys = pygame.key.get_pressed()
        if not pressed:
            if keys[pygame.K_h]:
                show_debug = not show_debug
                pressed = True
        else:
            if not keys[pygame.K_h]:
                pressed = False

        
        SCREEN.blit(DRAWING, (0, 0))

        if len(ships) == 0: break

        for i, ship in enumerate(ships):
            ge[i].fitness += 1
            if not ship.sprite.alive:
                remove(i)

        for i, ship in enumerate(ships):
            output = nets[i].activate(ship.sprite.data())
            if output[0] > 0.7:
                ship.sprite.direction = 1
            if output[1] > 0.7:
                ship.sprite.direction = -1
            if output[0] <= 0.7 and output[1] <= 0.7:
                ship.sprite.direction = 0

        # Update ship
        for ship in ships:
            ship.draw(SCREEN)
            ship.update()
        render()
        

def render():
    SCALED_SCREEN.blit(pygame.transform.scale(SCREEN,SCALED_SCREEN.get_rect().size), (0, 0))
    SCALED_SCREEN.blit(pygame.transform.scale(TEXT_SCREEN,SCALED_SCREEN.get_rect().size), (0, 0))
    pygame.display.update()

def draw():
    global DRAWING, SCALED_SCREEN
    SCREEN.fill((24, 62, 122))
    TEXT_SCREEN.fill((0,0,0,0))
    pygame.draw.circle(SCREEN, (24, 62, 122), (590, 670), 100)
    text1 = FONT.render("", True, (225, 225, 225))
    text2 = FONT.render("Press \"C\" to continue.", True, (225, 225, 225))
    TEXT_SCREEN.blit(text1, (5,0))
    TEXT_SCREEN.blit(text2, (5,25))

    while True:
        m_pressed = pygame.mouse.get_pressed()[0]
        m_pos = list(pygame.mouse.get_pos())
        m_pos[0] /= SCALE 
        m_pos[1] /= SCALE 
        if m_pressed:
            pygame.draw.circle(SCREEN, (24, 62, 122), m_pos, 60)
        keys = pygame.key.get_pressed()
        if keys[pygame.K_c]:
            DRAWING = SCREEN.copy()
            TEXT_SCREEN.fill((0,0,0,0))

            return

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit(1)
        render()

def run(config_path):
    global pop
    config = neat.config.Config(
        neat.DefaultGenome,
        neat.DefaultReproduction,
        neat.DefaultSpeciesSet,
        neat.DefaultStagnation,
        config_path
    )

    pop = neat.Population(config)

    pop.run(eval_genomes, 50)

if __name__ == '__main__':
    draw()
    run('config.txt')

config.txt:

[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 = 5
num_outputs = 2

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


When I tried to run my code, this error message popped up: ``` bullets.append(pygame.sprite.GroupSingle(Bullet())) TypeError: Bullet.__init__() missing 3 required positional arguments: 'game', 'pos', and 'dir' ```

How do I resolve this issue? Before this, I tried to automate it by manually pressing a key to fire the bullet, but that ended up breaking the program as the simulation paused the moment I pressed it.

If I’m reading this right, you’re just not instantiating your Bullet object correctly. You’re receiving the error because you’re missing the initial arguments.

class Bullet(pygame.sprite.Sprite):
    def __init__(self, game, pos, dir):

The above code means when you make your bullet object, you have to give it the game, its position, and its direction. So,

        bullets.append(pygame.sprite.GroupSingle(Bullet()))

needs to be changed to contain your new bullet’s game, position, and direction. I assume you’d give it default values.
For example ( formatted probably incorrectly with your code):

        bullets.append(pygame.sprite.GroupSingle(Bullet(game,(x,y),1)))
2 Likes