├── car.png ├── config.txt ├── map.png ├── map2.png ├── map3.png ├── map4.png ├── map5.png └── newcar.py /car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeuralNine/ai-car-simulation/ccb65092fcde031d4bad3c55e93f56402a1c810e/car.png -------------------------------------------------------------------------------- /config.txt: -------------------------------------------------------------------------------- 1 | [NEAT] 2 | fitness_criterion = max 3 | fitness_threshold = 100000000 4 | pop_size = 30 5 | reset_on_extinction = True 6 | 7 | [DefaultGenome] 8 | # node activation options 9 | activation_default = tanh 10 | activation_mutate_rate = 0.01 11 | activation_options = tanh 12 | 13 | # node aggregation options 14 | aggregation_default = sum 15 | aggregation_mutate_rate = 0.01 16 | aggregation_options = sum 17 | 18 | # node bias options 19 | bias_init_mean = 0.0 20 | bias_init_stdev = 1.0 21 | bias_max_value = 30.0 22 | bias_min_value = -30.0 23 | bias_mutate_power = 0.5 24 | bias_mutate_rate = 0.7 25 | bias_replace_rate = 0.1 26 | 27 | # genome compatibility options 28 | compatibility_disjoint_coefficient = 1.0 29 | compatibility_weight_coefficient = 0.5 30 | 31 | # connection add/remove rates 32 | conn_add_prob = 0.5 33 | conn_delete_prob = 0.5 34 | 35 | # connection enable options 36 | enabled_default = True 37 | enabled_mutate_rate = 0.01 38 | 39 | feed_forward = True 40 | initial_connection = full 41 | 42 | # node add/remove rates 43 | node_add_prob = 0.2 44 | node_delete_prob = 0.2 45 | 46 | # network parameters 47 | num_hidden = 0 48 | num_inputs = 5 49 | num_outputs = 4 50 | 51 | # node response options 52 | response_init_mean = 1.0 53 | response_init_stdev = 0.0 54 | response_max_value = 30.0 55 | response_min_value = -30.0 56 | response_mutate_power = 0.0 57 | response_mutate_rate = 0.0 58 | response_replace_rate = 0.0 59 | 60 | # connection weight options 61 | weight_init_mean = 0.0 62 | weight_init_stdev = 1.0 63 | weight_max_value = 30 64 | weight_min_value = -30 65 | weight_mutate_power = 0.5 66 | weight_mutate_rate = 0.8 67 | weight_replace_rate = 0.1 68 | 69 | [DefaultSpeciesSet] 70 | compatibility_threshold = 2.0 71 | 72 | [DefaultStagnation] 73 | species_fitness_func = max 74 | max_stagnation = 20 75 | species_elitism = 2 76 | 77 | [DefaultReproduction] 78 | elitism = 3 79 | survival_threshold = 0.2 -------------------------------------------------------------------------------- /map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeuralNine/ai-car-simulation/ccb65092fcde031d4bad3c55e93f56402a1c810e/map.png -------------------------------------------------------------------------------- /map2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeuralNine/ai-car-simulation/ccb65092fcde031d4bad3c55e93f56402a1c810e/map2.png -------------------------------------------------------------------------------- /map3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeuralNine/ai-car-simulation/ccb65092fcde031d4bad3c55e93f56402a1c810e/map3.png -------------------------------------------------------------------------------- /map4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeuralNine/ai-car-simulation/ccb65092fcde031d4bad3c55e93f56402a1c810e/map4.png -------------------------------------------------------------------------------- /map5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeuralNine/ai-car-simulation/ccb65092fcde031d4bad3c55e93f56402a1c810e/map5.png -------------------------------------------------------------------------------- /newcar.py: -------------------------------------------------------------------------------- 1 | # This Code is Heavily Inspired By The YouTuber: Cheesy AI 2 | # Code Changed, Optimized And Commented By: NeuralNine (Florian Dedov) 3 | 4 | import math 5 | import random 6 | import sys 7 | import os 8 | 9 | import neat 10 | import pygame 11 | 12 | # Constants 13 | # WIDTH = 1600 14 | # HEIGHT = 880 15 | 16 | WIDTH = 1920 17 | HEIGHT = 1080 18 | 19 | CAR_SIZE_X = 60 20 | CAR_SIZE_Y = 60 21 | 22 | BORDER_COLOR = (255, 255, 255, 255) # Color To Crash on Hit 23 | 24 | current_generation = 0 # Generation counter 25 | 26 | class Car: 27 | 28 | def __init__(self): 29 | # Load Car Sprite and Rotate 30 | self.sprite = pygame.image.load('car.png').convert() # Convert Speeds Up A Lot 31 | self.sprite = pygame.transform.scale(self.sprite, (CAR_SIZE_X, CAR_SIZE_Y)) 32 | self.rotated_sprite = self.sprite 33 | 34 | # self.position = [690, 740] # Starting Position 35 | self.position = [830, 920] # Starting Position 36 | self.angle = 0 37 | self.speed = 0 38 | 39 | self.speed_set = False # Flag For Default Speed Later on 40 | 41 | self.center = [self.position[0] + CAR_SIZE_X / 2, self.position[1] + CAR_SIZE_Y / 2] # Calculate Center 42 | 43 | self.radars = [] # List For Sensors / Radars 44 | self.drawing_radars = [] # Radars To Be Drawn 45 | 46 | self.alive = True # Boolean To Check If Car is Crashed 47 | 48 | self.distance = 0 # Distance Driven 49 | self.time = 0 # Time Passed 50 | 51 | def draw(self, screen): 52 | screen.blit(self.rotated_sprite, self.position) # Draw Sprite 53 | self.draw_radar(screen) #OPTIONAL FOR SENSORS 54 | 55 | def draw_radar(self, screen): 56 | # Optionally Draw All Sensors / Radars 57 | for radar in self.radars: 58 | position = radar[0] 59 | pygame.draw.line(screen, (0, 255, 0), self.center, position, 1) 60 | pygame.draw.circle(screen, (0, 255, 0), position, 5) 61 | 62 | def check_collision(self, game_map): 63 | self.alive = True 64 | for point in self.corners: 65 | # If Any Corner Touches Border Color -> Crash 66 | # Assumes Rectangle 67 | if game_map.get_at((int(point[0]), int(point[1]))) == BORDER_COLOR: 68 | self.alive = False 69 | break 70 | 71 | def check_radar(self, degree, game_map): 72 | length = 0 73 | x = int(self.center[0] + math.cos(math.radians(360 - (self.angle + degree))) * length) 74 | y = int(self.center[1] + math.sin(math.radians(360 - (self.angle + degree))) * length) 75 | 76 | # While We Don't Hit BORDER_COLOR AND length < 300 (just a max) -> go further and further 77 | while not game_map.get_at((x, y)) == BORDER_COLOR and length < 300: 78 | length = length + 1 79 | x = int(self.center[0] + math.cos(math.radians(360 - (self.angle + degree))) * length) 80 | y = int(self.center[1] + math.sin(math.radians(360 - (self.angle + degree))) * length) 81 | 82 | # Calculate Distance To Border And Append To Radars List 83 | dist = int(math.sqrt(math.pow(x - self.center[0], 2) + math.pow(y - self.center[1], 2))) 84 | self.radars.append([(x, y), dist]) 85 | 86 | def update(self, game_map): 87 | # Set The Speed To 20 For The First Time 88 | # Only When Having 4 Output Nodes With Speed Up and Down 89 | if not self.speed_set: 90 | self.speed = 20 91 | self.speed_set = True 92 | 93 | # Get Rotated Sprite And Move Into The Right X-Direction 94 | # Don't Let The Car Go Closer Than 20px To The Edge 95 | self.rotated_sprite = self.rotate_center(self.sprite, self.angle) 96 | self.position[0] += math.cos(math.radians(360 - self.angle)) * self.speed 97 | self.position[0] = max(self.position[0], 20) 98 | self.position[0] = min(self.position[0], WIDTH - 120) 99 | 100 | # Increase Distance and Time 101 | self.distance += self.speed 102 | self.time += 1 103 | 104 | # Same For Y-Position 105 | self.position[1] += math.sin(math.radians(360 - self.angle)) * self.speed 106 | self.position[1] = max(self.position[1], 20) 107 | self.position[1] = min(self.position[1], WIDTH - 120) 108 | 109 | # Calculate New Center 110 | self.center = [int(self.position[0]) + CAR_SIZE_X / 2, int(self.position[1]) + CAR_SIZE_Y / 2] 111 | 112 | # Calculate Four Corners 113 | # Length Is Half The Side 114 | length = 0.5 * CAR_SIZE_X 115 | 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] 116 | 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] 117 | 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] 118 | 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] 119 | self.corners = [left_top, right_top, left_bottom, right_bottom] 120 | 121 | # Check Collisions And Clear Radars 122 | self.check_collision(game_map) 123 | self.radars.clear() 124 | 125 | # From -90 To 120 With Step-Size 45 Check Radar 126 | for d in range(-90, 120, 45): 127 | self.check_radar(d, game_map) 128 | 129 | def get_data(self): 130 | # Get Distances To Border 131 | radars = self.radars 132 | return_values = [0, 0, 0, 0, 0] 133 | for i, radar in enumerate(radars): 134 | return_values[i] = int(radar[1] / 30) 135 | 136 | return return_values 137 | 138 | def is_alive(self): 139 | # Basic Alive Function 140 | return self.alive 141 | 142 | def get_reward(self): 143 | # Calculate Reward (Maybe Change?) 144 | # return self.distance / 50.0 145 | return self.distance / (CAR_SIZE_X / 2) 146 | 147 | def rotate_center(self, image, angle): 148 | # Rotate The Rectangle 149 | rectangle = image.get_rect() 150 | rotated_image = pygame.transform.rotate(image, angle) 151 | rotated_rectangle = rectangle.copy() 152 | rotated_rectangle.center = rotated_image.get_rect().center 153 | rotated_image = rotated_image.subsurface(rotated_rectangle).copy() 154 | return rotated_image 155 | 156 | 157 | def run_simulation(genomes, config): 158 | 159 | # Empty Collections For Nets and Cars 160 | nets = [] 161 | cars = [] 162 | 163 | # Initialize PyGame And The Display 164 | pygame.init() 165 | screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN) 166 | 167 | # For All Genomes Passed Create A New Neural Network 168 | for i, g in genomes: 169 | net = neat.nn.FeedForwardNetwork.create(g, config) 170 | nets.append(net) 171 | g.fitness = 0 172 | 173 | cars.append(Car()) 174 | 175 | # Clock Settings 176 | # Font Settings & Loading Map 177 | clock = pygame.time.Clock() 178 | generation_font = pygame.font.SysFont("Arial", 30) 179 | alive_font = pygame.font.SysFont("Arial", 20) 180 | game_map = pygame.image.load('map.png').convert() # Convert Speeds Up A Lot 181 | 182 | global current_generation 183 | current_generation += 1 184 | 185 | # Simple Counter To Roughly Limit Time (Not Good Practice) 186 | counter = 0 187 | 188 | while True: 189 | # Exit On Quit Event 190 | for event in pygame.event.get(): 191 | if event.type == pygame.QUIT: 192 | sys.exit(0) 193 | 194 | # For Each Car Get The Acton It Takes 195 | for i, car in enumerate(cars): 196 | output = nets[i].activate(car.get_data()) 197 | choice = output.index(max(output)) 198 | if choice == 0: 199 | car.angle += 10 # Left 200 | elif choice == 1: 201 | car.angle -= 10 # Right 202 | elif choice == 2: 203 | if(car.speed - 2 >= 12): 204 | car.speed -= 2 # Slow Down 205 | else: 206 | car.speed += 2 # Speed Up 207 | 208 | # Check If Car Is Still Alive 209 | # Increase Fitness If Yes And Break Loop If Not 210 | still_alive = 0 211 | for i, car in enumerate(cars): 212 | if car.is_alive(): 213 | still_alive += 1 214 | car.update(game_map) 215 | genomes[i][1].fitness += car.get_reward() 216 | 217 | if still_alive == 0: 218 | break 219 | 220 | counter += 1 221 | if counter == 30 * 40: # Stop After About 20 Seconds 222 | break 223 | 224 | # Draw Map And All Cars That Are Alive 225 | screen.blit(game_map, (0, 0)) 226 | for car in cars: 227 | if car.is_alive(): 228 | car.draw(screen) 229 | 230 | # Display Info 231 | text = generation_font.render("Generation: " + str(current_generation), True, (0,0,0)) 232 | text_rect = text.get_rect() 233 | text_rect.center = (900, 450) 234 | screen.blit(text, text_rect) 235 | 236 | text = alive_font.render("Still Alive: " + str(still_alive), True, (0, 0, 0)) 237 | text_rect = text.get_rect() 238 | text_rect.center = (900, 490) 239 | screen.blit(text, text_rect) 240 | 241 | pygame.display.flip() 242 | clock.tick(60) # 60 FPS 243 | 244 | if __name__ == "__main__": 245 | 246 | # Load Config 247 | config_path = "./config.txt" 248 | config = neat.config.Config(neat.DefaultGenome, 249 | neat.DefaultReproduction, 250 | neat.DefaultSpeciesSet, 251 | neat.DefaultStagnation, 252 | config_path) 253 | 254 | # Create Population And Add Reporters 255 | population = neat.Population(config) 256 | population.add_reporter(neat.StdOutReporter(True)) 257 | stats = neat.StatisticsReporter() 258 | population.add_reporter(stats) 259 | 260 | # Run Simulation For A Maximum of 1000 Generations 261 | population.run(run_simulation, 1000) 262 | --------------------------------------------------------------------------------