added fitness function, first version of main and genetics integration

This commit is contained in:
s452645 2021-06-17 20:30:32 +02:00
parent 120cc7489e
commit 8e7e279c4b
16 changed files with 195 additions and 49 deletions

View File

@ -5,6 +5,8 @@ from itertools import permutations
from numpy.random import randint from numpy.random import randint
from numpy.random import rand 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] # this is helper function for sum_distance function, it counts the taxi cab distance between 2 vectors [x,y]
def distance(x,y): def distance(x,y):
temp1 = abs(x[0]-y[0]) temp1 = abs(x[0]-y[0])
@ -76,22 +78,20 @@ def mutation(speciment, r_mut):
# genetic algorithm # 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 # 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 # 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]] speciment=helpers.get_mines_coords(minefield)
pop = [random.choice(list(permutations(speciment,len(speciment)))) for _ in range(n_pop)] pop = [random.sample(speciment, len(speciment)) for _ in range(n_pop)]
# permutation function returns tuples so I change them to lists # 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 # keep track of best solution
best, best_eval = 0, objective(pop[0]) best, best_eval = 0, objective(minefield, pop[0])
# enumerate generations # enumerate generations
for gen in range(n_iter): for gen in range(n_iter):
# evaluate all candidates in the population # 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 # check for new best solution
for i in range(n_pop): for i in range(n_pop):
if scores[i] < best_eval: 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] return [best, best_eval]
# define the total iterations if __name__ == "__main__":
n_iter = 100
# bits # define the total iterations
n_bits = 20 n_iter = 100
# define the population size # bits
n_pop = 100 n_bits = 20
# crossover rate # define the population size
r_cross = 0.9 n_pop = 100
# mutation rate # crossover rate
r_mut = 0.05 r_cross = 0.9
# perform the genetic algorithm search # mutation rate
best, score = genetic_algorithm(sum_distance, n_iter, n_pop, r_cross, r_mut) r_mut = 0.05
print('Done!')
print('f(%s) = %f' % (best, score)) # 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))

View File

@ -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

View File

@ -34,10 +34,6 @@ def get_node_cost(node: Node, minefield: Minefield):
if node.action != Action.GO: if node.action != Action.GO:
return node.parent.cost + 1 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
@ -95,7 +91,8 @@ def graphsearch(initial_state: State,
explored: List[Node] = None, explored: List[Node] = None,
target_type: str = "tile", target_type: str = "tile",
tox: int = None, tox: int = None,
toy: int = None): toy: int = None,
with_data=False):
# reset global priority queue helpers # reset global priority queue helpers
global entry_finder global entry_finder
@ -158,6 +155,10 @@ def graphsearch(initial_state: State,
parent = parent.parent parent = parent.parent
actions_sequence.reverse() actions_sequence.reverse()
if with_data:
return actions_sequence, element.state, element.cost
return actions_sequence return actions_sequence
# add current node to explored (prevents infinite cycles) # add current node to explored (prevents infinite cycles)

View File

@ -58,17 +58,21 @@ def blit_graphics(minefield):
pass # darken_tile(tile.position) pass # darken_tile(tile.position)
# draw a mine on top if there is one # 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): if isinstance(tile.mine, StandardMine):
const.SCREEN.blit(mine_asset_options["MINE"], tile_screen_coords) const.SCREEN.blit(mine_asset_options["MINE"], tile_screen_coords)
if isinstance(tile.mine, ChainedMine): if isinstance(tile.mine, ChainedMine):
predecessor = tile.mine.predecessor 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) display_chained_mine(tile.position, predecessor_position)
elif isinstance(tile.mine, TimeMine): 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) # changed sapper's movement from jumping to continuous movement (moved everything to Agent's class)
# # sapper # # sapper
@ -234,7 +238,7 @@ def display_chained_mine(coords, parent_coords=None):
display_number(const.Coords.Y, parent_coords[1]) 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): 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) number_coords = (mine_coords[0] + 44, mine_coords[1] + 22)
elif which_digit == const.Digit.TENS: elif which_digit == const.Digit.TENS:
number_coords = (mine_coords[0] + 36, mine_coords[1] + 22) 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( const.SCREEN.blit(
number_asset, number_asset,
@ -275,5 +281,6 @@ def display_time_mine(coords, time):
calculate_screen_position(coords) calculate_screen_position(coords)
) )
display_number(const.Digit.ONES, int(str(time)[-1])) display_number(const.Digit.ONES, int(str(seconds)[-1]))
display_number(const.Digit.TENS, int(str(time)[-2])) display_number(const.Digit.TENS, int(str(seconds)[-2]))
display_number(const.Digit.MINUTES, minutes)

View File

@ -532,7 +532,7 @@ class SampleWindow:
def run(self, mine: Mine): def run(self, mine: Mine):
timed_event = pygame.USEREVENT + 1 timed_event = pygame.USEREVENT + 1
pygame.time.set_timer(timed_event, 6000) pygame.time.set_timer(timed_event, 1000)
step = 0 step = 0
handler = DisarmingHandler(mine) handler = DisarmingHandler(mine)

34
game.py
View File

@ -5,6 +5,8 @@ import project_constants as const
from assets.display_assets import blit_graphics from assets.display_assets import blit_graphics
from algorithms.search import a_star from algorithms.search import a_star
from algorithms.learn.genetic_algorithm import genetic_algorithm, helpers
from minefield import Minefield from minefield import Minefield
from objects.mine_models.time_mine import TimeMine from objects.mine_models.time_mine import TimeMine
@ -35,6 +37,8 @@ class Game:
self.action_timer = 0 self.action_timer = 0
self.action_delta_time = 0 self.action_delta_time = 0
self.genetic_sequence = None
# declaring and initializing gui components # declaring and initializing gui components
# ui_component managers # ui_component managers
self.in_game_gui_components_manager = UiComponentsManager() self.in_game_gui_components_manager = UiComponentsManager()
@ -109,6 +113,9 @@ class Game:
# resetting timer # resetting timer
self.millisecond_timer %= const.TURN_INTERVAL self.millisecond_timer %= const.TURN_INTERVAL
def revert_minefield_turn(self):
self.minefield.turn -= 1
# returns turn number # returns turn number
def get_turn_number(self): def get_turn_number(self):
return self.turn return self.turn
@ -131,6 +138,33 @@ class Game:
else: else:
return False 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 # gets action sequence for agent
def get_action_sequence(self, target_type: str = "tile"): def get_action_sequence(self, target_type: str = "tile"):
return a_star.graphsearch( return a_star.graphsearch(

27
main.py
View File

@ -25,6 +25,8 @@ def main():
running = True running = True
in_menu = True in_menu = True
auto = False auto = False
genetics = False
genetics_ready = False
is_game_over = False is_game_over = False
# create and initialize_gui_components game instance # 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 # if auto button is clicked then leave menu and start auto-mode
auto = game.button_auto.is_clicked(pygame.mouse.get_pos(), events) 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 ==== # # ==== BEFORE GAME LOOP ==== #
# ========================== # # ========================== #
@ -68,9 +76,19 @@ def main():
# initializing action_sequence variable # initializing action_sequence variable
action_sequence = None action_sequence = None
if genetics and not genetics_ready:
game.run_genetics()
genetics_ready = True
# getting action sequence for agent # getting action sequence for agent
if auto and running: if auto and running:
in_menu = False in_menu = False
if genetics:
auto = game.set_next_genetic_target()
action_sequence = game.get_action_sequence("mine")
else:
auto = game.set_random_mine_as_target() auto = game.set_random_mine_as_target()
action_sequence = game.get_action_sequence("mine") action_sequence = game.get_action_sequence("mine")
@ -80,6 +98,9 @@ def main():
# initializing game attributes before the game loop # initializing game attributes before the game loop
game.initialize_before_game_loop() game.initialize_before_game_loop()
# prevent incrementing minefield turn before first action
first_turn_flag = True
# =================== # # =================== #
# ==== GAME LOOP ==== # # ==== GAME LOOP ==== #
# =================== # # =================== #
@ -115,6 +136,10 @@ def main():
game.update_time(time) game.update_time(time)
game.update_turns() game.update_turns()
if first_turn_flag:
game.revert_minefield_turn()
first_turn_flag = False
# make the next move from sequence of actions # make the next move from sequence of actions
if game.agent_should_take_next_action(action_sequence): if game.agent_should_take_next_action(action_sequence):
# give agent next action # give agent next action
@ -132,7 +157,7 @@ def main():
if auto: if auto:
if not game.agent.defuse_a_mine(game.get_mine(game.goal)): if not game.agent.defuse_a_mine(game.get_mine(game.goal)):
print("BOOOOOOM\n\n") print("BOOOOOOM\n\n")
is_game_over = True # is_game_over = True
else: else:
print("guess you will live a little longer...\n\n") print("guess you will live a little longer...\n\n")

View File

@ -7,8 +7,9 @@ import json_generator as jg
class Minefield: class Minefield:
def __init__(self, json_path): def __init__(self, json_path):
self.turn = 0 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 self.json_path = json_path
# open JSON with minefield info # open JSON with minefield info
@ -32,7 +33,7 @@ class Minefield:
successor_row, successor_column = successor_position successor_row, successor_column = successor_position
predecessor_row, predecessor_column = predecessor_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 self.matrix[successor_row][successor_column].mine.predecessor = predecessor
def next_turn(self): def next_turn(self):
@ -43,7 +44,12 @@ class Minefield:
mine = self.matrix[row][column].mine mine = self.matrix[row][column].mine
if mine is not None and isinstance(mine, TimeMine): 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): def get_active_mines(self):
mines = list() mines = list()

View File

@ -16,6 +16,7 @@ class ChainedMine(Mine):
return super().disarm(wire) return super().disarm(wire)
else: else:
self.active = False
return False return False
def investigate(self): def investigate(self):

View File

@ -25,6 +25,7 @@ class Mine(ABC):
return True return True
else: else:
self.active = False
return False return False
@abstractmethod @abstractmethod

View File

@ -10,7 +10,11 @@ class TimeMine(Mine):
super().__init__(TypeHash.TIME, position, active) super().__init__(TypeHash.TIME, position, active)
def disarm(self, wire): def disarm(self, wire):
if self.active:
return super().disarm(wire) return super().disarm(wire)
else:
# mine has already exploded, no need to return failure
return True
def investigate(self): def investigate(self):
return super().investigate() return super().investigate()

View File

@ -5,9 +5,8 @@ from project_constants import Terrain
# on what it is returns value of Terrein Enum # on what it is returns value of Terrein Enum
# It is used in Tile.cost (giving the value to the tile) # It is used in Tile.cost (giving the value to the tile)
def assume_cost(terrain_type, mine): def assume_cost(terrain_type, mine):
if mine is not None: if mine is not None and mine.active:
return Terrain.MINE return Terrain.MINE
if terrain_type == "CONCRETE": if terrain_type == "CONCRETE":
return Terrain.CONCRETE return Terrain.CONCRETE
@ -21,6 +20,16 @@ class Tile:
def __init__(self, position, terrain_type=None, mine=None): def __init__(self, position, terrain_type=None, mine=None):
self.position = position self.position = position
self.terrain_type = terrain_type 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 # mine is an instance of Mine class
self.mine = mine 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

View File

@ -24,7 +24,8 @@ V_FPS = 60
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
DIR_ASSETS = os.path.join(ROOT_DIR, "resources", "assets") 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_TILE_SIZE = 60
V_GRID_VER_TILES = 10 # vertical (number of rows) V_GRID_VER_TILES = 10 # vertical (number of rows)
@ -72,6 +73,7 @@ class Action(Enum):
class Digit(Enum): class Digit(Enum):
ONES = 0 ONES = 0
TENS = 1 TENS = 1
MINUTES = 2
class Coords(Enum): class Coords(Enum):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 503 B

View File

@ -48,7 +48,7 @@
"terrain": "MUD", "terrain": "MUD",
"mine": { "mine": {
"mine_type": "time", "mine_type": "time",
"timer": 20 "timer": 320
} }
}, },
"1,0": { "1,0": {
@ -154,7 +154,7 @@
"terrain": "GRASS", "terrain": "GRASS",
"mine": { "mine": {
"mine_type": "time", "mine_type": "time",
"timer": 19 "timer": 40
} }
}, },
"3,2": { "3,2": {
@ -363,7 +363,7 @@
"terrain": "MUD", "terrain": "MUD",
"mine": { "mine": {
"mine_type": "time", "mine_type": "time",
"timer": 39 "timer": 240
} }
}, },
"7,7": { "7,7": {
@ -386,7 +386,7 @@
"terrain": "MUD", "terrain": "MUD",
"mine": { "mine": {
"mine_type": "time", "mine_type": "time",
"timer": 24 "timer": 150
} }
}, },
"8,2": { "8,2": {