import math import random from copy import deepcopy from random import randrange from typing import List import numpy as np import numpy.typing as npt from common import Position, get_islands, AREAS_TO_CROSS, find_neighbours, get_tiles_positions from const import * class Genome: grid: npt.NDArray knights_red: List[Position] knights_blue: List[Position] waters: List[Position] trees: List[Position] sands: List[Position] monsters: List[Position] fitness: int sand_islands: List[Position] tree_islands: List[Position] water_islands: List[Position] def __init__(self): self.grid = np.zeros((ROWS, COLUMNS), dtype=int) self.fitness = 0 self.knights_red = spawn_objects_in_given_area( grid=self.grid, object_alias=MAP_ALIASES.get("KNIGHT_RED"), objects_count=KNIGHTS_PER_TEAM_COUNT, spawn_position_start=Position(row=LEFT_KNIGHTS_SPAWN_FIRST_ROW, col=LEFT_KNIGHTS_SPAWN_FIRST_COL), width=KNIGHTS_SPAWN_WIDTH, height=KNIGHTS_SPAWN_HEIGHT ) self.knights_blue = spawn_objects_in_given_area( grid=self.grid, object_alias=MAP_ALIASES.get("KNIGHT_BLUE"), objects_count=KNIGHTS_PER_TEAM_COUNT, spawn_position_start=Position(row=RIGHT_KNIGHTS_SPAWN_FIRST_ROW, col=RIGHT_KNIGHTS_SPAWN_FIRST_COL), width=KNIGHTS_SPAWN_WIDTH, height=KNIGHTS_SPAWN_HEIGHT ) spawn_objects_in_given_area( grid=self.grid, object_alias=MAP_ALIASES.get("CASTLE"), objects_count=4, spawn_position_start=Position(row=CASTLE_SPAWN_FIRST_ROW, col=CASTLE_SPAWN_FIRST_COL), width=2, height=2 ) self.waters = spawn_objects_in_given_area(grid=self.grid, object_alias=MAP_ALIASES.get("WATER"), objects_count=WATER_COUNT) self.trees = spawn_objects_in_given_area(grid=self.grid, object_alias=MAP_ALIASES.get("TREE"), objects_count=TREE_COUNT) self.sands = spawn_objects_in_given_area(grid=self.grid, object_alias=MAP_ALIASES.get("SAND"), objects_count=SAND_COUNT) self.monsters = spawn_objects_in_given_area(grid=self.grid, object_alias=MAP_ALIASES.get("MONSTER"), objects_count=MONSTERS_COUNT) self.sand_islands = get_islands(self.grid, self.sands) self.tree_islands = get_islands(self.grid, self.trees) self.water_islands = get_islands(self.grid, self.waters) def update_map(self): self.sands, self.trees, self.waters, self.monsters = get_tiles_positions(self.grid) self.sand_islands = get_islands(self.grid, self.sands) self.tree_islands = get_islands(self.grid, self.trees) self.water_islands = get_islands(self.grid, self.waters) def calc_fitness(self): score = SAND_COUNT + TREE_COUNT + WATER_COUNT score = score - len(self.sand_islands) - len(self.tree_islands) - len(self.water_islands) sands, trees, waters, monsters = get_tiles_positions(self.grid) if len(monsters) != MONSTERS_COUNT: self.fitness = 0 return if len(sands) < SAND_COUNT or len(trees) < TREE_COUNT or len(waters) < WATER_COUNT: self.fitness = 5 return self.fitness = score def crossover(self, partner): # replace a randomly selected part of the grid with partner's part child = Genome() child.grid = deepcopy(self.grid) area_to_cross = random.choice(AREAS_TO_CROSS) for row in range(area_to_cross.position.row, area_to_cross.position.row + area_to_cross.height): for col in range(area_to_cross.position.col, area_to_cross.position.col + area_to_cross.width): child.grid[row][col] = partner.grid[row][col] child.update_map() return child def mutate(self, mutation_rate: float): # remove 1 item from a random island and add a neighbor to another island if random.random() < mutation_rate: # select islands of the same, random type islands_of_same_type = random.choice([self.sand_islands, self.tree_islands, self.water_islands]) random_index = random.randint(0, len(islands_of_same_type) - 1) island = islands_of_same_type[random_index] next_island = islands_of_same_type[(random_index + 1) % len(islands_of_same_type)] free_tiles_nearby = find_neighbours(self.grid, next_island.col, next_island.row) tile_type = self.grid[island.row][island.col] self.grid[island.row][island.col] = MAP_ALIASES.get('GRASS') # todo: if there are no free tiles around then randomize another next_island if len(free_tiles_nearby) > 0: random_free_tile = random.choice(free_tiles_nearby) island.row = random_free_tile.row island.col = random_free_tile.col self.grid[island.row][island.col] = tile_type self.update_map() def is_empty(grid: npt.NDArray, position: Position) -> bool: return grid[position.row, position.col] in [MAP_ALIASES.get("GRASS"), MAP_ALIASES.get("SAND")] def is_invalid_area(spawn_position_start, height, width) -> bool: return spawn_position_start.row + height - 1 < 0 or \ spawn_position_start.row + height - 1 >= ROWS or \ spawn_position_start.col + width - 1 < 0 or \ spawn_position_start.col + width - 1 >= COLUMNS def spawn_objects_in_given_area(grid: npt.NDArray, object_alias: str, objects_count: int = 1, spawn_position_start: Position = Position(row=0, col=0), width: int = COLUMNS, height: int = ROWS) -> List[Position]: if is_invalid_area(spawn_position_start, height, width): raise ValueError("Invalid spawn area") objects_remaining = int(objects_count) positions = [] while objects_remaining > 0: row = randrange(spawn_position_start.row, spawn_position_start.row + height) col = randrange(spawn_position_start.col, spawn_position_start.col + width) position = Position(row=row, col=col) if is_empty(grid=grid, position=position): grid[position.row, position.col] = object_alias positions.append(position) objects_remaining -= 1 return positions