From 8e7e279c4b68706c27ea2d097ca0e50ada3cb348 Mon Sep 17 00:00:00 2001 From: s452645 Date: Thu, 17 Jun 2021 20:30:32 +0200 Subject: [PATCH] added fitness function, first version of main and genetics integration --- .../learn/genetic_algorithm/__init__.py | 0 .../genetic_algorithm.py | 50 ++++++++++-------- algorithms/learn/genetic_algorithm/helpers.py | 48 +++++++++++++++++ algorithms/search/a_star.py | 13 ++--- assets/display_assets.py | 19 ++++--- disarming/popup.py | 2 +- game.py | 34 ++++++++++++ main.py | 31 +++++++++-- minefield.py | 12 +++-- objects/mine_models/chained_mine.py | 1 + objects/mine_models/mine.py | 1 + objects/mine_models/time_mine.py | 6 ++- objects/tile.py | 15 ++++-- project_constants.py | 4 +- resources/assets/time_mine.png | Bin 296 -> 503 bytes resources/minefields/fourthmap.json | 8 +-- 16 files changed, 195 insertions(+), 49 deletions(-) create mode 100644 algorithms/learn/genetic_algorithm/__init__.py rename algorithms/learn/{ => genetic_algorithm}/genetic_algorithm.py (79%) create mode 100644 algorithms/learn/genetic_algorithm/helpers.py diff --git a/algorithms/learn/genetic_algorithm/__init__.py b/algorithms/learn/genetic_algorithm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/algorithms/learn/genetic_algorithm.py b/algorithms/learn/genetic_algorithm/genetic_algorithm.py similarity index 79% rename from algorithms/learn/genetic_algorithm.py rename to algorithms/learn/genetic_algorithm/genetic_algorithm.py index 101fe6b..6b9197d 100644 --- a/algorithms/learn/genetic_algorithm.py +++ b/algorithms/learn/genetic_algorithm/genetic_algorithm.py @@ -5,6 +5,8 @@ from itertools import permutations from numpy.random import randint from numpy.random import rand +from algorithms.learn.genetic_algorithm import helpers + # this is helper function for sum_distance function, it counts the taxi cab distance between 2 vectors [x,y] def distance(x,y): temp1 = abs(x[0]-y[0]) @@ -76,22 +78,20 @@ def mutation(speciment, r_mut): # genetic algorithm -def genetic_algorithm(objective, n_iter, n_pop, r_cross, r_mut): +def genetic_algorithm(minefield, objective, n_iter, n_pop, r_cross, r_mut): # this is hardcoded list of coordinates of all mines (for tests only) which represents one speciment in population # it is then permutated to get set number of species and create population - speciment=[[1,1],[5,2],[1,3],[2,5],[2,1],[3,2],[3,4],[5,0]] - pop = [random.choice(list(permutations(speciment,len(speciment)))) for _ in range(n_pop)] + speciment=helpers.get_mines_coords(minefield) + pop = [random.sample(speciment, len(speciment)) for _ in range(n_pop)] # permutation function returns tuples so I change them to lists - for i in range(len(pop)): - pop[i] = list(pop[i]) # keep track of best solution - best, best_eval = 0, objective(pop[0]) + best, best_eval = 0, objective(minefield, pop[0]) # enumerate generations for gen in range(n_iter): # evaluate all candidates in the population - scores = [objective(c) for c in pop] + scores = [objective(minefield, c) for c in pop] # check for new best solution for i in range(n_pop): if scores[i] < best_eval: @@ -115,19 +115,27 @@ def genetic_algorithm(objective, n_iter, n_pop, r_cross, r_mut): return [best, best_eval] -# define the total iterations -n_iter = 100 -# bits -n_bits = 20 -# define the population size -n_pop = 100 -# crossover rate -r_cross = 0.9 -# mutation rate -r_mut = 0.05 -# perform the genetic algorithm search -best, score = genetic_algorithm(sum_distance, n_iter, n_pop, r_cross, r_mut) -print('Done!') -print('f(%s) = %f' % (best, score)) +if __name__ == "__main__": + + # define the total iterations + n_iter = 100 + # bits + n_bits = 20 + # define the population size + n_pop = 100 + # crossover rate + r_cross = 0.9 + # mutation rate + r_mut = 0.05 + + # create new minefield instance + import os + from minefield import Minefield + minefield = Minefield(os.path.join("..", "..", "..", "resources", "minefields", "fourthmap.json")) + + # perform the genetic algorithm search + best, score = genetic_algorithm(minefield, helpers.get_score, n_iter, n_pop, r_cross, r_mut) + print('Done!') + print('f(%s) = %f' % (best, score)) diff --git a/algorithms/learn/genetic_algorithm/helpers.py b/algorithms/learn/genetic_algorithm/helpers.py new file mode 100644 index 0000000..3802b81 --- /dev/null +++ b/algorithms/learn/genetic_algorithm/helpers.py @@ -0,0 +1,48 @@ +from minefield import Minefield +import project_constants as const + +from project_constants import Direction +from algorithms.search.a_star import State, graphsearch +from objects.mine_models.chained_mine import ChainedMine + + +def get_score(minefield, speciment): + initial_state = State(row=0, column=0, direction=Direction.RIGHT) + score = 0 + + for el_index in range(len(speciment) - 1): + action_sequence, initial_state, cost = \ + graphsearch(initial_state, + minefield, + target_type="mine", + tox=speciment[el_index][0], + toy=speciment[el_index][1], + with_data=True) + + mine = minefield.matrix[speciment[el_index][0]][speciment[el_index][1]].mine + + if isinstance(mine, ChainedMine) and mine.predecessor is not None and mine.predecessor.active: + cost += 10000 + + mine.active = False + + for _ in range(cost): + minefield.next_turn() + + score += cost + + score += 10000 * minefield.explosions + return score + + +def get_mines_coords(minefield: Minefield): + mines = list() + + for row in range(const.V_GRID_VER_TILES): + for column in range(const.V_GRID_VER_TILES): + mine = minefield.matrix[row][column].mine + + if mine is not None: + mines.append([row, column]) + + return mines diff --git a/algorithms/search/a_star.py b/algorithms/search/a_star.py index 227c1ad..f31c2bb 100644 --- a/algorithms/search/a_star.py +++ b/algorithms/search/a_star.py @@ -34,11 +34,7 @@ def get_node_cost(node: Node, minefield: Minefield): if node.action != Action.GO: return node.parent.cost + 1 - # if Tile considered its mine in cost calculation, this code would be priettier - if minefield.matrix[row][column].mine is not None: - return node.parent.cost + 500 - else: - return node.parent.cost + minefield.matrix[row][column].cost.value + return node.parent.cost + minefield.matrix[row][column].cost.value def get_estimated_cost(node: Node): @@ -95,7 +91,8 @@ def graphsearch(initial_state: State, explored: List[Node] = None, target_type: str = "tile", tox: int = None, - toy: int = None): + toy: int = None, + with_data=False): # reset global priority queue helpers global entry_finder @@ -158,6 +155,10 @@ def graphsearch(initial_state: State, parent = parent.parent actions_sequence.reverse() + + if with_data: + return actions_sequence, element.state, element.cost + return actions_sequence # add current node to explored (prevents infinite cycles) diff --git a/assets/display_assets.py b/assets/display_assets.py index b2c3d0c..3d3dc52 100644 --- a/assets/display_assets.py +++ b/assets/display_assets.py @@ -58,17 +58,21 @@ def blit_graphics(minefield): pass # darken_tile(tile.position) # draw a mine on top if there is one - if tile.mine is not None: + if tile.mine is not None and tile.mine.active: if isinstance(tile.mine, StandardMine): const.SCREEN.blit(mine_asset_options["MINE"], tile_screen_coords) if isinstance(tile.mine, ChainedMine): predecessor = tile.mine.predecessor - predecessor_position = predecessor.position if predecessor is not None else None + predecessor_position = \ + predecessor.position if predecessor is not None and predecessor.active else None + display_chained_mine(tile.position, predecessor_position) elif isinstance(tile.mine, TimeMine): - display_time_mine(tile.position, "0" + str(tile.mine.timer)) + minutes = int(tile.mine.timer / 60) + seconds = tile.mine.timer % 60 + display_time_mine(tile.position, minutes, "0" + str(seconds)) # changed sapper's movement from jumping to continuous movement (moved everything to Agent's class) # # sapper @@ -234,7 +238,7 @@ def display_chained_mine(coords, parent_coords=None): display_number(const.Coords.Y, parent_coords[1]) -def display_time_mine(coords, time): +def display_time_mine(coords, minutes, seconds): def display_number(which_digit, number): @@ -264,6 +268,8 @@ def display_time_mine(coords, time): number_coords = (mine_coords[0] + 44, mine_coords[1] + 22) elif which_digit == const.Digit.TENS: number_coords = (mine_coords[0] + 36, mine_coords[1] + 22) + elif which_digit == const.Digit.MINUTES: + number_coords = (mine_coords[0] + 18, mine_coords[1] + 22) const.SCREEN.blit( number_asset, @@ -275,5 +281,6 @@ def display_time_mine(coords, time): calculate_screen_position(coords) ) - display_number(const.Digit.ONES, int(str(time)[-1])) - display_number(const.Digit.TENS, int(str(time)[-2])) + display_number(const.Digit.ONES, int(str(seconds)[-1])) + display_number(const.Digit.TENS, int(str(seconds)[-2])) + display_number(const.Digit.MINUTES, minutes) diff --git a/disarming/popup.py b/disarming/popup.py index d09ab00..139c6f4 100644 --- a/disarming/popup.py +++ b/disarming/popup.py @@ -532,7 +532,7 @@ class SampleWindow: def run(self, mine: Mine): timed_event = pygame.USEREVENT + 1 - pygame.time.set_timer(timed_event, 6000) + pygame.time.set_timer(timed_event, 1000) step = 0 handler = DisarmingHandler(mine) diff --git a/game.py b/game.py index 7295a75..271e6e3 100644 --- a/game.py +++ b/game.py @@ -5,6 +5,8 @@ import project_constants as const from assets.display_assets import blit_graphics from algorithms.search import a_star +from algorithms.learn.genetic_algorithm import genetic_algorithm, helpers + from minefield import Minefield from objects.mine_models.time_mine import TimeMine @@ -35,6 +37,8 @@ class Game: self.action_timer = 0 self.action_delta_time = 0 + self.genetic_sequence = None + # declaring and initializing gui components # ui_component managers self.in_game_gui_components_manager = UiComponentsManager() @@ -109,6 +113,9 @@ class Game: # resetting timer self.millisecond_timer %= const.TURN_INTERVAL + def revert_minefield_turn(self): + self.minefield.turn -= 1 + # returns turn number def get_turn_number(self): return self.turn @@ -131,6 +138,33 @@ class Game: else: return False + def run_genetics(self): + genetics_minefield = Minefield(const.MAP_RANDOM_10x10) + + sequence, score = \ + genetic_algorithm.genetic_algorithm(genetics_minefield, helpers.get_score, 10, 100, 0.9, 0.05) + + print('Done!') + print('f(%s) = %f' % (sequence, score)) + self.genetic_sequence = sequence + + def set_next_genetic_target(self): + if any(self.genetic_sequence): + self.goal = self.genetic_sequence.pop(0) + + # display new destination + self.input_box_row.set_texts(user_input=str(self.goal[0])) + self.input_box_column.set_texts(user_input=str(self.goal[1])) + + # prevents highlighting input_box_row, + # couldn't find any better solution w/o major Game class changes + self.input_box_row.set_is_selected(False) + + return True + + else: + return False + # gets action sequence for agent def get_action_sequence(self, target_type: str = "tile"): return a_star.graphsearch( diff --git a/main.py b/main.py index 4b72059..32c9554 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,8 @@ def main(): running = True in_menu = True auto = False + genetics = False + genetics_ready = False is_game_over = False # create and initialize_gui_components game instance @@ -61,6 +63,12 @@ def main(): # if auto button is clicked then leave menu and start auto-mode auto = game.button_auto.is_clicked(pygame.mouse.get_pos(), events) + # if genetics button is clocked then start genetic algorithm + genetics = game.button_genetic_algorithm.is_clicked(pygame.mouse.get_pos(), events) + + if genetics: + auto = True + # ========================== # # ==== BEFORE GAME LOOP ==== # # ========================== # @@ -68,11 +76,21 @@ def main(): # initializing action_sequence variable action_sequence = None + if genetics and not genetics_ready: + game.run_genetics() + genetics_ready = True + # getting action sequence for agent if auto and running: in_menu = False - auto = game.set_random_mine_as_target() - action_sequence = game.get_action_sequence("mine") + + if genetics: + auto = game.set_next_genetic_target() + action_sequence = game.get_action_sequence("mine") + + else: + auto = game.set_random_mine_as_target() + action_sequence = game.get_action_sequence("mine") elif running: action_sequence = game.get_action_sequence("tile") @@ -80,6 +98,9 @@ def main(): # initializing game attributes before the game loop game.initialize_before_game_loop() + # prevent incrementing minefield turn before first action + first_turn_flag = True + # =================== # # ==== GAME LOOP ==== # # =================== # @@ -115,6 +136,10 @@ def main(): game.update_time(time) game.update_turns() + if first_turn_flag: + game.revert_minefield_turn() + first_turn_flag = False + # make the next move from sequence of actions if game.agent_should_take_next_action(action_sequence): # give agent next action @@ -132,7 +157,7 @@ def main(): if auto: if not game.agent.defuse_a_mine(game.get_mine(game.goal)): print("BOOOOOOM\n\n") - is_game_over = True + # is_game_over = True else: print("guess you will live a little longer...\n\n") diff --git a/minefield.py b/minefield.py index fd50e21..0b89a72 100644 --- a/minefield.py +++ b/minefield.py @@ -7,8 +7,9 @@ import json_generator as jg class Minefield: def __init__(self, json_path): self.turn = 0 + self.explosions = 0 - self.agent = ag.Agent(const.MAP_RANDOM_10x10) + self.agent = ag.Agent(json_path) self.json_path = json_path # open JSON with minefield info @@ -32,7 +33,7 @@ class Minefield: successor_row, successor_column = successor_position predecessor_row, predecessor_column = predecessor_position - predecessor = self.matrix[predecessor_row][predecessor_column] + predecessor = self.matrix[predecessor_row][predecessor_column].mine self.matrix[successor_row][successor_column].mine.predecessor = predecessor def next_turn(self): @@ -43,7 +44,12 @@ class Minefield: mine = self.matrix[row][column].mine if mine is not None and isinstance(mine, TimeMine): - mine.timer = max(0, mine.starting_time - int(self.turn / 4)) + mine.timer = max(0, mine.starting_time - int(self.turn)) + + if mine.timer == 0 and mine.active: + # TODO: BOOM + self.explosions += 1 + mine.active = False def get_active_mines(self): mines = list() diff --git a/objects/mine_models/chained_mine.py b/objects/mine_models/chained_mine.py index 8de019b..60a9cc3 100644 --- a/objects/mine_models/chained_mine.py +++ b/objects/mine_models/chained_mine.py @@ -16,6 +16,7 @@ class ChainedMine(Mine): return super().disarm(wire) else: + self.active = False return False def investigate(self): diff --git a/objects/mine_models/mine.py b/objects/mine_models/mine.py index 23d65f7..ca10033 100644 --- a/objects/mine_models/mine.py +++ b/objects/mine_models/mine.py @@ -25,6 +25,7 @@ class Mine(ABC): return True else: + self.active = False return False @abstractmethod diff --git a/objects/mine_models/time_mine.py b/objects/mine_models/time_mine.py index 10ad349..01d828f 100644 --- a/objects/mine_models/time_mine.py +++ b/objects/mine_models/time_mine.py @@ -10,7 +10,11 @@ class TimeMine(Mine): super().__init__(TypeHash.TIME, position, active) def disarm(self, wire): - return super().disarm(wire) + if self.active: + return super().disarm(wire) + else: + # mine has already exploded, no need to return failure + return True def investigate(self): return super().investigate() diff --git a/objects/tile.py b/objects/tile.py index 0e060fc..6e4de55 100644 --- a/objects/tile.py +++ b/objects/tile.py @@ -5,9 +5,8 @@ from project_constants import Terrain # on what it is returns value of Terrein Enum # It is used in Tile.cost (giving the value to the tile) - def assume_cost(terrain_type, mine): - if mine is not None: + if mine is not None and mine.active: return Terrain.MINE if terrain_type == "CONCRETE": return Terrain.CONCRETE @@ -21,6 +20,16 @@ class Tile: def __init__(self, position, terrain_type=None, mine=None): self.position = position self.terrain_type = terrain_type - self.cost = assume_cost(terrain_type, mine) + self._cost = assume_cost(terrain_type, mine) # mine is an instance of Mine class self.mine = mine + + @property + def cost(self): + self._cost = assume_cost(self.terrain_type, self.mine) + return self._cost + + @cost.setter + def cost(self, new_value): + self._cost = new_value + diff --git a/project_constants.py b/project_constants.py index 28e683e..f269f2f 100644 --- a/project_constants.py +++ b/project_constants.py @@ -24,7 +24,8 @@ V_FPS = 60 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) DIR_ASSETS = os.path.join(ROOT_DIR, "resources", "assets") -TURN_INTERVAL = 0.3 # interval between two turns in seconds +# If below 1, in-game second flies faster than the real one +TURN_INTERVAL = 0.5 # interval between two turns in seconds V_TILE_SIZE = 60 V_GRID_VER_TILES = 10 # vertical (number of rows) @@ -72,6 +73,7 @@ class Action(Enum): class Digit(Enum): ONES = 0 TENS = 1 + MINUTES = 2 class Coords(Enum): diff --git a/resources/assets/time_mine.png b/resources/assets/time_mine.png index 9ebc3ff69dac1d6f35791ece9e5d69081ce4e407..3387e090bfd0142c763e985a00afab9cbe16890a 100644 GIT binary patch delta 477 zcmV<30V4jW0`~)uB!2;OQb$4nuFf3k00004XF*Lt006O%3;baP00009a7bBm000id z000id0mpBsWB>pGj7da6R7gu>7;%6RN;91Bu=@|9|Gs<6@bL2=22){91{M)fup9{d zdGng#^xN+k;vj%a4kj+o%fi4cBn%d_*l`GKCli=QA#jU2%zsdwdj?D+1DH5AK(WP6 zZtEC!Ewg24oooW8L40)an1mBxF=RlI#U4iu!L-j!H-_#{*djzC$Cjt zA1_&80p=qEvMmle?aQ#k`aGDuMST}TDc@9vu=`11{-IM)M{ElN`xqObIH*BBM2}k# zz$Fd?;1s5;rhg1h(WC;tulWq0xF<6FE4aYGG{FEWiU7F8@7}u0NRh?l0t|~q6!}3E z&A^-cAHg)pVytm|($^4k6JE;OlOI*~m7q`Xcn;}^R88H4sBn50CLJA-|JAoXA z&2hv6it{nKfNYCt7Sx0c9kM|^UO!B!BNoL_t(YiS3rp34<^c#$Cdb^biEKc+np0MZA@I><-<*5judB zjL=O4$q4n(Au_<%Q-Xh@1#4PS10OH>9^{u7LK4GRZ7b6RWtyOfd_49psyM*wa+0dz zK18wq!ou& z-l