class Player(): """ This class handles everything relating to the player """ LOW_HEALTH = 150 TASKS = Enum('Task', 'MINE, SMELT, FORGE') def __init__(self, game_map, task=TASKS.MINE): self.user_interface = UserInterface() self.backpack = Backpack() self.mine = Mine(self, game_map) self.smelt = Smelt(self, game_map) self.forge = Forge(self, game_map) # Set the default initial task self.task = task self.action_count = 0 def perform_task(self): """ Checks health, organizes backpack and then performs one of the following tasks: mine, smelt, or forge """ # Do pre-task checks self.action_count += 1 # Only check health every 25 turns (it is a slow process) if self.action_count % 25 == 0: self.action_count = 0 self.check_health() # Perform task if self.task == self.TASKS.MINE: self.task = self.mine.mine() delay = 0 elif self.task == self.TASKS.SMELT: self.task = self.smelt.smelt() delay = 1.4 elif self.task == self.TASKS.FORGE: self.task = self.forge.forge() delay = 2.1 # Give the task time to complete sleep(delay) # Organize backpack now that items have been potentially added self.organize_backpack() def check_health(self): """ Checks player's HP and uses a potion if it is low. If no potions are found, game will quit """ # Check if HP is low if self.user_interface.get_health() < self.LOW_HEALTH: # Attempt to use a potion utils.log("INFO", F"Health dropped below {self.LOW_HEALTH}") used_potion = self.backpack.use_item('potion', offset=(4, 9)) # No potions were found if not used_potion: # Potions may be obscurred by items, try selling self.forge.sell_items() # Try using a potion again used_potion = self.backpack.use_item('potion', offset=(4, 9)) # Still can't use potions, likely do not have any if not used_potion: utils.log("SEVERE", "No potions found") utils.quit_game() # Sleep so that there is no issue using the next item utils.log("INFO", F"Used a potion") sleep(6) def is_weight_below_threshold(self, threshold): # Calculate how much more weight the player can carry current_weight, max_weight = self.user_interface.get_weight() difference = max_weight - current_weight # Check if the weight the player can carry is below the threshold if difference < threshold: utils.log("INFO", F"Weight is {difference} from max, threshold was set to {threshold}") return True return False def organize_backpack(self): """ Move all items to the correct areas of the backpack """ self.backpack.move_item('ore', self.backpack.ORE_LOC, (8, 6)) self.backpack.move_item('gem', self.backpack.GEM_LOC, (4, 2)) self.backpack.move_item('jewel', self.backpack.GEM_LOC, (4, 2)) self.backpack.move_item('galantine', self.backpack.GEM_LOC, (5, 4)) self.backpack.move_item('pickaxe', self.backpack.PICKAXE_LOC, (9, 4)) self.backpack.move_item('dagger', self.backpack.DAGGER_LOC, (4, 6)) self.backpack.move_item('hammer', self.backpack.HAMMER_LOC, (7, 3)) self.backpack.move_item('gold', self.backpack.GEM_LOC, (6, 5)) self.backpack.move_item('ingot', self.backpack.INGOT_LOC)
class GameMap: """ This class is used to detect inaccessible tiles in the gameplay region and perform actions with them. """ TILE_DIM = 16 TILES = Enum( 'Tile', 'UNKNOWN, ACCESSIBLE, DOOR, GRAVEL, INACCESSIBLE, MOUNTAIN, WEAPON_SHOPKEEPER, ITEM_SHOPKEEPER, POTION_SHOPKEEPER, BANKER, BLACKSMITH, PLAYER' ) def __init__(self): """ Create a tuple for each tile containing the following properties: 0: tile data, 1: name """ self.backpack = Backpack() try: # Load the saved map from disk self.game_map = np.loadtxt('map.txt', dtype=int) except OSError: # Map does not exist, create a new one self.game_map = np.ones(shape=(82, 240), dtype="int") self.game_map[:, 0:166] = self.TILES.INACCESSIBLE.value self.game_map[:, -10:-1] = self.TILES.INACCESSIBLE.value self.game_map[0, :] = self.TILES.INACCESSIBLE.value self.game_map[-10:-1, :] = self.TILES.INACCESSIBLE.value # Initialize the player position self.player_position = (0, 0) self.templates = [] # Load all accessible tile templates for tile in [ f for f in os.listdir('./accessible_tiles') if f.endswith('.png') ]: tile_template = cv2.imread('./accessible_tiles/' + tile) tile_template = cv2.cvtColor(tile_template, cv2.COLOR_BGR2GRAY) self.templates.append( [tile_template, 'accessible/' + tile.split('.')[0]]) # Load all inaccessible tile templates for tile in [ f for f in os.listdir('./inaccessible_tiles') if f.endswith('.png') ]: tile_template = cv2.imread('./inaccessible_tiles/' + tile) tile_template = cv2.cvtColor(tile_template, cv2.COLOR_BGR2GRAY) self.templates.append( [tile_template, 'inaccessible/' + tile.split('.')[0]]) # Load all NPC tile templates for tile in [f for f in os.listdir('./npcs') if f.endswith('.png')]: tile_template = cv2.imread('./npcs/' + tile) tile_template = cv2.cvtColor(tile_template, cv2.COLOR_BGR2GRAY) self.templates.append( [tile_template, 'npcs/' + tile.split('.')[0]]) def update_player_position(self, screenshot): """To update the player position the following steps are taken. 1. The sextant is location and used 2. The coordinates are parsed and normalized 3. Old player position is removed from map 4. New player position is added to the map If any of these operations fails, the game will quit. Returns tuple containing player position as well as the tile map """ sextant_x = None sextant_y = None errors = 0 while sextant_x is None: # Move mouse to a neutral position that won't obstruct template matching pyautogui.moveTo(400, 400) # Find and use the sextant self.backpack.use_item('sextant', None, (5, 2), True) # Take a new screenshot that includes the location screenshot = utils.take_screenshot() # Find the current position position = screenshot[450:465, 120:180] # Resize and parse the image to a string position = cv2.resize(position, (0, 0), fx=5, fy=5) position = cv2.bitwise_not(position) position = cv2.blur(position, (8, 8)) text = '' try: text = pytesseract.image_to_string(position, config='--psm 8') # Split the text into coordinates sextant_x, sextant_y = text.split(", ")[0::1] except UnicodeDecodeError: utils.log("SEVERE", F"Unable to parse sextant coordinates string") utils.quit_game() except ValueError as value_error: utils.log("WARN", F"{value_error}") # Move mouse to a neutral position that won't obstruct template matching pyautogui.moveTo(400, 400) errors += 1 if errors == 10: utils.log("SEVERE", F"Sextant failed 10 times") utils.quit_game() # Normalize the coordinates to fit the map indicies normalized_x = int(sextant_x) - utils.NORMALIZATION_CONSTANT normalized_y = int(sextant_y) - utils.NORMALIZATION_CONSTANT # Remove old player position from the map self.game_map[self.game_map == self.TILES.PLAYER.value] = self.TILES.ACCESSIBLE.value self.player_position = (normalized_x, normalized_y) self.game_map[self.player_position] = self.TILES.PLAYER.value def match_template_type(self, tile, templates): """ Compare the tile with a set of templates """ potential_tiles = [] # Go through all accessible tiles for template in templates: result = cv2.matchTemplate(tile, template[0], cv2.TM_CCORR_NORMED) max_val = cv2.minMaxLoc(result)[1] # Confidence too low for a match if max_val < 0.90: continue # This is a potential tile potential_tiles.append((template, max_val)) # Very high confidence that this is the correct tile if max_val > 0.99: break return potential_tiles def update_map(self, screenshot=None): """ Takes a screenshot of the game, this method will iterate over the gameplay region in 16x16 chunks. Each chunk is compared with all potential inaccessible tiles until a match is found. When a match is found, the frequency of that tile is incremented so future chunks are compared with high frequency tiles first. """ # Get the visible tiles nearby = self.game_map[(self.player_position[0] - 10):(self.player_position[0] + 11), (self.player_position[1] - 10):(self.player_position[1] + 11)] # Clear NPCs in the nearby as they may have moved nearby[nearby == self.TILES.WEAPON_SHOPKEEPER.value] = self.TILES.UNKNOWN.value nearby[nearby == self.TILES.BLACKSMITH.value] = self.TILES.UNKNOWN.value # Take screenshot and isolate the gamplay region if screenshot is None: screenshot = utils.take_screenshot() play = screenshot[8:344, 8:344] # Loop through all unknown tiles in the nearby for i, j in zip(*np.where(nearby == self.TILES.UNKNOWN.value)): # Scale up the dimensions tile_x = i * self.TILE_DIM tile_y = j * self.TILE_DIM # The center cell is always the player if i == 10 and j == 10: tile_x = self.player_position[0] + int(tile_x / 16) - 10 tile_y = self.player_position[1] + int(tile_y / 16) - 10 self.game_map[(tile_x, tile_y)] = self.TILES.PLAYER.value continue # Slice the tile from the play region tile = play[tile_y:tile_y + self.TILE_DIM, tile_x:tile_x + self.TILE_DIM] tile_x = self.player_position[0] + int(tile_x / 16) - 10 tile_y = self.player_position[1] + int(tile_y / 16) - 10 # Go through all tile types looking for a high confidence match template = None for potential_template in self.templates: if np.allclose(potential_template[0], tile, 1, 1): template = potential_template break # No match, assume it is inaccessible if template is None: self.game_map[(tile_x, tile_y)] = self.TILES.INACCESSIBLE.value continue # By default, mark tile as inaccessible label = None # Mark as mineable if re.search(r'rock', template[1], re.M | re.I): label = self.TILES.MOUNTAIN.value elif re.search(r'door', template[1], re.M | re.I): label = self.TILES.DOOR.value elif re.search(r'gravel', template[1], re.M | re.I): label = self.TILES.GRAVEL.value elif re.search(r'shopkeeper', template[1], re.M | re.I): label = self.TILES.WEAPON_SHOPKEEPER.value elif re.search(r'blacksmith', template[1], re.M | re.I): label = self.TILES.BLACKSMITH.value elif re.search(r'guard', template[1], re.M | re.I): label = self.TILES.INACCESSIBLE.value elif re.search(r'inaccessible', template[1], re.M | re.I): label = self.TILES.INACCESSIBLE.value elif re.search(r'accessible', template[1], re.M | re.I): label = self.TILES.ACCESSIBLE.value # Calculate coordinates of tile in the map relative to the player self.game_map[(tile_x, tile_y)] = label # Go through all tiles in the gameplay region to find the mountains for i, j in zip(*np.where(nearby == self.TILES.MOUNTAIN.value)): # Get the tile to the left of the mountain tile_left = nearby[(i - 1, j)] # Only allow mountains to be minable if they are beside gravel if not tile_left == self.TILES.GRAVEL.value: nearby[(i, j)] = self.TILES.INACCESSIBLE.value # Save the game map to disk np.savetxt('map.txt', self.game_map, fmt='%d')