├── README.md ├── best.pickle ├── config.txt ├── main.py ├── pong ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── ball.cpython-38.pyc │ ├── game.cpython-38.pyc │ └── paddle.cpython-38.pyc ├── ball.py ├── game.py └── paddle.py ├── requirements.txt └── tutorial.py /README.md: -------------------------------------------------------------------------------- 1 | # NEAT-Pong-Python 2 | Using the NEAT algorithm to train an AI to play pong in Python! 3 | 4 | 5 | # 💻 Launch Your Software Development Career Today! 6 | 7 | 🎓 **No degree? No problem!** My program equips you with everything you need to break into tech and land an entry-level software development role. 8 | 9 | 🚀 **Why Join?** 10 | - 💼 **$70k+ starting salary potential** 11 | - 🕐 **Self-paced:** Complete on your own time 12 | - 🤑 **Affordable:** Low risk compared to expensive bootcamps or degrees 13 | - 🎯 **45,000+ job openings** in the market 14 | 15 | 👉 **[Start your journey today!](https://techwithtim.net/dev)** 16 | No experience needed—just your determination. Future-proof your career and unlock six-figure potential like many of our students have! 17 | -------------------------------------------------------------------------------- /best.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithtim/NEAT-Pong-Python/52edc429608f43206770390da73b5237407b7b19/best.pickle -------------------------------------------------------------------------------- /config.txt: -------------------------------------------------------------------------------- 1 | [NEAT] 2 | fitness_criterion = max 3 | fitness_threshold = 400 4 | pop_size = 50 5 | reset_on_extinction = False 6 | 7 | [DefaultStagnation] 8 | species_fitness_func = max 9 | max_stagnation = 20 10 | species_elitism = 2 11 | 12 | [DefaultReproduction] 13 | elitism = 2 14 | survival_threshold = 0.2 15 | 16 | [DefaultGenome] 17 | # node activation options 18 | activation_default = relu 19 | activation_mutate_rate = 1.0 20 | activation_options = relu 21 | 22 | # node aggregation options 23 | aggregation_default = sum 24 | aggregation_mutate_rate = 0.0 25 | aggregation_options = sum 26 | 27 | # node bias options 28 | bias_init_mean = 3.0 29 | bias_init_stdev = 1.0 30 | bias_max_value = 30.0 31 | bias_min_value = -30.0 32 | bias_mutate_power = 0.5 33 | bias_mutate_rate = 0.7 34 | bias_replace_rate = 0.1 35 | 36 | # genome compatibility options 37 | compatibility_disjoint_coefficient = 1.0 38 | compatibility_weight_coefficient = 0.5 39 | 40 | # connection add/remove rates 41 | conn_add_prob = 0.5 42 | conn_delete_prob = 0.5 43 | 44 | # connection enable options 45 | enabled_default = True 46 | enabled_mutate_rate = 0.01 47 | 48 | feed_forward = True 49 | initial_connection = full_direct 50 | 51 | # node add/remove rates 52 | node_add_prob = 0.2 53 | node_delete_prob = 0.2 54 | 55 | # network parameters 56 | num_hidden = 2 57 | num_inputs = 3 58 | num_outputs = 3 59 | 60 | # node response options 61 | response_init_mean = 1.0 62 | response_init_stdev = 0.0 63 | response_max_value = 30.0 64 | response_min_value = -30.0 65 | response_mutate_power = 0.0 66 | response_mutate_rate = 0.0 67 | response_replace_rate = 0.0 68 | 69 | # connection weight options 70 | weight_init_mean = 0.0 71 | weight_init_stdev = 1.0 72 | weight_max_value = 30 73 | weight_min_value = -30 74 | weight_mutate_power = 0.5 75 | weight_mutate_rate = 0.8 76 | weight_replace_rate = 0.1 77 | 78 | [DefaultSpeciesSet] 79 | compatibility_threshold = 3.0 -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # https://neat-python.readthedocs.io/en/latest/xor_example.html 2 | from pong import Game 3 | import pygame 4 | import neat 5 | import os 6 | import time 7 | import pickle 8 | 9 | 10 | class PongGame: 11 | def __init__(self, window, width, height): 12 | self.game = Game(window, width, height) 13 | self.ball = self.game.ball 14 | self.left_paddle = self.game.left_paddle 15 | self.right_paddle = self.game.right_paddle 16 | 17 | def test_ai(self, net): 18 | """ 19 | Test the AI against a human player by passing a NEAT neural network 20 | """ 21 | clock = pygame.time.Clock() 22 | run = True 23 | while run: 24 | clock.tick(60) 25 | game_info = self.game.loop() 26 | 27 | for event in pygame.event.get(): 28 | if event.type == pygame.QUIT: 29 | run = False 30 | break 31 | 32 | output = net.activate((self.right_paddle.y, abs( 33 | self.right_paddle.x - self.ball.x), self.ball.y)) 34 | decision = output.index(max(output)) 35 | 36 | if decision == 1: # AI moves up 37 | self.game.move_paddle(left=False, up=True) 38 | elif decision == 2: # AI moves down 39 | self.game.move_paddle(left=False, up=False) 40 | 41 | keys = pygame.key.get_pressed() 42 | if keys[pygame.K_w]: 43 | self.game.move_paddle(left=True, up=True) 44 | elif keys[pygame.K_s]: 45 | self.game.move_paddle(left=True, up=False) 46 | 47 | self.game.draw(draw_score=True) 48 | pygame.display.update() 49 | 50 | def train_ai(self, genome1, genome2, config, draw=False): 51 | """ 52 | Train the AI by passing two NEAT neural networks and the NEAt config object. 53 | These AI's will play against eachother to determine their fitness. 54 | """ 55 | run = True 56 | start_time = time.time() 57 | 58 | net1 = neat.nn.FeedForwardNetwork.create(genome1, config) 59 | net2 = neat.nn.FeedForwardNetwork.create(genome2, config) 60 | self.genome1 = genome1 61 | self.genome2 = genome2 62 | 63 | max_hits = 50 64 | 65 | while run: 66 | for event in pygame.event.get(): 67 | if event.type == pygame.QUIT: 68 | return True 69 | 70 | game_info = self.game.loop() 71 | 72 | self.move_ai_paddles(net1, net2) 73 | 74 | if draw: 75 | self.game.draw(draw_score=False, draw_hits=True) 76 | 77 | pygame.display.update() 78 | 79 | duration = time.time() - start_time 80 | if game_info.left_score == 1 or game_info.right_score == 1 or game_info.left_hits >= max_hits: 81 | self.calculate_fitness(game_info, duration) 82 | break 83 | 84 | return False 85 | 86 | def move_ai_paddles(self, net1, net2): 87 | """ 88 | Determine where to move the left and the right paddle based on the two 89 | neural networks that control them. 90 | """ 91 | players = [(self.genome1, net1, self.left_paddle, True), (self.genome2, net2, self.right_paddle, False)] 92 | for (genome, net, paddle, left) in players: 93 | output = net.activate( 94 | (paddle.y, abs(paddle.x - self.ball.x), self.ball.y)) 95 | decision = output.index(max(output)) 96 | 97 | valid = True 98 | if decision == 0: # Don't move 99 | genome.fitness -= 0.01 # we want to discourage this 100 | elif decision == 1: # Move up 101 | valid = self.game.move_paddle(left=left, up=True) 102 | else: # Move down 103 | valid = self.game.move_paddle(left=left, up=False) 104 | 105 | if not valid: # If the movement makes the paddle go off the screen punish the AI 106 | genome.fitness -= 1 107 | 108 | def calculate_fitness(self, game_info, duration): 109 | self.genome1.fitness += game_info.left_hits + duration 110 | self.genome2.fitness += game_info.right_hits + duration 111 | 112 | 113 | def eval_genomes(genomes, config): 114 | """ 115 | Run each genome against eachother one time to determine the fitness. 116 | """ 117 | width, height = 700, 500 118 | win = pygame.display.set_mode((width, height)) 119 | pygame.display.set_caption("Pong") 120 | 121 | for i, (genome_id1, genome1) in enumerate(genomes): 122 | print(round(i/len(genomes) * 100), end=" ") 123 | genome1.fitness = 0 124 | for genome_id2, genome2 in genomes[min(i+1, len(genomes) - 1):]: 125 | genome2.fitness = 0 if genome2.fitness == None else genome2.fitness 126 | pong = PongGame(win, width, height) 127 | 128 | force_quit = pong.train_ai(genome1, genome2, config, draw=True) 129 | if force_quit: 130 | quit() 131 | 132 | 133 | def run_neat(config): 134 | #p = neat.Checkpointer.restore_checkpoint('neat-checkpoint-85') 135 | p = neat.Population(config) 136 | p.add_reporter(neat.StdOutReporter(True)) 137 | stats = neat.StatisticsReporter() 138 | p.add_reporter(stats) 139 | p.add_reporter(neat.Checkpointer(1)) 140 | 141 | winner = p.run(eval_genomes, 50) 142 | with open("best.pickle", "wb") as f: 143 | pickle.dump(winner, f) 144 | 145 | 146 | def test_best_network(config): 147 | with open("best.pickle", "rb") as f: 148 | winner = pickle.load(f) 149 | winner_net = neat.nn.FeedForwardNetwork.create(winner, config) 150 | 151 | width, height = 700, 500 152 | win = pygame.display.set_mode((width, height)) 153 | pygame.display.set_caption("Pong") 154 | pong = PongGame(win, width, height) 155 | pong.test_ai(winner_net) 156 | 157 | 158 | if __name__ == '__main__': 159 | local_dir = os.path.dirname(__file__) 160 | config_path = os.path.join(local_dir, 'config.txt') 161 | 162 | config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction, 163 | neat.DefaultSpeciesSet, neat.DefaultStagnation, 164 | config_path) 165 | 166 | run_neat(config) 167 | test_best_network(config) 168 | -------------------------------------------------------------------------------- /pong/__init__.py: -------------------------------------------------------------------------------- 1 | from .game import Game 2 | -------------------------------------------------------------------------------- /pong/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithtim/NEAT-Pong-Python/52edc429608f43206770390da73b5237407b7b19/pong/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /pong/__pycache__/ball.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithtim/NEAT-Pong-Python/52edc429608f43206770390da73b5237407b7b19/pong/__pycache__/ball.cpython-38.pyc -------------------------------------------------------------------------------- /pong/__pycache__/game.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithtim/NEAT-Pong-Python/52edc429608f43206770390da73b5237407b7b19/pong/__pycache__/game.cpython-38.pyc -------------------------------------------------------------------------------- /pong/__pycache__/paddle.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithtim/NEAT-Pong-Python/52edc429608f43206770390da73b5237407b7b19/pong/__pycache__/paddle.cpython-38.pyc -------------------------------------------------------------------------------- /pong/ball.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import math 3 | import random 4 | 5 | 6 | class Ball: 7 | MAX_VEL = 5 8 | RADIUS = 7 9 | 10 | def __init__(self, x, y): 11 | self.x = self.original_x = x 12 | self.y = self.original_y = y 13 | 14 | angle = self._get_random_angle(-30, 30, [0]) 15 | pos = 1 if random.random() < 0.5 else -1 16 | 17 | self.x_vel = pos * abs(math.cos(angle) * self.MAX_VEL) 18 | self.y_vel = math.sin(angle) * self.MAX_VEL 19 | 20 | def _get_random_angle(self, min_angle, max_angle, excluded): 21 | angle = 0 22 | while angle in excluded: 23 | angle = math.radians(random.randrange(min_angle, max_angle)) 24 | 25 | return angle 26 | 27 | def draw(self, win): 28 | pygame.draw.circle(win, (255, 255, 255), (self.x, self.y), self.RADIUS) 29 | 30 | def move(self): 31 | self.x += self.x_vel 32 | self.y += self.y_vel 33 | 34 | def reset(self): 35 | self.x = self.original_x 36 | self.y = self.original_y 37 | 38 | angle = self._get_random_angle(-30, 30, [0]) 39 | x_vel = abs(math.cos(angle) * self.MAX_VEL) 40 | y_vel = math.sin(angle) * self.MAX_VEL 41 | 42 | self.y_vel = y_vel 43 | self.x_vel *= -1 44 | -------------------------------------------------------------------------------- /pong/game.py: -------------------------------------------------------------------------------- 1 | from .paddle import Paddle 2 | from .ball import Ball 3 | import pygame 4 | import random 5 | pygame.init() 6 | 7 | 8 | class GameInformation: 9 | def __init__(self, left_hits, right_hits, left_score, right_score): 10 | self.left_hits = left_hits 11 | self.right_hits = right_hits 12 | self.left_score = left_score 13 | self.right_score = right_score 14 | 15 | 16 | class Game: 17 | """ 18 | To use this class simply initialize and instance and call the .loop() method 19 | inside of a pygame event loop (i.e while loop). Inside of your event loop 20 | you can call the .draw() and .move_paddle() methods according to your use case. 21 | Use the information returned from .loop() to determine when to end the game by calling 22 | .reset(). 23 | """ 24 | SCORE_FONT = pygame.font.SysFont("comicsans", 50) 25 | WHITE = (255, 255, 255) 26 | BLACK = (0, 0, 0) 27 | RED = (255, 0, 0) 28 | 29 | def __init__(self, window, window_width, window_height): 30 | self.window_width = window_width 31 | self.window_height = window_height 32 | 33 | self.left_paddle = Paddle( 34 | 10, self.window_height // 2 - Paddle.HEIGHT // 2) 35 | self.right_paddle = Paddle( 36 | self.window_width - 10 - Paddle.WIDTH, self.window_height // 2 - Paddle.HEIGHT//2) 37 | self.ball = Ball(self.window_width // 2, self.window_height // 2) 38 | 39 | self.left_score = 0 40 | self.right_score = 0 41 | self.left_hits = 0 42 | self.right_hits = 0 43 | self.window = window 44 | 45 | def _draw_score(self): 46 | left_score_text = self.SCORE_FONT.render( 47 | f"{self.left_score}", 1, self.WHITE) 48 | right_score_text = self.SCORE_FONT.render( 49 | f"{self.right_score}", 1, self.WHITE) 50 | self.window.blit(left_score_text, (self.window_width // 51 | 4 - left_score_text.get_width()//2, 20)) 52 | self.window.blit(right_score_text, (self.window_width * (3/4) - 53 | right_score_text.get_width()//2, 20)) 54 | 55 | def _draw_hits(self): 56 | hits_text = self.SCORE_FONT.render( 57 | f"{self.left_hits + self.right_hits}", 1, self.RED) 58 | self.window.blit(hits_text, (self.window_width // 59 | 2 - hits_text.get_width()//2, 10)) 60 | 61 | def _draw_divider(self): 62 | for i in range(10, self.window_height, self.window_height//20): 63 | if i % 2 == 1: 64 | continue 65 | pygame.draw.rect( 66 | self.window, self.WHITE, (self.window_width//2 - 5, i, 10, self.window_height//20)) 67 | 68 | def _handle_collision(self): 69 | ball = self.ball 70 | left_paddle = self.left_paddle 71 | right_paddle = self.right_paddle 72 | 73 | if ball.y + ball.RADIUS >= self.window_height: 74 | ball.y_vel *= -1 75 | elif ball.y - ball.RADIUS <= 0: 76 | ball.y_vel *= -1 77 | 78 | if ball.x_vel < 0: 79 | if ball.y >= left_paddle.y and ball.y <= left_paddle.y + Paddle.HEIGHT: 80 | if ball.x - ball.RADIUS <= left_paddle.x + Paddle.WIDTH: 81 | ball.x_vel *= -1 82 | 83 | middle_y = left_paddle.y + Paddle.HEIGHT / 2 84 | difference_in_y = middle_y - ball.y 85 | reduction_factor = (Paddle.HEIGHT / 2) / ball.MAX_VEL 86 | y_vel = difference_in_y / reduction_factor 87 | ball.y_vel = -1 * y_vel 88 | self.left_hits += 1 89 | 90 | else: 91 | if ball.y >= right_paddle.y and ball.y <= right_paddle.y + Paddle.HEIGHT: 92 | if ball.x + ball.RADIUS >= right_paddle.x: 93 | ball.x_vel *= -1 94 | 95 | middle_y = right_paddle.y + Paddle.HEIGHT / 2 96 | difference_in_y = middle_y - ball.y 97 | reduction_factor = (Paddle.HEIGHT / 2) / ball.MAX_VEL 98 | y_vel = difference_in_y / reduction_factor 99 | ball.y_vel = -1 * y_vel 100 | self.right_hits += 1 101 | 102 | def draw(self, draw_score=True, draw_hits=False): 103 | self.window.fill(self.BLACK) 104 | 105 | self._draw_divider() 106 | 107 | if draw_score: 108 | self._draw_score() 109 | 110 | if draw_hits: 111 | self._draw_hits() 112 | 113 | for paddle in [self.left_paddle, self.right_paddle]: 114 | paddle.draw(self.window) 115 | 116 | self.ball.draw(self.window) 117 | 118 | def move_paddle(self, left=True, up=True): 119 | """ 120 | Move the left or right paddle. 121 | 122 | :returns: boolean indicating if paddle movement is valid. 123 | Movement is invalid if it causes paddle to go 124 | off the screen 125 | """ 126 | if left: 127 | if up and self.left_paddle.y - Paddle.VEL < 0: 128 | return False 129 | if not up and self.left_paddle.y + Paddle.HEIGHT > self.window_height: 130 | return False 131 | self.left_paddle.move(up) 132 | else: 133 | if up and self.right_paddle.y - Paddle.VEL < 0: 134 | return False 135 | if not up and self.right_paddle.y + Paddle.HEIGHT > self.window_height: 136 | return False 137 | self.right_paddle.move(up) 138 | 139 | return True 140 | 141 | def loop(self): 142 | """ 143 | Executes a single game loop. 144 | 145 | :returns: GameInformation instance stating score 146 | and hits of each paddle. 147 | """ 148 | self.ball.move() 149 | self._handle_collision() 150 | 151 | if self.ball.x < 0: 152 | self.ball.reset() 153 | self.right_score += 1 154 | elif self.ball.x > self.window_width: 155 | self.ball.reset() 156 | self.left_score += 1 157 | 158 | game_info = GameInformation( 159 | self.left_hits, self.right_hits, self.left_score, self.right_score) 160 | 161 | return game_info 162 | 163 | def reset(self): 164 | """Resets the entire game.""" 165 | self.ball.reset() 166 | self.left_paddle.reset() 167 | self.right_paddle.reset() 168 | self.left_score = 0 169 | self.right_score = 0 170 | self.left_hits = 0 171 | self.right_hits = 0 172 | -------------------------------------------------------------------------------- /pong/paddle.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | class Paddle: 5 | VEL = 4 6 | WIDTH = 20 7 | HEIGHT = 100 8 | 9 | def __init__(self, x, y): 10 | self.x = self.original_x = x 11 | self.y = self.original_y = y 12 | 13 | def draw(self, win): 14 | pygame.draw.rect( 15 | win, (255, 255, 255), (self.x, self.y, self.WIDTH, self.HEIGHT)) 16 | 17 | def move(self, up=True): 18 | if up: 19 | self.y -= self.VEL 20 | else: 21 | self.y += self.VEL 22 | 23 | def reset(self): 24 | self.x = self.original_x 25 | self.y = self.original_y 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | neat-python 2 | pygame 3 | -------------------------------------------------------------------------------- /tutorial.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pong import Game 3 | import neat 4 | import os 5 | import pickle 6 | 7 | 8 | class PongGame: 9 | def __init__(self, window, width, height): 10 | self.game = Game(window, width, height) 11 | self.left_paddle = self.game.left_paddle 12 | self.right_paddle = self.game.right_paddle 13 | self.ball = self.game.ball 14 | 15 | def test_ai(self, genome, config): 16 | net = neat.nn.FeedForwardNetwork.create(genome, config) 17 | 18 | run = True 19 | clock = pygame.time.Clock() 20 | while run: 21 | clock.tick(60) 22 | for event in pygame.event.get(): 23 | if event.type == pygame.QUIT: 24 | run = False 25 | break 26 | 27 | keys = pygame.key.get_pressed() 28 | if keys[pygame.K_w]: 29 | self.game.move_paddle(left=True, up=True) 30 | if keys[pygame.K_s]: 31 | self.game.move_paddle(left=True, up=False) 32 | 33 | output = net.activate( 34 | (self.right_paddle.y, self.ball.y, abs(self.right_paddle.x - self.ball.x))) 35 | decision = output.index(max(output)) 36 | 37 | if decision == 0: 38 | pass 39 | elif decision == 1: 40 | self.game.move_paddle(left=False, up=True) 41 | else: 42 | self.game.move_paddle(left=False, up=False) 43 | 44 | game_info = self.game.loop() 45 | self.game.draw(True, False) 46 | pygame.display.update() 47 | 48 | pygame.quit() 49 | 50 | def train_ai(self, genome1, genome2, config): 51 | net1 = neat.nn.FeedForwardNetwork.create(genome1, config) 52 | net2 = neat.nn.FeedForwardNetwork.create(genome2, config) 53 | 54 | run = True 55 | while run: 56 | for event in pygame.event.get(): 57 | if event.type == pygame.QUIT: 58 | quit() 59 | 60 | output1 = net1.activate( 61 | (self.left_paddle.y, self.ball.y, abs(self.left_paddle.x - self.ball.x))) 62 | decision1 = output1.index(max(output1)) 63 | 64 | if decision1 == 0: 65 | pass 66 | elif decision1 == 1: 67 | self.game.move_paddle(left=True, up=True) 68 | else: 69 | self.game.move_paddle(left=True, up=False) 70 | 71 | output2 = net2.activate( 72 | (self.right_paddle.y, self.ball.y, abs(self.right_paddle.x - self.ball.x))) 73 | decision2 = output2.index(max(output2)) 74 | 75 | if decision2 == 0: 76 | pass 77 | elif decision2 == 1: 78 | self.game.move_paddle(left=False, up=True) 79 | else: 80 | self.game.move_paddle(left=False, up=False) 81 | 82 | game_info = game.loop() 83 | 84 | game.draw(draw_score=False, draw_hits=True) 85 | pygame.display.update() 86 | 87 | if game_info.left_score >= 1 or game_info.right_score >= 1 or game_info.left_hits > 50: 88 | self.calculate_fitness(genome1, genome2, game_info) 89 | break 90 | 91 | def calculate_fitness(self, genome1, genome2, game_info): 92 | genome1.fitness += game_info.left_hits 93 | genome2.fitness += game_info.right_hits 94 | 95 | 96 | def eval_genomes(genomes, config): 97 | width, height = 700, 500 98 | window = pygame.display.set_mode((width, height)) 99 | 100 | for i, (genome_id1, genome1) in enumerate(genomes): 101 | if i == len(genomes) - 1: 102 | break 103 | genome1.fitness = 0 104 | for genome_id2, genome2 in genomes[i+1:]: 105 | genome2.fitness = 0 if genome2.fitness == None else genome2.fitness 106 | game = PongGame(window, width, height) 107 | game.train_ai(genome1, genome2, config) 108 | 109 | 110 | def run_neat(config): 111 | p = neat.Checkpointer.restore_checkpoint('neat-checkpoint-7') 112 | #p = neat.Population(config) 113 | p.add_reporter(neat.StdOutReporter(True)) 114 | stats = neat.StatisticsReporter() 115 | p.add_reporter(stats) 116 | p.add_reporter(neat.Checkpointer(1)) 117 | 118 | winner = p.run(eval_genomes, 1) 119 | with open("best.pickle", "wb") as f: 120 | pickle.dump(winner, f) 121 | 122 | 123 | def test_ai(config): 124 | width, height = 700, 500 125 | window = pygame.display.set_mode((width, height)) 126 | 127 | with open("best.pickle", "rb") as f: 128 | winner = pickle.load(f) 129 | 130 | game = PongGame(window, width, height) 131 | game.test_ai(winner, config) 132 | 133 | 134 | if __name__ == "__main__": 135 | local_dir = os.path.dirname(__file__) 136 | config_path = os.path.join(local_dir, "config.txt") 137 | 138 | config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction, 139 | neat.DefaultSpeciesSet, neat.DefaultStagnation, 140 | config_path) 141 | # run_neat(config) 142 | test_ai(config) 143 | --------------------------------------------------------------------------------