commit 8bee5d5d67a32bd08d8c135f6d240367274acf2d Author: NeuralNine Date: Tue Feb 2 21:02:03 2021 +0100 AI Car Simulation NEAT diff --git a/car.png b/car.png new file mode 100644 index 0000000..8245b19 Binary files /dev/null and b/car.png differ diff --git a/config.txt b/config.txt new file mode 100644 index 0000000..3fa3e3f --- /dev/null +++ b/config.txt @@ -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 diff --git a/map.png b/map.png new file mode 100644 index 0000000..ee5c6c3 Binary files /dev/null and b/map.png differ diff --git a/map_easy.png b/map_easy.png new file mode 100644 index 0000000..6f43fc9 Binary files /dev/null and b/map_easy.png differ diff --git a/simulation.py b/simulation.py new file mode 100644 index 0000000..bdbce46 --- /dev/null +++ b/simulation.py @@ -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)