added fitness function, first version of main and genetics integration
This commit is contained in:
parent
120cc7489e
commit
8e7e279c4b
0
algorithms/learn/genetic_algorithm/__init__.py
Normal file
0
algorithms/learn/genetic_algorithm/__init__.py
Normal file
@ -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))
|
||||
|
||||
|
48
algorithms/learn/genetic_algorithm/helpers.py
Normal file
48
algorithms/learn/genetic_algorithm/helpers.py
Normal 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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
34
game.py
34
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(
|
||||
|
31
main.py
31
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")
|
||||
|
12
minefield.py
12
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()
|
||||
|
@ -16,6 +16,7 @@ class ChainedMine(Mine):
|
||||
return super().disarm(wire)
|
||||
|
||||
else:
|
||||
self.active = False
|
||||
return False
|
||||
|
||||
def investigate(self):
|
||||
|
@ -25,6 +25,7 @@ class Mine(ABC):
|
||||
return True
|
||||
|
||||
else:
|
||||
self.active = False
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 503 B |
@ -48,7 +48,7 @@
|
||||
"terrain": "MUD",
|
||||
"mine": {
|
||||
"mine_type": "time",
|
||||
"timer": 20
|
||||
"timer": 320
|
||||
}
|
||||
},
|
||||
"1,0": {
|
||||
@ -154,7 +154,7 @@
|
||||
"terrain": "GRASS",
|
||||
"mine": {
|
||||
"mine_type": "time",
|
||||
"timer": 19
|
||||
"timer": 40
|
||||
}
|
||||
},
|
||||
"3,2": {
|
||||
@ -363,7 +363,7 @@
|
||||
"terrain": "MUD",
|
||||
"mine": {
|
||||
"mine_type": "time",
|
||||
"timer": 39
|
||||
"timer": 240
|
||||
}
|
||||
},
|
||||
"7,7": {
|
||||
@ -386,7 +386,7 @@
|
||||
"terrain": "MUD",
|
||||
"mine": {
|
||||
"mine_type": "time",
|
||||
"timer": 24
|
||||
"timer": 150
|
||||
}
|
||||
},
|
||||
"8,2": {
|
||||
|
Loading…
Reference in New Issue
Block a user