def get_ui_element(self, element, screenshot=None, exit_on_fail=True): """ Find a UI element on the screen """ # Move the mouse so it doesn't obstruct search pyautogui.moveTo(400, 400) # Get a screenshot if screenshot is None: screenshot = utils.take_screenshot(False) # Try to match the template in the screenshot result = cv2.matchTemplate(screenshot, self.templates[element], cv2.TM_CCORR_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) # Found the element, return the location if max_val > 0.9: return max_loc # Failed to find the element with high enough confidence if not exit_on_fail: return False # Not finding the element is severe enough to quit the game utils.log("SEVERE", F"Failed to find {element}, max confidence was {max_val}") utils.quit_game()
def step(self, new_position): """ Given a set of coordinates, calculate what direction to step and send that key to the game """ # Calculate direction of movement x_diff = self.game_map.player_position[0] - new_position[0] y_diff = self.game_map.player_position[1] - new_position[1] # Determine what key to press direction = '' if x_diff == 1: direction = 'a' elif x_diff == -1: direction = 'd' elif y_diff == 1: direction = 'w' elif y_diff == -1: direction = 's' else: utils.log( 'SEVERE', F"Invalid step difference. xDiff: {x_diff}, yDiff: {y_diff}") utils.quit_game() # Move along path pyautogui.press(direction) sleep(0.1) # Player moved, re-detect environment screenshot = utils.take_screenshot() self.game_map.update_player_position(screenshot) self.game_map.update_map()
def get_weight(self): """ Gets the player's current and max weight """ # Find the current and total weight screenshot = utils.take_screenshot(False) weight = self.get_ui_element('weight', screenshot) weight = screenshot[weight[1]:(weight[1] + 12), (weight[0] + 40):(weight[0] + 84)] # Resize and parse the image to a string weight = cv2.resize(weight, (0, 0), fx=3, fy=3) weight = cv2.cvtColor(weight, cv2.COLOR_BGR2GRAY) weight = cv2.bitwise_not(weight) weight = cv2.fastNlMeansDenoising(weight, None, 9, 13) _, weight = cv2.threshold(weight, 180, 255, cv2.THRESH_BINARY) weight = cv2.blur(weight, (4, 2)) weight_text = '' try: weight_text = pytesseract.image_to_string( weight, config=utils.TESSERACT_CONF) except UnicodeDecodeError: utils.log( "SEVERE", "Tesseract failed to parse player weight from screenshot") utils.quit_game() # Split the equation and calculate the difference current_weight, max_weight = weight_text.split("/")[0::1] return int(current_weight), int(max_weight)
def get_health(self): """ Gets the player's current health """ # Reduce the screenshot to include only the player's health screenshot = utils.take_screenshot(False) health = self.get_ui_element('health', screenshot) health = screenshot[health[1]:(health[1] + 12), (health[0] + 36):(health[0] + 92)] # Resize and parse the image to a string health = cv2.resize(health, (0, 0), fx=3, fy=3) health = cv2.cvtColor(health, cv2.COLOR_BGR2GRAY) health = cv2.bitwise_not(health) health = cv2.fastNlMeansDenoising(health, None, 9, 13) _, health = cv2.threshold(health, 180, 255, cv2.THRESH_BINARY) health = cv2.blur(health, (4, 2)) health_text = '' try: health_text = pytesseract.image_to_string( health, config=utils.TESSERACT_CONF) except UnicodeDecodeError: utils.log( "SEVERE", "Tesseract failed to parse player health from screenshot") utils.quit_game() # Split the equation and calculate the difference current = health_text.split("/")[0] return int(current)
def wait_for_ui_element(self, element, exit_on_fail=True): """ Some elements take time to appear after an event, typically a click, was sent. This method will attempt to find a UI element on the screen 10 times. After each failed attempt, it will delay 0.5 seconds and try again. If the element is not found after 10 attempts, it will either quit the game or return false depending on the value of exit_on_fail. An example of when to use this method is after clicking on a merchant, the "Buy or sell" window can take some time to appear on screen. """ attempts = 0 element_loc = None while attempts < 20: # Try and click on the merchant element_loc = self.get_ui_element(element, exit_on_fail=False) # Found the element if element_loc: return element_loc sleep(0.25) attempts += 1 # Failed to find the UI element after 10 attempts if not element_loc and exit_on_fail: utils.log( "SEVERE", F"Failed to find {element} after waiting and searching 20 times" ) utils.quit_game() # Failed to find element but failure will be handled elsewhere return False
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 move_to_merchant(self, merchant_type): """ Move to a merchant """ # Move to the appropriate merchant merchant_tile = None # Weapons shopkeeper if merchant_type == self.MERCHANTS.WEAPONS: merchant_tile = self.game_map.TILES.WEAPON_SHOPKEEPER.value self.move.move_to(self.move.WEAPON_SHOPKEEPER) # Blacksmith elif merchant_type == self.MERCHANTS.BLACKSMITH: merchant_tile = self.game_map.TILES.BLACKSMITH.value self.move.move_to(self.move.FURNACE) # Potions shopkeeper elif merchant_type == self.MERCHANTS.POTIONS: merchant_tile = self.game_map.TILES.POTIONS_SHOPKEEPER.value self.move.move_to(self.move.POTIONS_SHOPKEEPER) # Items shopkeeper elif merchant_type == self.MERCHANTS.ITEMS: merchant_tile = self.game_map.TILES.ITEM_SHOPKEEPER.value self.move.move_to(self.move.ITEMS) # Banker elif merchant_type == self.MERCHANTS.BANKER: merchant_tile = self.game_map.TILES.BANKER.value self.move.move_to(self.move.BANKER) else: utils.log("SEVERE", "Invalid merchant_type supplied to move_to_merchant") utils.quit_game() # Update map so merchant's position will be current self.game_map.update_map() # Get the 5x5 matrix of tiles surrounding the player clickable_tiles = self.game_map.game_map[( self.game_map.player_position[0] - 2):(self.game_map.player_position[0] + 3), (self.game_map.player_position[1] - 2):(self.game_map.player_position[1] + 3)] # Find the index where merchant is located merchant_indices = np.argwhere(clickable_tiles == merchant_tile) if merchant_indices.size == 0: return False # Get the merchant index merchant_index = merchant_indices[0] # Calculate where to click (2 because player is in center of 5x5 matrix that is 0 indexed) x_coord = merchant_index[0] - 2 y_coord = merchant_index[1] - 2 return (176 + (x_coord * 16), 176 + (y_coord * 16))
def sell_item(self, item, merchant_type): """ Move to a merchant and sell an item """ # Open the "Buy or Sell" window buy_or_sell = self.open_merchant_window(merchant_type) if not buy_or_sell: utils.log( "SEVERE", F"Failed to click on {merchant_type} and open 'Buy or Sell' after 10 attempts" ) utils.quit_game() # Click the sell button sell_button = self.user_interface.wait_for_ui_element('sell') pyautogui.moveTo(sell_button[0] + 10, sell_button[1] + 10, 0.15) pyautogui.click() # Wait for the sell menu to open self.user_interface.wait_for_ui_element('sellMenu') # Offer up to 12 items items_sold = 0 for _ in range(12): # Move the cursor away so it will register an "hover" event when move back pyautogui.moveTo(330, 206) # Find a item to sell item_loc = self.user_interface.get_ui_element(item, exit_on_fail=False) # No item_locs left to sell if not item_loc: utils.log("INFO", F"No {item} left to offer shopkeeper") break items_sold += 1 pyautogui.moveTo(item_loc[0] + 6, item_loc[1] + 12, 0.15) pyautogui.doubleClick() sleep(0.5) # Confirm the sale check_mark = self.user_interface.wait_for_ui_element('checkMark') pyautogui.moveTo(check_mark[0] + 5, check_mark[1] + 5, 0.15) pyautogui.click() # Click cancel to leave the window cancel = self.user_interface.wait_for_ui_element('cancel') pyautogui.moveTo(cancel[0] + 5, cancel[1] + 5, 0.15) pyautogui.click() utils.log("INFO", F"Sold {items_sold} {item}(s)") return items_sold
def get_backpack(self): """ Find and return the player's backpack. Returns a tuple containing the backpack and the coordinates """ # Find backpack in screenshot screenshot = utils.take_screenshot(False) result = cv2.matchTemplate(screenshot, self.backpack_template, cv2.TM_CCORR_NORMED) _, max_val, _, backpack_loc = cv2.minMaxLoc(result) # Failed to find the backpack with high confidence if max_val < 0.9: utils.log("SEVERE", "Unable to find backpack in screenshot") utils.quit_game() # Restrict screenshot to just include the player's backpack #backpack_loc = (backpack_loc[0] + 11, backpack_loc[1] + 33) backpack = screenshot[(backpack_loc[1]):(backpack_loc[1] + 144), (backpack_loc[0]):(backpack_loc[0] + 128)] return (backpack, backpack_loc)
def buy_item(self, item, merchant_type): """ Move to a merchant and buy an item """ # Open the "Buy or Sell" window buy_or_sell = self.open_merchant_window(merchant_type) if not buy_or_sell: utils.log( "SEVERE", F"Failed to click on {merchant_type} and open 'Buy or Sell' after 10 attempts" ) utils.quit_game() # Click the buy button buy_button = self.user_interface.wait_for_ui_element('buy') pyautogui.moveTo(buy_button[0] + 10, buy_button[1] + 10, 0.15) pyautogui.click() # Wait for the buy menu to open self.user_interface.wait_for_ui_element('buyMenu') # Find the item to buy item_loc = self.user_interface.wait_for_ui_element(item) pyautogui.moveTo(item_loc[0] + 6, item_loc[1] + 6, 0.15) pyautogui.doubleClick() # Confirm the sale check_mark = self.user_interface.wait_for_ui_element('checkMark') pyautogui.moveTo(check_mark[0] + 5, check_mark[1] + 5, 0.15) pyautogui.click() # Click cancel to leave the window cancel = self.user_interface.wait_for_ui_element('cancel') pyautogui.moveTo(cancel[0] + 5, cancel[1] + 5, 0.15) pyautogui.click() pyautogui.moveTo(400, 400) utils.log("INFO", F"Bought a {item}")
def get_item(self, item, exit_on_failure=False): """ Find an item in the player's backpack and return it's pixel coordinates """ # Get the player's backpack backpack, backpack_loc = self.get_backpack() # Search the backpack for the item result = cv2.matchTemplate(backpack, self.item_templates[item], cv2.TM_CCORR_NORMED) _, max_val, _, item_loc = cv2.minMaxLoc(result) # Failed to find item in backpack with high confidence if max_val < 0.9: if exit_on_failure: utils.log( "SEVERE", F"Unable to find {item} in backpack. max_val: {max_val:3.2f}" ) utils.quit_game() else: return False return (backpack_loc[0] + item_loc[0], backpack_loc[1] + item_loc[1])
def forge(self): """ Forge all ingots into Battle Axes """ # Get player's weight if self.player.is_weight_below_threshold(15): utils.log("INFO", F"Weight is below threshold, selling {self.ITEM}s") self.sell_items() # Buy a hammer if player has none if not self.player.backpack.get_item('hammer'): # Hammer hay be obscured by a forged weapon, sell first self.sell_items() # Still no hammer, buy one if not self.player.backpack.get_item('hammer'): utils.log("INFO", "No hammers remain, buying a hammer") self.merchant.buy_item('hammer', self.merchant.MERCHANTS.BLACKSMITH) # Move to the anvil, if already there, nothing happens self.move.go_to_anvil() # Find the ingots in the player's backpack ingot = self.player.backpack.get_item('ingot') if not ingot: utils.log("INFO", F"No ingots remain, selling {self.ITEM}s") self.sell_items() self.move.go_to_anvil() # Check again to see if there are ingots, may have been obscurred ingot = self.player.backpack.get_item('ingot') if not ingot: return self.player.TASKS.MINE # Use the hammer on the ingots self.player.backpack.use_item('hammer', (ingot[0] + 6, ingot[1] + 6), (7, 3)) # Allow the blacksmith menu to fail a few times if not self.user_interface.wait_for_ui_element('blacksmithMenu', False): self.errors += 1 # Something must actually be wrong if self.errors == 5: utils.log("SEVERE", "Failed to open blacksmith menu after 5 attempts") utils.quit_game() # Start the forge task over again return self.player.TASKS.FORGE # Found the menu, reset error count self.errors = 0 # Forge a Battle Axe weapon = self.user_interface.wait_for_ui_element(self.ITEM) pyautogui.moveTo(weapon[0] + 9, weapon[1] + 10, 0.15) pyautogui.doubleClick() # Continue forging return self.player.TASKS.FORGE
def move_to(self, destination, mining=False): """ Finds a path from the players position to the destination using the aStar shortest path algorithm. Once a path is found, the player takes a step. After each step, the surroundings are examined again and a new path is calculated. This extra work is required because obstacles such as the shopkeepers can move and impede the players movement. """ error = 0 turns_without_moving = 0 while not self.game_map.player_position == destination: # Find a path to the destination path = astar.get_path(self.game_map.game_map, self.game_map.player_position, destination) # Failed to get a path, retry up to 3 times if not path: error += 1 utils.log( "WARN", F"Failed to get path from {self.game_map.player_position} to {destination}" ) # Update map incase something was mislabeled self.game_map.update_map() # Try mining at a different mountain if mining: return False if error == 10: utils.log("SEVERE", "Failed to get path after 10 attempts") utils.debug_print_matrix(self.game_map.game_map.T) utils.quit_game() # Wait for merchant to move blacksmith_errors = 0 while self.game_map.game_map[ destination] == self.game_map.TILES.BLACKSMITH.value: # Don't wait forever for him to move blacksmith_errors += 1 if blacksmith_errors == 20: utils.log( "SEVERE", "Waited 100 seconds for blacksmith to move, suspect error" ) utils.quit_game() utils.log( "INFO", "Blacksmith is blocking spot, waiting for him to move") sleep(5) continue # Reset error count error = 0 # Get the next move from the path step_to = path[0] destination = path[-1] # Take a step towards the destination previous_position = self.game_map.player_position self.step(step_to) # Check if the player has gone several turns without movement if previous_position == self.game_map.player_position: turns_without_moving += 1 else: turns_without_moving = 0 if turns_without_moving == 10: utils.log("SEVERE", "Failed to move after 10 attempts") utils.debug_print_matrix(self.game_map.game_map.T) utils.quit_game() return True
screenshot = utils.take_screenshot(False) result = cv2.matchTemplate(screenshot, user_interface.templates['sponsored'], cv2.TM_CCORR_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) # Found the blocking window window with high confidence if max_val > 0.9: click_at = (max_loc[0] + 428, max_loc[1] + 144) utils.log("INIT", "Closed blocking window") pyautogui.moveTo(click_at[0], click_at[1], 0.15) pyautogui.click() sleep(5) # Bring game to foreground utils.bring_game_to_foreground() # Detect environment screenshot = utils.take_screenshot() game_map.update_player_position(screenshot) utils.log("INIT", F"Player location initialized") game_map.update_map() utils.log("INIT", "Field of view mapped") utils.log("INIT", "Initialization complete") utils.log("INIT", "====================================================") try: while utils.bring_game_to_foreground(): player.perform_task() except Exception as exception: utils.log("SEVERE", exception) utils.log("SEVERE", traceback.format_exc()) utils.quit_game()
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