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 ->
- Is it clear in forward direction ? (1-YES/0-NO)
- Is it clear in left direction ? (1-YES/0-NO)
- Is it clear in right direction ? (1-YES/0-NO)
- Is food there in forward direction ? (1-YES/0-NO)
- Is food there in left direction ? (1-YES/0-NO)
- 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 ->
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 !!