1

I am trying to make an AI that plays the Snake Game using the NEAT library in Python. I have most things figured out. I have chosen 6 inputs for my neural network ->

  1. Is it clear in forward direction ? (1-YES/0-NO)
  2. Is it clear in left direction ? (1-YES/0-NO)
  3. Is it clear in right direction ? (1-YES/0-NO)
  4. Is food there in forward direction ? (1-YES/0-NO)
  5. Is food there in left direction ? (1-YES/0-NO)
  6. Is food there in right direction ? (1-YES/0-NO)

The output is simply an array of three number -> (LEFT, FORWARD, RIGHT) which tell the snake whether to continue straight, or move to left or right. I have given fitness points as follows ->

  • +1 - moving towards the food
  • -1.5 - moving away from food
  • -5 - colliding with itself or a wall
  • +10 - eating a food

Even though I run my code for 150 generations, it still moves in circles as follows ->

GIF image here

I also added a maximum number of moves for the snake after which it dies. After eating a food, it gains some additional moves. But still it seems to have zero effect.

I am completely new to Neural Networks and this is one of my first projects. I am completely clueless as of now. Please do help. Below is my complete code

import pygame
import os
import time
import random
import neat


BLOCK_SIZE = 300
GRID_SIZE = 20
SIZE = BLOCK_SIZE//GRID_SIZE

GREY = (150, 150, 150)
BLACK = (0,0,0)
RED = (250, 50, 50)
ORANGE = (250, 120, 10)

GREEN = (23, 250, 40)
LIGHT_GREEN = (23, 150, 40)

GRID = [(0,0), (0, BLOCK_SIZE), (0, 2*BLOCK_SIZE), (0, 3*BLOCK_SIZE), (0, 4*BLOCK_SIZE),
        (BLOCK_SIZE, 0), (BLOCK_SIZE, BLOCK_SIZE), (BLOCK_SIZE, 2*BLOCK_SIZE), (BLOCK_SIZE, 3*BLOCK_SIZE), (BLOCK_SIZE, 4*BLOCK_SIZE)]

CELLS = [(x//GRID_SIZE, x - (x//GRID_SIZE)*GRID_SIZE) for x in range(GRID_SIZE*GRID_SIZE)]

def drawGrid(world):
    pygame.draw.line(world, GREY, (BLOCK_SIZE, 0), (BLOCK_SIZE, 2*BLOCK_SIZE), 3)
    pygame.draw.line(world, GREY, (2*BLOCK_SIZE, 0), (2*BLOCK_SIZE, 2*BLOCK_SIZE), 3)
    pygame.draw.line(world, GREY, (3*BLOCK_SIZE, 0), (3*BLOCK_SIZE, 2*BLOCK_SIZE), 3)
    pygame.draw.line(world, GREY, (4*BLOCK_SIZE, 0), (4*BLOCK_SIZE, 2*BLOCK_SIZE), 3)
    pygame.draw.line(world, GREY, (0, BLOCK_SIZE), (5*BLOCK_SIZE, BLOCK_SIZE), 3)

    pygame.draw.rect(world, GREY, (0, 0, 5*BLOCK_SIZE, 2*BLOCK_SIZE), 3)
    pygame.display.update()


class Snake:
    MAX_MOVES = 250
    INIT_MOVES = 100
    ADD_MOVES = 50
    def __init__(self,x, y, GRID_POS, world):
        self.X = GRID_POS[0]
        self.Y = GRID_POS[1]
        self.body = [(x, y), (x, y+1), (x, y+2)]
        self.WALLS = []
        self.tail = (-1, -1)
        self.moves = self.INIT_MOVES
        self.defineWalls(world)
        self.draw(world)

    def defineWalls(self, world):
        WALLS1 = [(-1 + self.X, x + self.Y) for x in range(-1, 21)]
        WALLS2 = [(20 + self.X, x + self.Y) for x in range(-1, 21)]
        WALLS3 = [(x + self.X, -1 + self.Y) for x in range(-1, 21)]
        WALLS4 = [(x + self.X, 20 + self.Y) for x in range(-1, 21)]

        self.WALLS = WALLS1 + WALLS2 + WALLS3 + WALLS4

    def draw(self, world):
        for part in self.body:
            pygame.draw.rect(world, RED, (part[1]*SIZE + self.Y, part[0]* SIZE + self.X, SIZE, SIZE), 0)
            pygame.draw.rect(world, BLACK, (part[1]*SIZE + self.Y, part[0]* SIZE + self.X, SIZE, SIZE), 1)

        pygame.draw.rect(world, ORANGE, (self.body[0][1]*SIZE + self.Y, self.body[0][0]* SIZE + self.X, SIZE, SIZE), 0)
        pygame.draw.rect(world, BLACK, (self.body[0][1]*SIZE + self.Y, self.body[0][0]* SIZE + self.X, SIZE, SIZE), 1)

        pygame.draw.rect(world, BLACK, (self.tail[1]*SIZE + self.Y, self.tail[0]* SIZE + self.X, SIZE, SIZE), 0)

        pygame.display.update()

    def collideSelf(self, dirs):
        snake_direction = (self.body[1][0] - self.body[0][0], self.body[1][1] - self.body[0][1])
        

    
        Dir = 0
        if dirs == (1, 0, 0):
            Dir = -1
        elif dirs == (0, 1, 0):
            Dir = 0
        elif dirs == (0, 0, 1):
            Dir = 1

        direction = 0

        if snake_direction[0] == 0:
            if snake_direction[1] == 1:
                direction = 3
            elif snake_direction[1] == -1:
                direction = 1

        elif snake_direction[1] == 0:
            if snake_direction[0] == 1:
                direction = 0
            elif snake_direction[0] == -1:
                direction = 2

        dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]

        move = dirs[(direction + Dir) % 4]
        head = (self.body[0][0] + move[0], self.body[0][1] + move[1])

        if head in self.body:
            return True

        return False

    def collideWall(self, dirs):
        snake_direction = (self.body[1][0] - self.body[0][0], self.body[1][1] - self.body[0][1])
        

    
        Dir = 0
        if dirs == (1, 0, 0):
            Dir = -1
        elif dirs == (0, 1, 0):
            Dir = 0
        elif dirs == (0, 0, 1):
            Dir = 1

        direction = 0

        if snake_direction[0] == 0:
            if snake_direction[1] == 1:
                direction = 3
            elif snake_direction[1] == -1:
                direction = 1

        elif snake_direction[1] == 0:
            if snake_direction[0] == 1:
                direction = 0
            elif snake_direction[0] == -1:
                direction = 2

        dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]

        move = dirs[(direction + Dir) % 4]
        head = (self.body[0][0] + move[0] + self.X, self.body[0][1] + move[1] + self.Y)

        # print(head)

        if head in self.WALLS:
            return True
        return False


    def move(self, output):
        snake_direction = (self.body[1][0] - self.body[0][0], self.body[1][1] - self.body[0][1])

        Dir = next(x for x, op in enumerate(output) if op == 1)

        if Dir == 0:
            Dir = -1
        elif Dir == 1:
            Dir = 0
        elif Dir == 2:
            Dir = 1

        direction = 0

        if snake_direction[0] == 0:
            if snake_direction[1] == 1:
                direction = 3
            elif snake_direction[1] == -1:
                direction = 1

        elif snake_direction[1] == 0:
            if snake_direction[0] == 1:
                direction = 0
            elif snake_direction[0] == -1:
                direction = 2

        dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]

        move = dirs[(direction + Dir) % 4]

        self.tail = self.body[len(self.body) - 1]

        for i in range(len(self.body)-1, 0, -1):
            self.body[i] = self.body[i-1]

        self.body[0] = (self.body[0][0] + move[0], self.body[0][1] + move[1])

        self.moves -= 1

    def grow(self):
        tail = self.body[len(self.body) - 1]

        self.body.append(tail)

        self.moves += self.ADD_MOVES

        if self.moves > self.MAX_MOVES:
            self.moves = self.MAX_MOVES


class Food:
    def __init__(self, snake):
        self.snake = snake.body
        self.x = 0
        self.y = 0
        self.X = snake.X
        self.Y = snake.Y
        self.distance = 0
        self.prX = -1
        self.prY = -1
        self.setPos(snake)
        self.setDist(snake)

    def setPos(self, snake):
        self.snake = snake.body

        possibilities = list(set(CELLS) - set(self.snake))

        position = random.choice(possibilities)

        self.prX = self.x
        self.prY = self.y

        self.x = position[0] 
        self.y = position[1] 

    def draw(self, world):
        pygame.draw.rect(world, LIGHT_GREEN, (self.y*SIZE + self.Y, self.x* SIZE + self.X, SIZE, SIZE), 0)
        pygame.draw.rect(world, GREEN, (self.y*SIZE + self.Y, self.x* SIZE + self.X, SIZE, SIZE), 2)

        pygame.draw.rect(world, BLACK, (self.prY*SIZE + self.Y, self.prX* SIZE + self.X, SIZE, SIZE), 0)
        pygame.draw.rect(world, BLACK, (self.prY*SIZE + self.Y, self.prX* SIZE + self.X, SIZE, SIZE), 2)

        pygame.display.update()

    def setDist(self, snake):
        self.snake = snake.body

        distance = ((self.snake[0][0] - self.x)**2 + (self.snake[0][1] - self.y)**2)**0.5

        self.distance = distance

def isClear(snake, Dir):
    snake_direction = (snake.body[1][0] - snake.body[0][0], snake.body[1][1] - snake.body[0][1])
        
    direction = 0

    if snake_direction[0] == 0:
        if snake_direction[1] == 1:
            direction = 3
        elif snake_direction[1] == -1:
            direction = 1

    elif snake_direction[1] == 0:
        if snake_direction[0] == 1:
            direction = 0
        elif snake_direction[0] == -1:
            direction = 2

    dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]

    move = dirs[(direction + Dir) % 4]
    head = (snake.body[0][0] + move[0], snake.body[0][1] + move[1])

    if snake.collideSelf(head) or snake.collideWall(head):
        return 1

    return 0

def whereIsFood(snake, food):
    snake_direction = (snake.body[1][0] - snake.body[0][0], snake.body[1][1] - snake.body[0][1])

    forward = 0
    right = 1

    direction = 0

    if snake_direction[0] == 0:
        if snake_direction[1] == 1:
            direction = 3
        elif snake_direction[1] == -1:
            direction = 1

    elif snake_direction[1] == 0:
        if snake_direction[0] == 1:
            direction = 0
        elif snake_direction[0] == -1:
            direction = 2

    dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]

    Left = dirs[(direction + left) % 4]
    Right = dirs[(direction + right) % 4]
    Forward = dirs[(direction + forward) % 4]

    head = snake.body[0]
    headLeft = (head[0] + Left[0], head[1] + Left[1])
    headRight = (head[0] + Right[0], head[1] + Right[1])
    headForward = (head[0] + Forward[0], head[1] + Forward[1])

    distLeft = ((headLeft[0] - food.x)**2 + (headLeft[1] - food.y)**2)**0.5
    distRight = ((headRight[0] - food.x)**2 + (headRight[1] - food.y)**2)**0.5
    distForward = ((headForward[0] - food.x)**2 + (headForward[1] - food.y)**2)**0.5

    minDist = min(distLeft, distRight, distForward)

    Foods = []
    if minDist == distLeft:
        Foods = [1, 0, 0]

    elif minDist == distRight:
        Foods = [0, 0, 1]

    elif minDist == distForward:
        Foods = [0, 1, 0]

    return Foods

def finalDirection(snake, maxOutput):
    if maxOutput == 0:
        Dir = -1
    elif maxOutput == 1:
        Dir = 0

    elif maxOutput == 2:
        Dir = 1

    snake_direction = (snake.body[1][0] - snake.body[0][0], snake.body[1][1] - snake.body[0][1])

    direction = 0

    if snake_direction[0] == 0:
        if snake_direction[1] == 1:
            direction = 3
        elif snake_direction[1] == -1:
            direction = 1

    elif snake_direction[1] == 0:
        if snake_direction[0] == 1:
            direction = 0
        elif snake_direction[0] == -1:
            direction = 2

    dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]

    move = dirs[(direction + Dir) % 4]
    head = (snake.body[0][0] + move[0], snake.body[0][1] + move[1])

    return head


def main(genomes, config):
    world = pygame.display.set_mode((5*BLOCK_SIZE, 2*BLOCK_SIZE))
    drawGrid(world)

    nets = []
    ge = []
    snakes = []
    foods = []

    for _, g in genomes:
        net = neat.nn.FeedForwardNetwork.create(g, config)
        nets.append(net)
        g.fitness = 0
        ge.append(g)

    for i in range(10):
        snakes.append(Snake(6, 10, GRID[i % 10], world))

    for snake in snakes:
        foods.append(Food(snake))

    for x, food in enumerate(foods):
        if x < 10:
            food.draw(world)

    run = True
    while run:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
                pygame.quit()

        for x, snake in enumerate(snakes):
            time.sleep(0.005)

            inputs = whereIsFood(snake, food)
            inputs.append(isClear(snake, -1))
            inputs.append(isClear(snake, 0))
            inputs.append(isClear(snake, 1))
            # inputs.append(foods[x].x)
            # inputs.append(foods[x].y)

            output = nets[x].activate(inputs)

            maxOutput = next(x for x, op in enumerate(output) if op == max(output))

            if maxOutput == 0:
                output = (1, 0, 0)

            elif maxOutput == 1:
                output = (0, 1, 0)

            elif maxOutput == 2:
                output = (0, 0, 1)

            headed = finalDirection(snake, maxOutput)

            rem = []
            if snake.collideWall(output) or snake.collideSelf(output):
                ge[x].fitness -= 10
                rem.append(snake)

            else:
                snake.move(output)
                if x < 10:
                    snake.draw(world)
                    drawGrid(world)

                # ge[x].fitness += 0.1

                if snake.moves <= 0:
                    rem.append(snake)
                    ge[x].fitness -= 10

                if foods[x].distance >= ((snake.body[0][0] - foods[x].x)**2 + (snake.body[0][1] - foods[x].y)**2)**0.5:
                    # print(foods[x].distance ,((snake.body[0][0] - foods[x].x)**2 + (snake.body[0][1] - foods[x].y)**2)**0.5)
                    ge[x].fitness += 1

                else:
                    ge[x].fitness -= 1.5

                foods[x].setDist(snake)

                if (foods[x].x, foods[x].y) == snake.body[0]:
                    ge[x].fitness += 30
                    snake.grow()
                    foods[x].setPos(snake)
                    if x < 10:
                        foods[x].draw(world)

        for x, snake in enumerate(snakes):
            if snake in rem:
                nets.pop(x)
                ge.pop(x)
                foods.pop(x)
                snakes.pop(x)

        if len(snakes) == 0:
            run = False
            break

def run(config_path):
    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(main,150)


if __name__ == "__main__":
    local_dir = os.path.dirname(__file__)
    config_path = os.path.join(local_dir, "config-FeedForward.txt")
    run(config_path)

And this is the NEAT Config File ->


[NEAT]
fitness_criterion     = max
fitness_threshold     = 1000
pop_size              = 1000
reset_on_extinction   = False

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

# node aggregation options
aggregation_default     = random
aggregation_mutate_rate = 0.05
aggregation_options     = sum product min max mean median maxabs

# node bias options
bias_init_mean          = 0.05
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.1 

# connection enable options
enabled_default         = False
enabled_mutate_rate     = 0.2 

feed_forward            = False
initial_connection      = full

# node add/remove rates
node_add_prob           = 0.5
node_delete_prob        = 0.1

# network parameters
num_hidden              = 0 
num_inputs              = 6
num_outputs             = 3

# node response options
response_init_mean      = 1.0
response_init_stdev     = 0.05
response_max_value      = 30.0
response_min_value      = -30.0
response_mutate_power   = 0.1
response_mutate_rate    = 0.75
response_replace_rate   = 0.1

# connection weight options
weight_init_mean        = 0.1
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 = 2.5

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

[DefaultReproduction]
elitism            = 8
survival_threshold = 0.3

Please let me know what I have done wrong. Thank you in advance !!

  • 1
    This question likely belongs on another StackExchange site. Your code runs (and your agent is definitely learning at least _some_ behaviour), so the issue is likely related to some RL-specific thing. You might try asking on https://ai.stackexchange.com/, perhaps someone over there might help – GPhilo Jul 29 '20 at 11:16
  • Thanks a lot !!. I actually figured something out, that my output is returning [0.0, 0.0, 0.0] for like 95 % of time. The way I am manipulating the output makes the snake take a left turn which is why it keeps circling. Can you help me with this ? – Niraj Sawant Jul 29 '20 at 11:30
  • I didn't go through all the details of the code, but your collide functions seems a bit over complicated to me. After all, you have the coordinates where everything is, and if you need to check that the head of the snake collided with something you simply need to check if the coordinates of the head are the same as the coordinates of either the wall or the body, right? Or am I missing something? – ChatterOne Jul 31 '20 at 09:28
  • I got all my code worked up, there were some issues with collide function and everything related to the functionality of the game works fine now . I played the game myself. The main issue I found out was that my OUTPUT array was [0.0, 0.0, 0.0]. It gave me same output even when I ran for 150 generations, changed the activation function and even population size (1000). I have uploaded my files on [My GitHub](https://github.com/NirajSawant136/Snake-AI-not-working-). I think I am doing some great mistake in my Config file. Please do help me, would be truly grateful – Niraj Sawant Jul 31 '20 at 09:52

0 Answers0