diff --git a/__pycache__/gen_algo.cpython-38.pyc b/__pycache__/gen_algo.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..405cda6c515797b0d78076361d961454e2fab437 Binary files /dev/null and b/__pycache__/gen_algo.cpython-38.pyc differ diff --git a/__pycache__/graph.cpython-38.pyc b/__pycache__/graph.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a852878ebe6729f05b9e42ed9f5e00e27a15367 Binary files /dev/null and b/__pycache__/graph.cpython-38.pyc differ diff --git a/gen_algo.py b/gen_algo.py new file mode 100644 index 0000000000000000000000000000000000000000..5aba5f06ad5ca2fc675cec194302d9bb0d9a09ef --- /dev/null +++ b/gen_algo.py @@ -0,0 +1,325 @@ +import numpy as np +import random + + +def manh_dist(pos1, pos2): + return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1]) + + +class Game(object): + # nothing : 0 + # wall : 1 + # entrance : 2 + # exit : 3 + + def __init__(self, shape=(3, 7)): + + self.dist_factor = 5 + self.exploration_factor = 6 + + self.wall_penality = 2 + self.entrance_penality = 50 + self.exit_reward = 100 + + self.entrance = (1 + 2 * random.randint(0, shape[0] - 1), 0) + self.exit = (1 + 2 * random.randint(0, shape[0] - 1), shape[1] * 2) + + self.maze = np.zeros((shape[0] * 2 + 1, shape[1] * 2 + 1)) + + # recursive generation with queue + + # delimitations : + + self.maze[0] = 1 + self.maze[-1] = 1 + self.maze[:, 0] = 1 + self.maze[:, -1] = 1 + + # generation : + + def recursive_maze(maze): + if maze.shape[0] <= 1 and maze.shape[1] <= 1: + return [] + + if maze.shape[0] < maze.shape[1]: + verti = ( + 2 * random.randint(0, maze.shape[1] // 2 - 1) + 1 + ) # odd between 0 and shape[1]-1 + hori = 2 * random.randint(0, maze.shape[0] // 2) + + maze[:, verti] = 1 + maze[hori, verti] = 0 + + return [maze[:, :verti], maze[:, verti + 1 :]] + else: + hori = 2 * random.randint(0, maze.shape[0] // 2 - 1) + 1 + verti = 2 * random.randint(0, maze.shape[1] // 2) + maze[hori] = 1 + maze[hori, verti] = 0 + return [maze[:hori, :], maze[hori + 1 :, :]] + + queue = [self.maze[1:-1, 1:-1]] + while len(queue) != 0: + sub = queue.pop(0) + temp = recursive_maze(sub) + for i in temp: + queue.append(i) + + self.maze[self.entrance] = 2 + self.maze[self.exit] = 3 + + def play(self, player, record=False): + """ + argument : + - player, a Sample class object + return : + - list with score and end position + """ + position = self.entrance + score = 0 + + memo = [position] + for i in player.genome: + + if i == 0: + temp = (position[0], position[1] + 1) + if i == 1: + temp = (position[0] + 1, position[1]) + if i == 2: + temp = (position[0], position[1] - 1) + if i == 3: + temp = (position[0] - 1, position[1]) + if i == 4: + temp = (position[0], position[1]) + + if temp[1] >= 0 and temp[1] < self.maze.shape[1]: + # this can occur at the entrance and at the exit + if self.maze[temp] == 1: + score -= self.wall_penality + if self.maze[temp] == 0: + position = temp + if self.maze[temp] == 2: + position = temp + score -= self.entrance_penality + if self.maze[temp] == 3: + # this makes it go for exit faster and staying there + score += self.exit_reward + position = temp + + memo.append(position) + else: + score -= self.wall_penality + + ### push the exploration : + score += self.exploration_factor * len(set(memo)) + + ### push the way toward the exit : + score -= self.dist_factor * manh_dist(position, self.exit) + if record: + return score, position, memo + return score, position + + @property + def entrance(self): + return self._entrance + + @entrance.setter + def entrance(self, new): + self._entrance = new + + @property + def exit(self): + return self._exit + + @exit.setter + def exit(self, new): + self._exit = new + + @property + def maze(self): + return self._maze + + @maze.setter + def maze(self, new): + self._maze = new + + def print_maze(self): + print(self.maze) + + +class Sample(object): + """ + genome : list of integers between 0 and 4 + 0 : up + 1 : right + 2 : down + 3 : left + 4 : stay still -> this is usefull to lessen the number + of moves without changing the length of the list + + Some tweaks were done to adapt to the particular genome + """ + + def __init__( + self, creation="random", max_length=30, genome=[], parent1=None, parent2=None, + ): + """ + creation = "random" : random, be carefull to set the length + "cross over" : do a cross over between 2 samples + be carefull to set parent1 and parent2 + """ + self.score = None + self.end_position = None + if creation == "random": + self.genome = [random.randint(0, 4) for l in range(max_length)] + elif creation == "genome": + self.genome = genome + elif creation == "cross over": + if parent1 is None or parent2 is None: + raise NameError("Parents are not defined") + # we want to keep the beginning of the best parent + if parent1.score > parent2.score: + begin = parent1 + end = parent2 + else: + begin = parent2 + end = parent1 + cross_point = random.randint(0, len(parent1.genome)) + self.genome = begin.genome[:cross_point] + end.genome[cross_point:] + + else: + raise NameError("Mode of creation not defined") + + def mutate(self): + """ + That mutation tries to favour straight lines to get out of local minimum + """ + x1 = random.randint(0, len(self.genome)) + x2 = random.randint(0, (len(self.genome) - x1)) + + for k in range(x1, x1 + x2): + temp = random.randint(0, 6) + if temp < 5: + self.genome[k] = temp + else: + if k > 0: + self.genome[k] = self.genome[k - 1] + + @property + def genome(self): + return self._genome + + @genome.setter + def genome(self, new): + self._genome = new + + @property + def score(self): + if self._score is None: + raise NameError("Score is None") + return self._score + + @score.setter + def score(self, new): + self._score = new + + @property + def end_position(self): + if self._end_position is None: + raise NameError("End position is None") + return self._end_position + + @end_position.setter + def end_position(self, new): + self._end_position = new + + +class GA(object): + """ + Attributes : + pop_card : cardinal of population + pop : list of people which compose the population + elite_card : cardinal of the elite + the elite corresponds to pop[:elite_card] + death_card : cardinal of the deaths in each generation + they correspond to pop[mortality_card:] + mutation_rate + max_length + + This class tries to be as general as it can to solve a game + The only thing you have to change in it is the maxlength parameter + it is only used during the initialisation + + """ + + def __init__( + self, + game, + genome_length=None, + pop_card=5000, + elite=0.01, + mortality=0.4, + mutation_rate=0.2, + ): + self.game = game + self.mutation_rate = mutation_rate + self.pop_card = pop_card + self.elite_card = int(elite * pop_card) + self.death_card = int(mortality * pop_card) + self.pop = [ + Sample(creation="random", max_length=genome_length) for _ in range(pop_card) + ] + for sample in self.pop: + temp = game.play(sample) + sample.score = temp[0] + sample.end_position = temp[1] + self.pop.sort(key=lambda sample: sample.score, reverse=True) + + def cross_over_step(self, number): + """ + self.pop[number] is replaced by a new sample obtained by a cross over + between 2 random samples + """ + parent1 = random.randint(0, self.pop_card - 1) + + parent2 = random.randint(0, self.pop_card - 1) + self.pop[number] = Sample( + creation="cross over", parent1=self.pop[parent1], parent2=self.pop[parent2], + ) + temp = self.game.play(self.pop[number]) + self.pop[number].score = temp[0] + self.pop[number].end_position = temp[1] + + def cross_over(self): + """ + the elite won't be changed at all + the worst pop will get replaced by offspring + we allow new offspring to reproduce at the moment they are created + in order not to complexify too much the implementation + to do this, we have to calculate the score at the same time because + it is useful to do the cross over + + """ + for k in range(self.pop_card - self.death_card, self.pop_card): + self.cross_over_step(k) + + def mutation_step(self, number): + """ + mutation of self.pop[number] + """ + if random.random() < self.mutation_rate: + self.pop[number].mutate() + temp = self.game.play(self.pop[number]) + self.pop[number].score = temp[0] + self.pop[number].end_position = temp[1] + + def mutation(self): + """we will mutate all the population that is not the elite""" + for k in range(self.elite_card, self.pop_card): + self.mutation_step(k) + + def do_gen(self): + self.cross_over() + self.mutation() + self.pop.sort(key=lambda sample: sample.score, reverse=True) + diff --git a/graph.py b/graph.py new file mode 100644 index 0000000000000000000000000000000000000000..fecfbc35cd3a98c8a5b2e5f4d46f242035108631 --- /dev/null +++ b/graph.py @@ -0,0 +1,59 @@ +import pygame +from gen_algo import * + + +########### What to show each generation + +### four ways to show : +# 1) only the final position of all the elite +# 2) the path taken by the best one with arrows to make it clear +# 3) animation of the path taken by the best one +# 4) leaderboard with 10 elites on another window + +# I implemented #2 + + +def draw_maze(window, maze, square_size, maze_color): + + for i in range(maze.shape[0]): + for j in range(maze.shape[1]): + if maze[i, j] == 1: + pygame.draw.rect( + window, + maze_color, + (j * square_size, i * square_size, square_size, square_size), + ) + pygame.display.update() + + +def clear_maze(window, maze, square_size, back_ground_color): + for i in range(maze.shape[0]): + for j in range(maze.shape[1]): + if maze[i, j] != 1: + pygame.draw.rect( + window, + back_ground_color, + (j * square_size, i * square_size, square_size, square_size), + ) + pygame.display.update() + + +def show_path(window, game, sample, square_size, show_path_color): + memo = game.play(sample, record=True)[2] + for pos in memo: + pygame.draw.rect( + window, + show_path_color, + (pos[1] * square_size, pos[0] * square_size, square_size, square_size), + ) + pygame.display.update() + + +def show_gen(gen, algo, game, time=None): + print("--------------------------------------------") + print("benchmark of generation", gen) + if time is not None: + print("time (s) :", time) + print("best score :", algo.pop[0].score) + print("Manhattan distance :", manh_dist(algo.pop[0].end_position, game.exit)) + print("--------------------------------------------") diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..5aef399df4166997ec6b3c719b5665f1c1a1f280 --- /dev/null +++ b/main.py @@ -0,0 +1,80 @@ +import pygame +from pygame.locals import * +from gen_algo import * +from graph import * +import time + +###### Parameters + +#### Graphism +window_length = 640 +window_width = 480 +maze_color = (255, 0, 0) +backgroud_color = (255, 255, 255) +entrance_color = (0, 255, 0) +exit_color = (0, 0, 255) + +show_path_color = (160, 225, 55) + +#### Back +shape = (10, 10) +number_of_generations = 1000 +pop_card = 3000 +elite = 0.01 +mortality = 0.4 +mutation_rate = 0.4 +max_moves = 100 + + +################### Inititialisation of pygame +pygame.init() + + +# Init game +t0 = time.time() + +game = Game(shape) +algo = GA( + game, + genome_length=max_moves, + pop_card=pop_card, + elite=elite, + mortality=mortality, + mutation_rate=mutation_rate, +) +square_size = min( + window_length // game.maze.shape[1], window_width // game.maze.shape[0] +) + +window = pygame.display.set_mode( + (square_size * game.maze.shape[1], square_size * game.maze.shape[0]) +) + +window.fill(backgroud_color) + + +draw_maze(window, game.maze, square_size, maze_color) +game.print_maze() + + +print("Initialisation took ", time.time() - t0) +t0 = time.time() + + +show_gen(0, algo, game) +show_path(window, game, algo.pop[0], square_size, show_path_color) + +for gen in range(1, number_of_generations): + print("starting generation", gen) + algo.do_gen() + clear_maze(window, game.maze, square_size, backgroud_color) + show_path(window, game, algo.pop[0], square_size, show_path_color) + show_gen(gen, algo, game, time=time.time() - t0) + t0 = time.time() + + +flag = 1 +while flag: + for event in pygame.event.pump(): + if event.type == QUIT: + flag = 0 diff --git a/main_seq.py b/main_seq.py new file mode 100644 index 0000000000000000000000000000000000000000..ad07dc4468a81d64c7f2bb84e7eaab3b63081fc2 --- /dev/null +++ b/main_seq.py @@ -0,0 +1,106 @@ +import pygame +from pygame.locals import * +from gen_algo import * +from graph import * +import time + +###### Parameters + +#### Graphism +window_length = 640 +window_width = 480 +maze_color = (255, 0, 0) +backgroud_color = (255, 255, 255) +entrance_color = (0, 255, 0) +exit_color = (0, 0, 255) + +show_path_color = (160, 225, 55) + +#### Back +shape = (10, 20) +number_of_generations = 1000 +pop_card = 3000 +elite = 0.01 +mortality = 0.4 +mutation_rate = 0.4 +max_moves = 300 + + +################### Inititialisation of pygame +pygame.init() + + +# Init game +t0 = time.time() + +game = Game(shape) +algo = GA( + game, + genome_length=max_moves, + pop_card=pop_card, + elite=elite, + mortality=mortality, + mutation_rate=mutation_rate, +) +square_size = min( + window_length // game.maze.shape[1], window_width // game.maze.shape[0] +) + +window = pygame.display.set_mode( + (square_size * game.maze.shape[1], square_size * game.maze.shape[0]) +) + +window.fill(backgroud_color) + + +draw_maze(window, game.maze, square_size, maze_color) +game.print_maze() + +print("Initialisation took ", time.time() - t0) +t0 = time.time() +show_gen(0, algo, game) +show_path(window, game, algo.pop[0], square_size, show_path_color) + +flag = 1 +state = ( + "cross_over" # this variable is either equal to "cross_over", "mutation" or "sort" +) +count_co = algo.pop_card - algo.death_card +count_mutate = algo.elite_card +gen = 1 + + +while flag: + for event in pygame.event.get(): + if event.type == QUIT: + flag = 0 + + if state == "cross_over": + if count_co == 0: + print("starting generation", gen) + if count_co == algo.pop_card: + print("Cross over done") + count_co = algo.pop_card - algo.death_card + state = "mutation" + else: + algo.cross_over_step(count_co) + count_co += 1 + + elif state == "mutation": + if count_mutate == algo.pop_card: + print("Mutation done") + count_mutate = algo.elite_card + state = "sort" + else: + algo.mutation_step(count_mutate) + count_mutate += 1 + + elif state == "sort": + state = "cross_over" + algo.pop.sort(key=lambda sample: sample.score, reverse=True) + gen += 1 + clear_maze(window, game.maze, square_size, backgroud_color) + show_path(window, game, algo.pop[0], square_size, show_path_color) + show_gen(gen, algo, game, time=time.time() - t0) + t0 = time.time() +