I am trying to implement NEAT for the snake game. My game logic is ready, which is working properly and NEAT configured. But even after 100 generations with 200 genomes per generation, the snakes perform very poorly. It barely ever eats more than 2 food. Below is the snip of the eval_genome function:
def eval_genome(genomes, config): clock = pygame.time.Clock() win = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT)) for genome_id, g in genomes: net = neat.nn.FeedForwardNetwork.create(g, config) g.fitness = 0 snake = Snake() food = Food(snake.body) run = True UP = DOWN = RIGHT = LEFT = MOVE_SNAKE = False moveToFood = 0 score = 0 moveCount = 0 while run: pygame.time.delay(50) clock.tick_busy_loop(10) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False snakeHeadX = snake.body['x'] snakeHeadY = snake.body['y'] snakeTailX = snake.body[len(snake.body)-1]['x'] snakeTailY = snake.body[len(snake.body)-1]['y'] snakeLength = len(snake.body) snakeHeadBottomDist = WIN_HEIGHT - snakeHeadY - STEP snakeHeadRightDist = WIN_WIDTH - snakeHeadX - STEP foodBottomDist = WIN_HEIGHT - food.y - STEP foodRightDist = WIN_WIDTH - food.x - STEP snakeFoodDistEuclidean = math.sqrt((snakeHeadX - food.x)**2 + (snakeHeadY - food.y)**2) snakeFoodDistManhattan = abs(snakeHeadX - food.x) + abs(snakeHeadY - food.y) viewDirections = snake.checkDirections(food, UP, DOWN, LEFT, RIGHT) deltaFoodDist = snakeFoodDistEuclidean outputs = net.activate((snakeHeadX, snakeHeadY, snakeHeadBottomDist, snakeHeadRightDist, snakeTailX, snakeTailY, snakeLength, moveCount, moveToFood, food.x, food.y, foodBottomDist, foodRightDist, snakeFoodDistEuclidean, snakeFoodDistManhattan, viewDirections, viewDirections, viewDirections, viewDirections, viewDirections, viewDirections, viewDirections, viewDirections, deltaFoodDist)) if (outputs == max(outputs) and not DOWN): snake.setDir(0,-1) UP = True LEFT = False RIGHT = False MOVE_SNAKE = True elif (outputs == max(outputs) and not UP): snake.setDir(0,1) DOWN = True LEFT = False RIGHT = False MOVE_SNAKE = True elif (outputs == max(outputs) and not RIGHT): snake.setDir(-1,0) LEFT = True UP = False DOWN = False MOVE_SNAKE = True elif (outputs == max(outputs) and not LEFT): snake.setDir(1,0) RIGHT = True UP = False DOWN = False MOVE_SNAKE = True elif (not MOVE_SNAKE): if (outputs == max(outputs)): snake.setDir(0,-1) UP = True MOVE_SNAKE = True elif (outputs == max(outputs)): snake.setDir(0,1) DOWN = True MOVE_SNAKE = True elif (outputs == max(outputs)): snake.setDir(-1,0) LEFT = True MOVE_SNAKE = True elif (outputs == max(outputs)): snake.setDir(1,0) RIGHT = True MOVE_SNAKE = True win.fill((0, 0, 0)) food.showFood(win) if(MOVE_SNAKE): snake.update() newSnakeHeadX = snake.body['x'] newSnakeHeadY = snake.body['y'] newFoodDist = math.sqrt((newSnakeHeadX - food.x)**2 + (newSnakeHeadY - food.y)**2) deltaFoodDist = newFoodDist - snakeFoodDistEuclidean moveCount += 1 if (newFoodDist <= snakeFoodDistEuclidean): g.fitness += 1 else: g.fitness -= 10 snake.show(win) if(snake.collision()): if score != 0: print('FINAL SCORE IS: '+ str(score)) g.fitness -= 50 break if(snake.eat(food,win)): g.fitness += 15 score += 1 if score == 1 : moveToFood = moveCount # foodEatenMove = pygame.time.get_ticks()/1000 else: moveToFood = moveCount - moveToFood food.foodLocation(snake.body) food.showFood(win)
Additionally, I am putting the definition of the checkDirections function. What it does is returns an array of size 8 corresponding to 8 directions where each value can be either 0 (not food or body), 1(food found but no body), 3(body found but no food), or 4(both body and food found).
def checkDirections(self, food, up, down, left, right): ''' x+STEP, y-STEP x+STEP, y+STEP x-STEP, y-STEP x-STEP, y+STEP x+STEP, y x, y-STEP x, y+STEP x-STEP, y ''' view =  x = self.xdir y = self.ydir view.append(self.check(x, y, STEP, -STEP, food.x, food.y)) view.append(self.check(x, y, STEP, STEP, food.x, food.y)) view.append(self.check(x, y, -STEP, -STEP, food.x, food.y)) view.append(self.check(x, y, -STEP, STEP, food.x, food.y)) view.append(self.check(x, y, STEP, 0, food.x, food.y)) view.append(self.check(x, y, 0, -STEP, food.x, food.y)) view.append(self.check(x, y, 0, STEP, food.x, food.y)) view.append(self.check(x, y, -STEP, 0, food.x, food.y)) if up == True: view = -999 elif down == True: view = -999 elif left == True: view == -999 elif right == True: view == -999 return view def check(self, x, y, xIncrement, yIncrement, foodX, foodY): value = 0 foodFound = False bodyFound = False while (x >= 0 and x <= WIN_WIDTH and y >= 0 and y <= WIN_HEIGHT): x += xIncrement y += yIncrement if (not foodFound): if (foodX == x and foodY == y): foodFound = True if (not bodyFound): for i in range(1, len(self.body)): if ((x == self.body[i]['x']) and (y == self.body[i]['y'])): bodyFound = True if (not bodyFound and not foodFound): value = 0 elif (not bodyFound and foodFound): value = 1 elif (bodyFound and not foodFound): value = 2 else: value = 3 return value
I am using sigmoid as the activation function. Although I have tried with tanh and relu as well with no luck. Below is the NEAT config file that I am using:
[NEAT] fitness_criterion = max fitness_threshold = 10000 pop_size = 200 reset_on_extinction = False [DefaultGenome] # node activation options activation_default = sigmoid activation_mutate_rate = 0.0 activation_options = sigmoid # 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 # was 30 max and -30 for min bias bias_max_value = 100.0 bias_min_value = -100.0 bias_mutate_power = 0.5 bias_mutate_rate = 0.7 bias_replace_rate = 0.3 # genome compatibility options compatibility_disjoint_coefficient = 1.0 compatibility_weight_coefficient = 0.5 # connection add/remove rates conn_add_prob = 0.8 conn_delete_prob = 0.56 # connection enable options enabled_default = True # below was 0.01 enabled_mutate_rate = 0.3 feed_forward = True initial_connection = full # node add/remove rates node_add_prob = 0.7 node_delete_prob = 0.4 # network parameters num_hidden = 0 num_inputs = 24 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 [DefaultStagnation] species_fitness_func = max max_stagnation = 20 species_elitism = 2 [DefaultReproduction] elitism = 2 survival_threshold = 0.2
If anyone has any insights or thoughts that could help improve the performance of the snake AI, please let me know.