AI Car Simulation NEAT
This commit is contained in:
commit
8bee5d5d67
79
config.txt
Normal file
79
config.txt
Normal file
@ -0,0 +1,79 @@
|
||||
[NEAT]
|
||||
fitness_criterion = max
|
||||
fitness_threshold = 10000000000
|
||||
pop_size = 50
|
||||
reset_on_extinction = True
|
||||
|
||||
[DefaultGenome]
|
||||
# node activation options
|
||||
activation_default = tanh
|
||||
activation_mutate_rate = 0.1
|
||||
activation_options = tanh
|
||||
|
||||
# node aggregation options
|
||||
aggregation_default = sum
|
||||
aggregation_mutate_rate = 0.01
|
||||
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.1
|
||||
|
||||
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 = 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.8
|
||||
weight_mutate_rate = 0.9
|
||||
weight_replace_rate = 0.1
|
||||
|
||||
[DefaultSpeciesSet]
|
||||
compatibility_threshold = 2.0
|
||||
|
||||
[DefaultStagnation]
|
||||
species_fitness_func = max
|
||||
max_stagnation = 20
|
||||
species_elitism = 2
|
||||
|
||||
[DefaultReproduction]
|
||||
elitism = 2
|
||||
survival_threshold = 0.4
|
||||
BIN
map_easy.png
Normal file
BIN
map_easy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
257
simulation.py
Normal file
257
simulation.py
Normal file
@ -0,0 +1,257 @@
|
||||
# This Code is Heavily Inspired By The YouTuber: Cheesy AI
|
||||
# Code Changed, Optimized And Commented By: NeuralNine (Florian Dedov)
|
||||
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
import os
|
||||
|
||||
import neat
|
||||
import pygame
|
||||
|
||||
# Constants
|
||||
WIDTH = 1600
|
||||
HEIGHT = 880
|
||||
|
||||
CAR_SIZE_X = 30
|
||||
CAR_SIZE_Y = 30
|
||||
|
||||
BORDER_COLOR = (255, 255, 255, 255) # Color To Crash on Hit
|
||||
|
||||
current_generation = 0 # Generation counter
|
||||
|
||||
class Car:
|
||||
|
||||
def __init__(self):
|
||||
# Load Car Sprite and Rotate
|
||||
self.sprite = pygame.image.load('car.png').convert() # Convert Speeds Up A Lot
|
||||
self.sprite = pygame.transform.scale(self.sprite, (CAR_SIZE_X, CAR_SIZE_Y))
|
||||
self.rotated_sprite = self.sprite
|
||||
|
||||
self.position = [690, 740] # Starting Position
|
||||
self.angle = 0
|
||||
self.speed = 0
|
||||
|
||||
self.speed_set = False # Flag For Default Speed Later on
|
||||
|
||||
self.center = [self.position[0] + CAR_SIZE_X / 2, self.position[1] + CAR_SIZE_Y / 2] # Calculate Center
|
||||
|
||||
self.radars = [] # List For Sensors / Radars
|
||||
self.drawing_radars = [] # Radars To Be Drawn
|
||||
|
||||
self.alive = True # Boolean To Check If Car is Crashed
|
||||
|
||||
self.distance = 0 # Distance Driven
|
||||
self.time = 0 # Time Passed
|
||||
|
||||
def draw(self, screen):
|
||||
screen.blit(self.rotated_sprite, self.position) # Draw Sprite
|
||||
# self.draw_radar(screen) OPTIONAL FOR SENSORS
|
||||
|
||||
def draw_radar(self, screen):
|
||||
# Optionally Draw All Sensors / Radars
|
||||
for radar in self.radars:
|
||||
position = radar[0]
|
||||
pygame.draw.line(screen, (0, 255, 0), self.center, position, 1)
|
||||
pygame.draw.circle(screen, (0, 255, 0), position, 5)
|
||||
|
||||
def check_collision(self, game_map):
|
||||
self.alive = True
|
||||
for point in self.corners:
|
||||
# If Any Corner Touches Border Color -> Crash
|
||||
# Assumes Rectangle
|
||||
if game_map.get_at((int(point[0]), int(point[1]))) == BORDER_COLOR:
|
||||
self.alive = False
|
||||
break
|
||||
|
||||
def check_radar(self, degree, game_map):
|
||||
length = 0
|
||||
x = int(self.center[0] + math.cos(math.radians(360 - (self.angle + degree))) * length)
|
||||
y = int(self.center[1] + math.sin(math.radians(360 - (self.angle + degree))) * length)
|
||||
|
||||
# While We Don't Hit BORDER_COLOR AND length < 300 (just a max) -> go further and further
|
||||
while not game_map.get_at((x, y)) == BORDER_COLOR and length < 300:
|
||||
length = length + 1
|
||||
x = int(self.center[0] + math.cos(math.radians(360 - (self.angle + degree))) * length)
|
||||
y = int(self.center[1] + math.sin(math.radians(360 - (self.angle + degree))) * length)
|
||||
|
||||
# Calculate Distance To Border And Append To Radars List
|
||||
dist = int(math.sqrt(math.pow(x - self.center[0], 2) + math.pow(y - self.center[1], 2)))
|
||||
self.radars.append([(x, y), dist])
|
||||
|
||||
def update(self, game_map):
|
||||
# Set The Speed To 20 For The First Time
|
||||
# Only When Having 4 Output Nodes With Speed Up and Down
|
||||
if not self.speed_set:
|
||||
self.speed = 10
|
||||
self.speed_set = True
|
||||
|
||||
# Get Rotated Sprite And Move Into The Right X-Direction
|
||||
# Don't Let The Car Go Closer Than 20px To The Edge
|
||||
self.rotated_sprite = self.rotate_center(self.sprite, self.angle)
|
||||
self.position[0] += math.cos(math.radians(360 - self.angle)) * self.speed
|
||||
self.position[0] = max(self.position[0], 20)
|
||||
self.position[0] = min(self.position[0], WIDTH - 120)
|
||||
|
||||
# Increase Distance and Time
|
||||
self.distance += self.speed
|
||||
self.time += 1
|
||||
|
||||
# Same For Y-Position
|
||||
self.position[1] += math.sin(math.radians(360 - self.angle)) * self.speed
|
||||
self.position[1] = max(self.position[1], 20)
|
||||
self.position[1] = min(self.position[1], WIDTH - 120)
|
||||
|
||||
# Calculate New Center
|
||||
self.center = [int(self.position[0]) + CAR_SIZE_X / 2, int(self.position[1]) + CAR_SIZE_Y / 2]
|
||||
|
||||
# Calculate Four Corners
|
||||
# Length Is Half The Side
|
||||
length = 0.5 * CAR_SIZE_X
|
||||
left_top = [self.center[0] + math.cos(math.radians(360 - (self.angle + 30))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 30))) * length]
|
||||
right_top = [self.center[0] + math.cos(math.radians(360 - (self.angle + 150))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 150))) * length]
|
||||
left_bottom = [self.center[0] + math.cos(math.radians(360 - (self.angle + 210))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 210))) * length]
|
||||
right_bottom = [self.center[0] + math.cos(math.radians(360 - (self.angle + 330))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 330))) * length]
|
||||
self.corners = [left_top, right_top, left_bottom, right_bottom]
|
||||
|
||||
# Check Collisions And Clear Radars
|
||||
self.check_collision(game_map)
|
||||
self.radars.clear()
|
||||
|
||||
# From -90 To 120 With Step-Size 45 Check Radar
|
||||
for d in range(-90, 120, 45):
|
||||
self.check_radar(d, game_map)
|
||||
|
||||
def get_data(self):
|
||||
# Get Distances To Border
|
||||
radars = self.radars
|
||||
return_values = [0, 0, 0, 0, 0]
|
||||
for i, radar in enumerate(radars):
|
||||
return_values[i] = int(radar[1] / 30)
|
||||
|
||||
return return_values
|
||||
|
||||
def is_alive(self):
|
||||
# Basic Alive Function
|
||||
return self.alive
|
||||
|
||||
def get_reward(self):
|
||||
# Calculate Reward (Maybe Change?)
|
||||
# return self.distance / 50.0
|
||||
return self.distance / (CAR_SIZE_X / 2)
|
||||
|
||||
def rotate_center(self, image, angle):
|
||||
# Rotate The Rectangle
|
||||
rectangle = image.get_rect()
|
||||
rotated_image = pygame.transform.rotate(image, angle)
|
||||
rotated_rectangle = rectangle.copy()
|
||||
rotated_rectangle.center = rotated_image.get_rect().center
|
||||
rotated_image = rotated_image.subsurface(rotated_rectangle).copy()
|
||||
return rotated_image
|
||||
|
||||
|
||||
def run_simulation(genomes, config):
|
||||
|
||||
# Empty Collections For Nets and Cars
|
||||
nets = []
|
||||
cars = []
|
||||
|
||||
# Initialize PyGame And The Display
|
||||
pygame.init()
|
||||
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||
|
||||
# For All Genomes Passed Create A New Neural Network
|
||||
for i, g in genomes:
|
||||
net = neat.nn.FeedForwardNetwork.create(g, config)
|
||||
nets.append(net)
|
||||
g.fitness = 0
|
||||
|
||||
cars.append(Car())
|
||||
|
||||
# Clock Settings
|
||||
# Font Settings & Loading Map
|
||||
clock = pygame.time.Clock()
|
||||
generation_font = pygame.font.SysFont("Arial", 30)
|
||||
alive_font = pygame.font.SysFont("Arial", 20)
|
||||
game_map = pygame.image.load('map.png').convert() # Convert Speeds Up A Lot
|
||||
|
||||
global current_generation
|
||||
current_generation += 1
|
||||
|
||||
# Simple Counter To Roughly Limit Time (Not Good Practice)
|
||||
counter = 0
|
||||
|
||||
while True:
|
||||
# Exit On Quit Event
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
sys.exit(0)
|
||||
|
||||
# For Each Car Get The Acton It Takes
|
||||
for i, car in enumerate(cars):
|
||||
output = nets[i].activate(car.get_data())
|
||||
choice = output.index(max(output))
|
||||
if choice == 0:
|
||||
car.angle += 10 # Left
|
||||
elif choice == 1:
|
||||
car.angle -= 10 # Right
|
||||
elif choice == 2:
|
||||
if(car.speed - 2 >= 12):
|
||||
car.speed -= 2 # Slow Down
|
||||
else:
|
||||
car.speed += 2 # Speed Up
|
||||
|
||||
# Check If Car Is Still Alive
|
||||
# Increase Fitness If Yes And Break Loop If Not
|
||||
still_alive = False
|
||||
for i, car in enumerate(cars):
|
||||
if car.is_alive():
|
||||
still_alive = True
|
||||
car.update(game_map)
|
||||
genomes[i][1].fitness += car.get_reward()
|
||||
|
||||
if still_alive == False:
|
||||
break
|
||||
|
||||
counter += 1
|
||||
if counter == 30 * 20: # Stop After About 20 Seconds
|
||||
break
|
||||
|
||||
# Draw Map And All Cars That Are Alive
|
||||
screen.blit(game_map, (0, 0))
|
||||
for car in cars:
|
||||
if car.is_alive():
|
||||
car.draw(screen)
|
||||
|
||||
# Display Info
|
||||
text = generation_font.render("Generation: " + str(current_generation), True, (0,0,0))
|
||||
text_rect = text.get_rect()
|
||||
text_rect.center = (100, 100)
|
||||
screen.blit(text, text_rect)
|
||||
|
||||
text = alive_font.render("Still Alive: " + str(still_alive), True, (0, 0, 0))
|
||||
text_rect = text.get_rect()
|
||||
text_rect.center = (100, 150)
|
||||
screen.blit(text, text_rect)
|
||||
|
||||
pygame.display.flip()
|
||||
clock.tick(60) # 60 FPS
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Load Config
|
||||
config_path = "./config.txt"
|
||||
config = neat.config.Config(neat.DefaultGenome,
|
||||
neat.DefaultReproduction,
|
||||
neat.DefaultSpeciesSet,
|
||||
neat.DefaultStagnation,
|
||||
config_path)
|
||||
|
||||
# Create Population And Add Reporters
|
||||
population = neat.Population(config)
|
||||
population.add_reporter(neat.StdOutReporter(True))
|
||||
stats = neat.StatisticsReporter()
|
||||
population.add_reporter(stats)
|
||||
|
||||
# Run Simulation For A Maximum of 1000 Generations
|
||||
population.run(run_simulation, 1000)
|
||||
Loading…
x
Reference in New Issue
Block a user