diff --git a/gaTraveling.md b/gaTraveling.md index 933809a..ef32e4d 100644 --- a/gaTraveling.md +++ b/gaTraveling.md @@ -1,2 +1,131 @@ # Wyznaczanie trasy algorytmem genetycznym -**Autor:** Mateusz Tylka +**Autor:** *Mateusz Tylka* + +## Cel algorytmu +Celem tego algorytmu jest wyznaczenie optymalnej trasy w zbieraniu ziół o konkretnych pozycjach, które +są generowane losowo. Algorytm decyduje po które zioło udać się najpierw, starając się, aby końcowa suma odległości +pomiędzy odwiedzonymi pozycjami była jak najmniejsza. + +## Osobnik Traveling +Osobnik jest to jednostka polegająca ewolucji za pomocą operacji genetycznych. +W mojej implementacji osobnika reprezentuje obiekt [Traveling.py](). Ten obiekt przechowuje następujące metody: + +```python +class Traveling: + def __init__(self, coords): + self.coords = coords + self.fitness = self.evaluate() +``` +* W konstruktorze przyjmowany jako parametr jest zestaw koordynatów, który zostaje zapisany jako atrybut, +następnie tworzymy atrybut reprezentujący sprawność danego osobnika, który jest wynikiem metody określającej +poprawność danej trasy. + +```python +def evaluate(self): + sum = 0 + for i, c in enumerate(self.coords): + if i + 1 > len(self.coords) - 1: + break + nextCoord = self.coords[i + 1] + # calculate distance + sum += sqrt((nextCoord[0] - c[0]) ** 2 + (nextCoord[1] - c[1]) ** 2) + return sum +``` +* Metoda **evaluate** odpowiedzialna jest za ocenę osobnika. Liczymy w niej odległość od punktu startu do +pierwszego punktu, następnie odległość między drugim a trzecim miejscem i tak dalej..., aż do końca listy pozycji +ziół z rozważanej trasy. Uzyskane wyniki sumujemy, czyli uzyskujemy długość konretnej drogi. + +```python +def crossover(self, parentCoords): + childCoords = self.coords[1:int(len(self.coords) / 2)] + for coord in parentCoords.coords: + if coord not in childCoords and coord not in END_COORD + START_COORD: + childCoords.append(coord) + + if len(childCoords) == len(parentCoords.coords): + break + return Traveling(START_COORD + childCoords + END_COORD) +``` +* Metoda **crossover** reprezentuję operację genetyczną krzyżowania osobników. Bierzemy w niej z pierwszego osobnika +część punktów jego trasy (w naszym przypadku połowę) i dobieramy w pętli kolejne koordynaty z drugiego osobnika +tak, aby się one nie powtarzały. Gdy już osiągniemy odpowiednią długość nowego osobnika kończymy pętlę i zwracamy go. + +```python +def mutation(self): + first = randint(1, len(self.coords) - 2) + second = randint(1, len(self.coords) - 2) + self.coords[first], self.coords[second] = self.coords[second], self.coords[first] + self.fitness = self.evaluate() +``` +* Ta metoda przedstawia proces mutacji. Polega on po prostu na zamianę miejscami dwóch losowych koordynatów +na trasie. + +```python +def __repr__(self): + return str(self.coords) +``` +* Obiekt ten zwracany jest w formie tekstowej listy koordynatów. + +## Obiekt GeneticAlgorithm +W pliku [GeneticAlgorithm.py]() znajduje się model selekcji osobników, warunek stopu, oraz główna pętla +algorytmu. + +```python +class GeneticAlgorithm: + def __init__(self, firstPopulation, mutationProbability): + self.firstPopulation = firstPopulation + self.mutationProbability = mutationProbability +``` +* Obiekt ten przyjmuje pierwszą populację oraz prawdopodobieństwo mutacji jako parametry i zapisuje je +w odpowiednich atrybutach. + +```python +def selectionModel(self, generation): + max_selected = int(len(generation) / 10) + sorted_by_assess = sorted(generation, key=lambda x: x.fitness) + return sorted_by_assess[:max_selected] +``` + +* Model w mojej implementacji opiera się na elitaryzmie - czyli wybraniu pewnej ilości najlepszych chromosomów, +które z pewnością przetrwają do następnej generacji. Definiujemy w niej 10% spośród przyjętej generacji jako parametr. +Sortujemy naszą generację według odległości (metody *evaluate*) czyli wartości atrybutu **fitness**. + +```python +def stopCondition(self, i): + return i == 64 +``` + +* Warunkiem końca algorytmu jest osiągnięcie 64 generacji. + +```python +def run(self): + population = self.firstPopulation + population.sort(key=lambda x: x.fitness) + populationLen = len(population) + i = 0 + while True: + selected = self.selectionModel(population) + newPopulation = selected.copy() + while len(newPopulation) != populationLen: + child = choice(population).crossover(choice(population)) + if random() <= self.mutationProbability: + child.mutation() + newPopulation.append(child) + + population = newPopulation + theBestMatch = min(population, key=lambda x: x.fitness) + print("Generation: {} S: {} fitness: {}".format(i+1, theBestMatch, theBestMatch.fitness)) + + i += 1 + if self.stopCondition(i): + return str(theBestMatch) +``` +* W metodzie **run** zaimplementowana jest główna pętla algorytmu +genetycznego. Na początku wskazujemy pierwszą populację i sortujemy ją według dopasowania **fitness**, +a następnie obliczamy długość populacji i deklarujemy iterator pętli, która przebiega w następujących krokach; + * Wybieramy najlepszych osobników według modelu selekcji (metody **selectionModel**) + * Tworzymy nową populację z najlepszych wybranych osobników, jednak do pełnej populacji brakuje nam kilku chromosomów + * Dopełniamy do pełnej liczności populacji, poprzez operację krzyżowania (metoda **crossover**), oraz + ewentualną mutację (metodą **mutation**). + * Wybieramy najlepszego osobnika z populacji po minimalnej odległości, oraz wyświetlamy wynik. + * Przeprowadzamy w ten sposób kolejną generację dopóki nie będzie ich 64. \ No newline at end of file diff --git a/src/entities/Interactable.py b/src/entities/Interactable.py index 688a242..518ef0e 100644 --- a/src/entities/Interactable.py +++ b/src/entities/Interactable.py @@ -32,6 +32,7 @@ class Interactable(Entity): def on_interaction(self, player): """ Applies outcome to the Player + Add some ifs to handle collect herbs (traveling ga algorithm) :param Player: Player object """ @@ -46,6 +47,5 @@ class Interactable(Entity): if player.herbs == 10 and self.classifier == Classifiers.REST: player.readyToCrafting = True - def __str__(self): return "Entity - ID:{}, pos:({}x, {}y), {}".format(self.id, self.x, self.y, self.classifier.name) diff --git a/src/entities/Player.py b/src/entities/Player.py index 1ab3f28..eb67e37 100644 --- a/src/entities/Player.py +++ b/src/entities/Player.py @@ -22,8 +22,8 @@ class Player(Entity): super().__init__("player.png", size, spawnpoint, False) self.statistics = Statistics(100, 0, 0, 100) - self.herbs = 0 - self.readyToCrafting = False + self.herbs = 0 # Need to collect herbs (traveling ga algorithm) + self.readyToCrafting = False # Need to reset statistics (traveling ga algorithm) # How many steps has the player taken through its lifetime self.movePoints = 0 # Tracks how much time has passed since the player is alive @@ -52,7 +52,7 @@ class Player(Entity): def applyWalkingFatigue(self): """ Lowers player's statistics. Applied every few steps. - + Modify to not die until collect all herbs. """ # looses hunger # self.statistics.set_hunger(1.7) diff --git a/src/game/EventManager.py b/src/game/EventManager.py index a1aa0be..2c89edb 100644 --- a/src/game/EventManager.py +++ b/src/game/EventManager.py @@ -116,17 +116,17 @@ class EventManager: target = pickEntity(self.player, self.game.map) self.player.gotoToTarget(target, self.game.map) - if keys[pygame.K_t]: + if keys[pygame.K_t]: # Handle traveling movement to collect herbs if self.player.movementTarget is None and self.iterator <= 10: target = self.game.entityToVisitList[self.iterator] self.player.gotoToTarget(target, self.game.map) self.iterator += 1 - if self.player.herbs > self.takenHerbs: + if self.player.herbs > self.takenHerbs: # Console log when player collect herb self.game.screen.ui.console.printToConsole("Ziele zebrane! Ilość: " + str(self.player.herbs)) self.takenHerbs = self.player.herbs - if self.player.readyToCrafting: + if self.player.readyToCrafting: # Console log and reset statistics because of collect all herbs self.game.screen.ui.console.printToConsole("Eliksir został utworzony i spożyty!") self.player.statistics.set_hp(100) self.player.statistics.set_stamina(100) diff --git a/src/game/Game.py b/src/game/Game.py index c5c6758..f3e84ec 100644 --- a/src/game/Game.py +++ b/src/game/Game.py @@ -101,6 +101,7 @@ class Game: filesPath) + os.sep + "data" + os.sep + "AI_data" + os.sep + "dt_exmpls" + os.sep + "dt_examples" dtExampleManager = ExamplesManager(examplesFilePath) dtExampleManager.generateExamples() + # Traveling ga algorithm elif argv[1] == "ga_travel": self.travelRun(filesPath) # Invalid game mode @@ -394,7 +395,7 @@ class Game: avg = sum(scores) / iterations print("Average: {}".format(str(avg))) - def travelRun(self, filesPath): + def travelRun(self, filesPath): # Run game with traveling ga algorithm self.running = True print("Initializing screen, params: " + str(self.config["window"]) + "...", end=" ") @@ -420,10 +421,12 @@ class Game: self.map.addEntity(self.player, DONTADD=True) self.eventManager = EventManager(self, self.player) + # Generate random travel list self.travelCoords = random.sample(self.map.movableList(), 10) import ast self.travelCoords = ast.literal_eval(str(self.travelCoords)) + # Insert herbs on random travel coordinates self.map.insertHerbs(self.travelCoords) # Initialize genetic algorithm @@ -433,12 +436,15 @@ class Game: ga = GeneticAlgorithm(firstGeneration, mutationProbability) self.movementList = ga.listOfTravel() + # Define list of entities which player should pass to collect herbs self.entityToVisitList = [] for i in self.movementList: self.entityToVisitList.append(self.map.getEntityOnCoord(i)) - self.screen.ui.console.printToConsole("First generation: " + str(firstGeneration[0])) + # Remove first element, because start coordinates is None self.entityToVisitList.remove(self.entityToVisitList[0]) + + self.screen.ui.console.printToConsole("First generation: " + str(firstGeneration[0])) self.screen.ui.console.printToConsole("The best generation: " + str(self.entityToVisitList)) self.mainLoop() diff --git a/src/game/Map.py b/src/game/Map.py index b21cf9f..8530e29 100644 --- a/src/game/Map.py +++ b/src/game/Map.py @@ -243,7 +243,7 @@ class Map: return True return False - def insertHerbs(self, coordsList): + def insertHerbs(self, coordsList): # Insert herbs on right coordinates nr = 1 for i in range(10): entity = Pickupable("herb" + str(nr) + ".png", self.tileSize, coordsList[i], Statistics(0, 0, 0, 0), "herb") @@ -252,6 +252,10 @@ class Map: nr += 1 def movableList(self): + """ + Return list which is movable on beginning of game, + that is terrainTilesList without entity on self + """ terrainList = self.terrainTilesList for i in self.entities: terrainList.remove(self.getTileOnCoord((i.x, i.y))) diff --git a/src/game/TerrainTile.py b/src/game/TerrainTile.py index 0180220..161b640 100644 --- a/src/game/TerrainTile.py +++ b/src/game/TerrainTile.py @@ -2,6 +2,7 @@ from pathlib import Path import pygame + # TODO: Relative coords class TerrainTile(pygame.sprite.Sprite): def __init__(self, x, y, texture, tileSize, cost): @@ -27,4 +28,4 @@ class TerrainTile(pygame.sprite.Sprite): def __repr__(self): coords = (self.x, self.y) - return str(coords) \ No newline at end of file + return str(coords)