def build_cycle(self, env: Environment) -> Optional[List[Vector]]: # Attempt to build the list of next actions head = env.snake.head() tail = env.snake.tail() # We build a cycle by building the longest path from the snakes # head to it's tail. If the snake is only 1 tile long, then we # make an 'adjustment' and choose a tile next to the head # as the target, essentially faking a tail. # This is necessary as our algorithm won't return any actions # if the from tile is the same as the target tile. if head == tail: if tail.x > 1: adjustment = Vector(-1, 0) else: adjustment = Vector(1, 0) tail = tail + adjustment built = self._bfsl.longest_path(env, head, tail, env.snake.action.vector) if built is None: return None # We've built the longest path from head to tail, but we need to # check that it covers all vectors. if len(built) != env.available_tiles_count(): return None built.append(head) return built
def prepare_training_environment(self, horizontal_pixels=Constants.ENV_WIDTH, vertical_pixels=Constants.ENV_HEIGHT): environment = Environment(width=horizontal_pixels, height=vertical_pixels) environment.set_wall() environment.set_fruit() environment.set_snake() return environment
def _should_search_vector(self, env: Environment, vector: Vector): t = env.tile_at(vector) # Snake can't move to a vector that would kill it if t == tile.WALL: return False if t == tile.SNAKE: return False return True
def __init__(self, game_model: AbstractModel, fps: int, horizontal_tiles: int, vertical_tiles: int, screen_width: int, screen_height: int, score_logger: ScoreLogger, font: str, screen_depth: int): self.clock = pygame.time.Clock() self.model = game_model self.fps = fps self.screen = pygame.display.set_mode(( screen_width, screen_height, ), 0, screen_depth) self.surface = pygame.Surface(self.screen.get_size()) self.screen_width = screen_width self.screen_height = screen_height self.horizontal_tiles = horizontal_tiles self.vertical_tiles = vertical_tiles self.tile_width = int(screen_width / horizontal_tiles) self.tile_height = int(screen_height / vertical_tiles) self._score_logger = score_logger self._font = font self.environment = Environment(width=self.horizontal_tiles, height=self.vertical_tiles) self.environment.init_wall() self.environment.init_fruit() self.environment.init_snake() self.screen_objects = [] normalised_wall_vectors = self._normalise_vectors( self.environment.wall.get_vectors()) self.wall = WallScreenObject(self, normalised_wall_vectors) self.screen_objects.append(self.wall) normalised_fruit_vectors = self._normalise_vectors( self.environment.fruit.get_vectors()) self.fruit = FruitScreenObject(self, normalised_fruit_vectors) self.screen_objects.append(self.fruit) normalised_snake_vectors = self._normalise_vectors( self.environment.snake.get_vectors()) self.snake = SnakeScreenObject(self, normalised_snake_vectors) self.screen_objects.append(self.snake)
def _can_populate_vector(self, env: Environment, vector: Vector, path: List[Vector], goal: Vector) -> bool: t = env.tile_at(vector) if t == tile.WALL: return False if t == tile.SNAKE: return False if vector in path: return False if vector == goal: return False return True
def next_action(self, environment: Environment) -> act.Action: # We calculate the cycle one and iterate over it if not self._actions: cycle_vectors = self.build_cycle(environment) if not cycle_vectors: # If we're not able to build them, it usually means there # is no path to the fruit. Continue to any available position. if environment.tile_at( environment.snake.head() + environment.snake.action.vector) != tile.EMPTY: return environment.random_action() return environment.snake.action cycle_action_vectors = to_direction_vectors(cycle_vectors) self._actions = act.vectors_to_action(cycle_action_vectors) self._i = 0 # Keep looping over our list of actions next_action = self._actions[self._i] self._i += 1 if self._i == len(self._actions): self._i = 0 return next_action
class Game: def __init__(self, game_model: AbstractModel, fps: int, horizontal_tiles: int, vertical_tiles: int, screen_width: int, screen_height: int, score_logger: ScoreLogger, font: str, screen_depth: int): self.clock = pygame.time.Clock() self.model = game_model self.fps = fps self.screen = pygame.display.set_mode(( screen_width, screen_height, ), 0, screen_depth) self.surface = pygame.Surface(self.screen.get_size()) self.screen_width = screen_width self.screen_height = screen_height self.horizontal_tiles = horizontal_tiles self.vertical_tiles = vertical_tiles self.tile_width = int(screen_width / horizontal_tiles) self.tile_height = int(screen_height / vertical_tiles) self._score_logger = score_logger self._font = font self.environment = Environment(width=self.horizontal_tiles, height=self.vertical_tiles) self.environment.init_wall() self.environment.init_fruit() self.environment.init_snake() self.screen_objects = [] normalised_wall_vectors = self._normalise_vectors( self.environment.wall.get_vectors()) self.wall = WallScreenObject(self, normalised_wall_vectors) self.screen_objects.append(self.wall) normalised_fruit_vectors = self._normalise_vectors( self.environment.fruit.get_vectors()) self.fruit = FruitScreenObject(self, normalised_fruit_vectors) self.screen_objects.append(self.fruit) normalised_snake_vectors = self._normalise_vectors( self.environment.snake.get_vectors()) self.snake = SnakeScreenObject(self, normalised_snake_vectors) self.screen_objects.append(self.snake) def tick(self) -> bool: continue_game = self._handle_user_input() if not continue_game: return False action = self.model.next_action(self.environment) reason = self.environment.step(action) if reason: print(f'died: {reason.reason}') self.snake_died() elif self.environment.won(): print('won') self.snake_died() self._sync_screen_with_environment() self._draw_screen() self._display() self.clock.tick(self.fps) return True def snake_died(self): self._score_logger.log_score(self.model.short_name, self.environment.reward()) self.model.reset() self.environment.init_snake() def draw_tile(self, surface: Surface, col: colour.Colour, vector: Vector): rect = pygame.Rect((vector.x, vector.y), (self.tile_width, self.tile_height)) pygame.draw.rect(surface, col, rect) def _draw_screen(self): self.surface.fill(colour.WHITE) for game_object in self.screen_objects: game_object.draw(self.surface) font = pygame.font.SysFont(self._font, int(self.tile_height / 1.3)) score_text = font.render(str(self.environment.reward()), 1, colour.WHITE) score_text_rect = score_text.get_rect() score_text_rect.center = (self.screen_width / 2, self.tile_height / 2) self.surface.blit(score_text, score_text_rect) self.screen.blit(self.surface, (0, 0)) def _sync_screen_with_environment(self): self.fruit.set_vectors( self._normalise_vectors(self.environment.fruit.get_vectors())) self.snake.set_vectors( self._normalise_vectors(self.environment.snake.get_vectors())) def _display(self): pygame.display.flip() pygame.display.update() def _normalise_vectors(self, vectors: List[Vector]) -> List[Vector]: return list(map(lambda x: self._normalise_vector(x), vectors)) def _normalise_vector(self, vector: Vector) -> Vector: return Vector(vector.x * self.tile_width, vector.y * self.tile_height) def _handle_user_input(self) -> bool: for event in pygame.event.get(): if event.type == QUIT: return False elif event.type == KEYDOWN: self.model.user_input(event) return True