# 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)