diff --git a/ requirements-dev.txt b/ requirements-dev.txt new file mode 100644 index 0000000..0e4ee0a --- /dev/null +++ b/ requirements-dev.txt @@ -0,0 +1 @@ +ruff==0.6.9 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 01d7f95..361dafc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__ -venv \ No newline at end of file +venv +.ruff_cache +error_log.txt +error/* \ No newline at end of file diff --git a/Images/END.jpg b/Images/END.jpg new file mode 100644 index 0000000..ec2a5ff Binary files /dev/null and b/Images/END.jpg differ diff --git a/Images/End.jpeg b/Images/End.jpeg deleted file mode 100644 index 9579218..0000000 Binary files a/Images/End.jpeg and /dev/null differ diff --git a/Images/FIGHT.jpeg b/Images/FIGHT.jpeg deleted file mode 100644 index e1f80fe..0000000 Binary files a/Images/FIGHT.jpeg and /dev/null differ diff --git a/Images/FIGHT2.jpeg b/Images/FIGHT2.jpeg deleted file mode 100644 index ff6f595..0000000 Binary files a/Images/FIGHT2.jpeg and /dev/null differ diff --git a/Images/OK.jpeg b/Images/OK.jpeg index 0d1f6f5..b9ebc78 100644 Binary files a/Images/OK.jpeg and b/Images/OK.jpeg differ diff --git a/Images/OK2.jpeg b/Images/OK2.jpeg deleted file mode 100644 index b9ebc78..0000000 Binary files a/Images/OK2.jpeg and /dev/null differ diff --git a/Images/STA.jpg b/Images/STA.jpg new file mode 100644 index 0000000..53d7213 Binary files /dev/null and b/Images/STA.jpg differ diff --git a/Images/START.jpeg b/Images/START.jpeg deleted file mode 100644 index 7058359..0000000 Binary files a/Images/START.jpeg and /dev/null differ diff --git a/Images/START.jpg b/Images/START.jpg new file mode 100644 index 0000000..51a6f6f Binary files /dev/null and b/Images/START.jpg differ diff --git a/README.md b/README.md index 45c997d..70c4d66 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,6 @@ The Dokkan Battle EZA Farming Bot is a Python script designed to automate the process of farming the Extreme Z-Awakening levels in Dragon Ball Z Dokkan Battle. The script runs on your computer and interacts with the game on your phone. -_This bot is no a autoclicker_; it not only completes the levels but also manages the current level of the EZA and switches between different EZAs. - - - If you encounter any errors or have any doubts, you can create an issue or contact me at pcaladomoura@gmail.com. ## Features @@ -20,6 +16,8 @@ If you encounter any errors or have any doubts, you can create an issue or conta - It identifies buttons and actions using images, so it can adapt to different screen sizes and resolutions. - Can swipe bettwen eza and only +***Tip***: If the bot doesn’t seem to be working correctly, particularly if it's failing to detect buttons or actions in the game, try replacing the images in the script with screenshots from your own game setup. These images are used to identify in-game elements, and if your screen layout differs, custom screenshots may improve accuracy. + ## Requirements 1. You need an Android device with USB debugging enabled. This allows the script to communicate with the phone via a USB cable. @@ -65,11 +63,3 @@ This project is intended for educational and personal use only. The use of autom ## Contributing Contributions to this project are welcome! If you encounter any issues, have suggestions for improvements, or want to add new features, feel free to open an issue or submit a pull request. - -## License - -This project is licensed under the [MIT License](LICENSE). - ---- - -Feel free to modify the README according to your project's specific details, and don't forget to add a proper license file (e.g., `LICENSE`) to your project if you plan to share it with others. diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..7be667d --- /dev/null +++ b/debug.py @@ -0,0 +1,32 @@ +from adbutils import adb +from src.image import ImageProcessor +import cv2 +import numpy as np + +device = adb.device() + +image_path = "Images/OK2.jpeg" +image_processor = ImageProcessor() + + +class Debug: + @staticmethod + def draw_rectangle_around_match(screenshot, top_left, w, h): + cv2.rectangle( + screenshot, top_left, (top_left[0] + w, top_left[1] + h), (255, 0, 0), 2 + ) + cv2.imshow("Matched Image", screenshot) + cv2.setWindowProperty("Matched Image", 1, cv2.WINDOW_NORMAL) + cv2.waitKey(0) + cv2.destroyAllWindows() + + +screenshot = np.array(device.screenshot().convert("RGB"), dtype=np.uint8) +template_image = cv2.imread(image_path) + +found, x, y = image_processor.find_image_position(template_image, screenshot, threshold=0.8) + +if found: + Debug.draw_rectangle_around_match(screenshot, (x, y), *template_image.shape[:2]) +else: + print(f"Image {image_path} not found in current screen.") diff --git a/eza_farming_bot.py b/eza_farming_bot.py deleted file mode 100644 index 9d222b4..0000000 --- a/eza_farming_bot.py +++ /dev/null @@ -1,404 +0,0 @@ -from adbutils import adb -from adbutils._device import AdbDevice -from functools import wraps -from PIL import Image -from typing import Callable -import cv2 -import datetime -import numpy as np -import pytesseract -import time - - -def log_error(message): - """Log an error message with a timestamp.""" - with open("error_log.txt", "a") as log_file: - log_file.write(f"{datetime.datetime.now()}: {message}\n") - - -def retry(retries=5, wait=2): - """Decorator to retry a method call with specified retries and wait time.""" - - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - nonlocal retries, wait - attempts = retries - while attempts > 0: - try: - return func(*args, **kwargs) - except Exception as e: - log_error(f"Error during {func.__name__}: {str(e)}") - time.sleep(wait) - attempts -= 1 - return False - - return wrapper - - return decorator - - -def find_image_position(template_image: str, screenshot, threshold=0.8): - # Convert both images to grayscale - target_gray = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) - template_gray = cv2.cvtColor(template_image, cv2.COLOR_BGR2GRAY) - - result = cv2.matchTemplate(target_gray, template_gray, cv2.TM_CCOEFF_NORMED) - - # Get the maximum correlation value and its location - _, max_val, _, max_loc = cv2.minMaxLoc(result) - - if max_val >= threshold: - h, w = template_gray.shape - x, y = max_loc - center_x = x + w // 2 - center_y = y + h // 2 - return True, center_x, center_y - else: - return False, None, None - - -def find_image_in_another(source_image, template, threshold: int = 0.3) -> bool: - result = cv2.matchTemplate(source_image, template, cv2.TM_CCOEFF_NORMED) - _, max_val, _, _ = cv2.minMaxLoc(result) - if max_val >= threshold: - return True - else: - return False - - -# Path to the source image and the template image -PRINT_TEXT = { - "./Images/FIGHT.jpeg": "Click to start Fight", - "./Images/FIGHT2.jpeg": "Click to start Fight", - "./Images/START.jpeg": "Click to confirm battle", - "./Images/End.jpeg": "Battle ends", - "./Images/OK.jpeg": "Click OK button", - "./Images/OK2.jpeg": "Click OK button", - "./Images/CANCEL.jpeg": "CLick in Cancel button", - "./Images/EZA.jpeg": "CLick to select EZA", - "./Images/EXIT.jpeg": "Click to exit EZA", - "./Images/LREZA.jpeg": "CLick to select LR EZA", -} - -import sys - - -def delete_last_line(): - sys.stdout.write("\x1b[1A") - sys.stdout.write("\x1b[2K") - - -class EZA: - @retry(retries=3, wait=1) - def OK2(self, trys=30, raise_error: bool = True): - """Handle OK2 clicks with retries.""" - image_path = "./Images/OK2.jpeg" - if not self._find_and_click(image_path, trys): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - return True - - def handle_friend_request(self): - if not self.OK2(trys=2, raise_error=False): - print("Fallback to generic OK") - self.OK(trys=2, raise_error=False) - - def __init__(self, device: AdbDevice, debug: bool = False) -> None: - self.device = device - self.debug = debug - - @retry(retries=3, wait=1) - def _find_and_click(self, image_path: str, trys=30, wait=1, special=0): - """Attempts to find and click on an image, retrying on failure.""" - for _ in range(trys): - find, x_pos, y_pos = self._find_image_position(image_path) - if find: - print(f"{PRINT_TEXT[image_path]}", end="\r") - self.device.click(x_pos - special, y_pos) - return True - for i in range(1, wait + 1): - print("Waiting loading" + "." * (i % 4)) - delete_last_line() - time.sleep(1) - return False - - def _find(self, image_path: str, trys=30, wait: int = 1): - """Return True if success else False""" - for _ in range(trys): - find, _, _ = self._find_image_position(image_path) - if find: - return True - time.sleep(wait) - return False - - def _find_dual_images(self, image_path_1: str, image_path_2: str, trys=30, wait=1): - """ - Tries to find two images at the same time. - - :param image_path_1: Path to the first image to search for. - :param image_path_2: Path to the second image to search for. - :param trys: Number of attempts to find the images. - :param wait: Time to wait between attempts. - :return: If the first image is found, returns (0, x1, y1). - If the second image is found, returns (1, x2, y2). - If neither image is found, returns None. - """ - for _ in range(trys): - find_1, x1, y1 = self._find_image_position(image_path_1) - find_2, x2, y2 = self._find_image_position(image_path_2) - - if find_1: - return 0, x1, y1 - elif find_2: - return 1, x2, y2 - - for i in range(1, wait + 1): - if image_path_1 == "./Images/End.jpeg": - print("Waiting for the battle to end" + "." * (i % 4)) - else: - print("Waiting loading" + "." * (i % 4)) - delete_last_line() - time.sleep(1) - return None - - def _find_image_position(self, image_path: str): - screenshot = np.array(self.device.screenshot().convert("RGB")) - template_image = cv2.imread(image_path) - return find_image_position(template_image, screenshot) - - @retry(retries=3, wait=1) - def SelectLevel(self, isLREZA: bool, trys=30, raise_error: bool = True): - """Select level with retry logic.""" - image_path = "./Images/EZA.jpeg" if not isLREZA else "./Images/LREZA.jpeg" - if not self._find_and_click(image_path, trys): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - return True - - @retry(retries=3, wait=1) - def ExitLevel(self, trys=30, raise_error: bool = True): - """Exit level with retries on fail.""" - image_path = "./Images/EXIT.jpeg" - if not self._find_and_click(image_path, trys): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - return True - - @retry(retries=3, wait=1) - def Fight(self, trys=30, raise_error: bool = True): - """Initiate a fight, retrying on initial failures.""" - image_path = "./Images/FIGHT2.jpeg" - if not self._find_and_click(image_path, trys): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - return True - - @retry(retries=3, wait=1) - def Start(self, trys=30, raise_error: bool = True): - """Start a level, with retries on failures.""" - image_path = "./Images/START.jpeg" - if not self._find_and_click(image_path, trys): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - return True - - @retry(retries=3, wait=1) - def OK(self, trys=30, raise_error: bool = True): - """Click OK with retries.""" - image_path = "./Images/OK.jpeg" - if not self._find_and_click(image_path, trys): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - return True - - @retry(retries=3, wait=1) - def End(self, trys=45): - """Handle end of battle with retry logic.""" - battle_end = self._find_dual_images( - "./Images/End.jpeg", "./Images/FIGHT2.jpeg", trys, wait=10 - ) - if battle_end == None: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error( - "Neither 'End' nor 'Fight2' template is present in the target image." - ) - elif battle_end[0] == 0: - self.device.click(battle_end[1] - 150, battle_end[2]) - elif battle_end[0] == 1: - return False - return True - - @retry(retries=3, wait=1) - def Cancel(self, trys=30, raise_error: bool = True): - """Attempt to cancel with retry logic.""" - image_path = "./Images/CANCEL.jpeg" - if not self._find_and_click(image_path, trys, wait=5): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - return True - - def click_center_screen(self): - x, y = self.device.window_size() - print("Clicking at the center of the screen.", end="\r") - self.device.click(x / 2, y / 2) - - @retry(retries=3, wait=1) - def get_level(self, zone: int = 1) -> int: - """Get the current level using OCR with retries on fail.""" - screenshot = np.array(self.device.screenshot()) - pil_image = Image.fromarray(screenshot) - if zone == 1: - x1, y1, x2, y2 = 890, 570, 1010, 630 - else: - _, x, y = self._find_image_position("./Images/NEXT.jpeg") - if not x: - x1, y1, x2, y2 = 890, 570, 1010, 630 - else: - x1, y1, x2, y2 = x - 40, y + 40, x + 80, y + 100 - cropped_image = pil_image.crop((x1, y1, x2, y2)) - gray_cropped_image = cropped_image.convert("L") - thresh_image = cv2.threshold( - np.array(gray_cropped_image), - 0, - 255, - cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV, - )[1] - result = pytesseract.image_to_string(thresh_image, config="--psm 7 digits") - try: - level = int(result.split()[0]) - return level - except (IndexError, ValueError): - log_error("Failed to read level from OCR result.") - return 1 # Default to level 1 if OCR fails - - def Swipe(self): - x, y = self.device.window_size() - self.device.swipe( - (x / 2) + 100, (y / 2) + 300, (x / 2) - 100, (y / 2) + 450, 0.5 - ) - - @retry(retries=3, wait=1) - def WaitUntil( - self, - image_path: str, - function: Callable[[], None], - wait: int, - trys=10, - raise_error: bool = True, - ): - """Wait for an image and perform an action, with retry logic.""" - if not self._find(image_path, trys, wait): - if raise_error: - self.device.screenshot().save( - f"ERROR_{datetime.datetime.now().strftime('%H_%M_%S')}.jpeg" - ) - log_error(f"Template {image_path} is not present in the target image.") - return False - function() - return True - - def isLR(self) -> bool: - screenshot = np.array(self.device.screenshot().convert("RGB")) - template_image = cv2.imread("./Images/LREZA.jpeg") - return find_image_in_another(template_image, screenshot, 0.63) - - -import os - - -def start(debug: bool): - device: AdbDevice = adb.device() - eza = EZA(device, debug=debug) - n = 0 - while True: - time.sleep(0.5) - level: int = eza.get_level(2) - maxlevel = 11 if eza.isLR() else 31 - if level < maxlevel: - print("Start eza", end="\r") - eza.SelectLevel(isLREZA=maxlevel == 11) - for _ in range(maxlevel - level): - print(f"Current levels complete: {n}") - print("============================================") - time.sleep(0.5) - eza.Fight() - time.sleep(1) - eza.Start() - time.sleep(1) - if not eza.End(50): - print("Battle lost, change eza") - break - time.sleep(1.5) - eza.OK() - time.sleep(1) - if not eza.Cancel(trys=2, raise_error=False): - eza.handle_friend_request() - time.sleep(1.5) - eza.click_center_screen() - n += 1 - os.system("cls" if os.name == "nt" else "clear") - eza.ExitLevel() - - print("Change EZA") - eza.WaitUntil("./Images/EZA.jpeg", trys=30, function=eza.Swipe, wait=5) - - -def inf(no_lost: bool): - device: AdbDevice = adb.device() - eza = EZA(device) - n = 0 - while True: - print( - f"Current levels complete: {n}\n============================================" - ) - n += 1 - time.sleep(0.5) - - eza.Fight() - time.sleep(1) - eza.Start() - time.sleep(1) - if not eza.End(50): - print("Battle lost") - if not no_lost: - break - continue - time.sleep(1.5) - eza.OK() - time.sleep(1) - if not eza.Cancel(trys=3, raise_error=False): - eza.handle_friend_request() - time.sleep(1) - eza.click_center_screen() - os.system("cls" if os.name == "nt" else "clear") diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..0ea2582 --- /dev/null +++ b/format.sh @@ -0,0 +1 @@ +ruff format \ No newline at end of file diff --git a/main.py b/main.py index a5a0b48..5d2367d 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,11 @@ -import eza_farming_bot +import src.eza_farming_bot as eza_farming_bot import sys if __name__ == "__main__": print("### Program starting ###\n") - if "inf" in sys.argv: no_lost = "--nolost" in sys.argv eza_farming_bot.inf(no_lost=no_lost) else: debug_mode = "debug" in sys.argv - eza_farming_bot.start(debug=debug_mode) diff --git a/messages.json b/messages.json new file mode 100644 index 0000000..ffeac3c --- /dev/null +++ b/messages.json @@ -0,0 +1,11 @@ +{ + "./Images/FIGHT.jpeg": "Click to start Fight", + "./Images/STA.jpg": "Click to start Fight", + "./Images/START.jpg": "Click to confirm battle", + "./Images/END.jpg": "Waiting for the battle to end", + "./Images/OK.jpeg": "Click OK button", + "./Images/CANCEL.jpeg": "CLick in Cancel button", + "./Images/EZA.jpeg": "CLick to select EZA", + "./Images/EXIT.jpeg": "Click to exit EZA", + "./Images/LREZA.jpeg": "CLick to select LR EZA" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0c74932..8eeaa1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ requests==2.32.3 retry==0.9.2 urllib3==2.2.2 xmltodict==0.13.0 +waiting==1.5.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bot.py b/src/bot.py new file mode 100644 index 0000000..7b15c85 --- /dev/null +++ b/src/bot.py @@ -0,0 +1,108 @@ +from adbutils._device import AdbDevice +from datetime import datetime +from .image import ImageProcessor +from waiting import wait as wait_for, TimeoutExpired +import cv2 +import json +import numpy as np +import time +from .utils import ( + log_error, + delete_last_line, +) +with open('messages.json', 'r') as f: + PRINT_TEXT = json.load(f) + + +class Bot: + def __init__(self, device: AdbDevice, debug: bool = False) -> None: + self.device = device + self.debug = debug + self.img_processor = ImageProcessor() + + def get_image_position(self, image_path: str): + screenshot = np.array(self.device.screenshot().convert("RGB")) + template_image = cv2.imread(image_path) + return self.img_processor.find_image_position(template_image, screenshot) + + def _find_and_click(self, image_path: str, trys=30, wait=1, special=0): + """Attempts to find and click on an image, retrying on failure.""" + for _ in range(trys): + find, x_pos, y_pos = self.get_image_position(image_path) + if find: + print(f"{PRINT_TEXT[image_path]}", end="\r") + self.device.click(x_pos - special, y_pos) + return True + for i in range(1, wait + 1): + print("Waiting loading" + "." * (i % 4)) + delete_last_line() + time.sleep(1) + return False + + def _find(self, image_path: str, trys=30, wait: int = 1): + """Return True if success else False""" + for _ in range(trys): + find, _, _ = self.get_image_position(image_path) + if find: + return True + time.sleep(wait) + return False + + def _find_dual_images( + self, image_path1: str, image_path2: str, trys: int = 30, wait: int = 1 + ): + image1 = cv2.imread(image_path1) + image2 = cv2.imread(image_path2) + for _ in range(trys): + screenshot = np.array(self.device.screenshot().convert("RGB"), dtype=np.uint8) + result = self.img_processor.find_dual_images( + image1, image2, screenshot + ) + if result[0] != -1: + return result + time.sleep(wait) + return False + + def _swipe(self): + x, y = self.device.window_size() + self.device.swipe( + (x / 2) + 100, (y / 2) + 300, (x / 2) - 100, (y / 2) + 450, 0.5 + ) + + def _click_center_screen(self): + x, y = self.device.window_size() + print("Clicking at the center of the screen.", end="\r") + self.device.click(x / 2, y / 2) + + def _wait_util( + self, + image_path: str, + wait: int, + trys=10, + raise_error: bool = True, + ): + """Wait for an image to be present and return True if found else False.""" + try: + wait_for(lambda: self._find(image_path, trys, wait), timeout_seconds=10) + except TimeoutExpired: + if raise_error: + self._handle_error(image_path) + return False + return True + + def _handle_error(self, image_path: str) -> None: + """Handle errors by taking a screenshot and logging the issue.""" + self.device.screenshot().save( + f"error/ERROR_{datetime.now().strftime('%H_%M_%S')}.jpeg" + ) + log_error(f"Template {image_path} is not present in the target image.") + + def _perform_action( + self, image_path: str, trys=30, raise_error: bool = True + ) -> bool: + """Perform an action by finding and clicking the image, with retries and error handling.""" + if not self._find_and_click(image_path, trys): + if raise_error: + self._handle_error(image_path) + return False + return True diff --git a/src/custom_types.py b/src/custom_types.py new file mode 100644 index 0000000..1406cf5 --- /dev/null +++ b/src/custom_types.py @@ -0,0 +1,13 @@ +from typing import NamedTuple, Union + + +class ImageLocation(NamedTuple): + found: bool + x: Union[int, None] + y: Union[int, None] + + +class DuelImageLocation(NamedTuple): + which: int + x: Union[int, None] + y: Union[int, None] diff --git a/src/eza_farming_bot.py b/src/eza_farming_bot.py new file mode 100644 index 0000000..a5b1e0f --- /dev/null +++ b/src/eza_farming_bot.py @@ -0,0 +1,158 @@ +from adbutils import adb +from adbutils._device import AdbDevice +from .bot import Bot +from PIL import Image +from .utils import log_error, retry +import cv2 +import datetime +import numpy as np +import os +import time + +class EZA(Bot): + @retry(retries=3) + def SelectLevel(self, isLREZA: bool, raise_error: bool = True): + """Select level with retry logic.""" + image_path = "./Images/EZA.jpeg" if not isLREZA else "./Images/LREZA.jpeg" + self._perform_action(image_path, raise_error=raise_error) + + @retry(retries=3) + def ExitLevel(self, raise_error: bool = True): + """Exit level with retries on fail.""" + self._perform_action("./Images/EXIT.jpeg", raise_error=raise_error) + + @retry(retries=3) + def Fight(self, raise_error: bool = True): + """Initiate a fight, retrying on initial failures.""" + self._perform_action("./Images/STA.jpg", raise_error=raise_error) + + @retry(retries=3) + def Start(self, raise_error: bool = True): + """Start a level, with retries on failures.""" + self._perform_action("./Images/START.jpg", raise_error=raise_error) + + @retry(retries=3) + def OK(self, raise_error: bool = True): + """Click OK with retries.""" + self._perform_action("./Images/OK.jpeg", raise_error=raise_error) + + @retry(retries=2) + def Cancel(self, raise_error: bool = True): + """Attempt to cancel with retry logic.""" + return self._perform_action("./Images/CANCEL.jpeg", raise_error=False) + + @retry(retries=3) + def End(self, raize_error: bool = True): + """Handle end of battle with retry logic.""" + battle_status = self._find_dual_images( + "./Images/END.jpg", "./Images/STA.jpg", wait=10 + ) + if battle_status[0] == -1 and raize_error: + self._handle_error("'END' nor 'STA'") + elif battle_status[0] == 0: + self.device.click(battle_status[1] - 150, battle_status[2]) + elif battle_status[0] == 1: + return False + return True + + @retry(retries=3) + def get_level(self, zone: int = 1) -> int: + """Get the current level using OCR with retries on fail.""" + pil_image = Image.fromarray(np.array(self.device.screenshot())) + + x1, y1, x2, y2 = 890, 570, 1010, 630 + if zone != 1: + _, x, y = self.get_image_position("./Images/NEXT.jpeg") + + if x and y: + x1, y1, x2, y2 = x - 40, y + 40, x + 80, y + 100 + + level = self.img_processor.extract_information( + pil_image, (x1, y1, x2, y2), ocr_config="--psm 7 digits" + ) + try: + return int(level.split()[0]) + except (IndexError, ValueError): + log_error("Failed to read level from OCR result.") + return 1 # + + def isLR(self) -> bool: + """Check if eza stage is LR or not.""" + screenshot = np.array(self.device.screenshot().convert("RGB")) + template_image = cv2.imread("./Images/LREZA.jpeg") + return self.img_processor.find_image_in_another( + template_image, screenshot, 0.63 + ) + + + + +def start(debug: bool): + device: AdbDevice = adb.device() + eza = EZA(device, debug=debug) + n = 0 + while True: + time.sleep(0.5) + level: int = eza.get_level(2) + maxlevel = 11 if eza.isLR() else 31 + if level < maxlevel: + print("Start eza", end="\r") + eza.SelectLevel(isLREZA=maxlevel == 11) + for _ in range(maxlevel - level): + print(f"Current levels complete: {n}") + print("============================================") + time.sleep(0.5) + eza.Fight() + time.sleep(1) + eza.Start() + time.sleep(1) + if not eza.End(): + print("Battle lost, change eza") + break + time.sleep(1.5) + eza.OK() + time.sleep(1) + if not eza.Cancel(raise_error=False): + eza.handle_friend_request() + time.sleep(1.5) + eza._click_center_screen() + n += 1 + os.system("cls" if os.name == "nt" else "clear") + eza.ExitLevel() + + print("Change EZA") + if not eza._wait_util("./Images/EZA.jpeg", wait=5): + print("Could not change EZA") + eza._handle_error("./Images/EZA.jpeg") + break + eza._swipe() + + +def inf(no_lost: bool): + device: AdbDevice = adb.device() + eza = EZA(device) + n = 0 + while True: + print( + f"Current levels complete: {n}\n============================================" + ) + n += 1 + time.sleep(0.5) + eza.Fight() + time.sleep(1) + eza.Start() + time.sleep(1) + print('Waiting battle to end') + if not eza.End(): + print("Battle lost") + if not no_lost: + break + continue + time.sleep(1.5) + eza.OK() + time.sleep(1) + if not eza.Cancel(raise_error=False): + eza.OK() + time.sleep(1) + eza._click_center_screen() + os.system("cls" if os.name == "nt" else "clear") diff --git a/src/image.py b/src/image.py new file mode 100644 index 0000000..95dd7b2 --- /dev/null +++ b/src/image.py @@ -0,0 +1,89 @@ +from .custom_types import ImageLocation, DuelImageLocation +import cv2 +import numpy as np +import pytesseract + + +class ImageProcessor: + def extract_information( + self, image, cords: tuple, ocr_config="--psm 7" + ) -> str | None: + """Processes a PIL image to extract text using Tesseract OCR. + :param Image image: The PIL image to process. + :param tuple cords: A tuple containing coordinates for cropping the image in the format (x1, y1, x2, y2). + :param str ocr_config: (optional) Configuration string for Tesseract OCR. Defaults to "--psm 7". + + :return: The extracted text from the image. Returns a default message if extraction fails. + :rtype: str + """ + cropped_image = image.crop(cords) + gray_cropped_image = cropped_image.convert("L") + + # Apply thresholding for better OCR results + thresh_image = cv2.threshold( + np.array(gray_cropped_image), + 0, + 255, + cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV, + )[1] + # Perform OCR to extract text + try: + result = pytesseract.image_to_string( + thresh_image, config=ocr_config + ).strip() + except Exception as e: + print(e) + return None + return result + + def find_dual_images( + self, image1: str, image2: str, screenshot, threshold=0.8 + ) -> DuelImageLocation: + """Tries to find two images at the same time + :param image_path_1: Path to the first image to search for. + :param image_path_2: Path to the second image to search for. + :return: + - (0, x1, y1) if the first image is found, + - (1, x2, y2) if the second image is found, + - (-1, None, None) if neither image is found after all attempts. + """ + + find_1, x1, y1 = self.find_image_position(image1, screenshot, threshold) + find_2, x2, y2 = self.find_image_position(image2, screenshot, threshold) + if find_1: + return 0, x1, y1 + elif find_2: + return 1, x2, y2 + return -1, None, None + + def is_image_present( + self, image_path: str, search_image_path: str, threshold=0.4 + ) -> bool: + """Find an image within another image using cv2.""" + result = cv2.matchTemplate(image_path, search_image_path, cv2.TM_CCOEFF_NORMED) + _, max_val, _, _ = cv2.minMaxLoc(result) + if max_val >= threshold: + return True + else: + return False + + def find_image_position( + self, image: str, screenshot, threshold=0.8 + ) -> ImageLocation: + """Find the position of an image in a scene.""" + # Convert both images to grayscale + target_gray = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) + template_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + + result = cv2.matchTemplate(target_gray, template_gray, cv2.TM_CCOEFF_NORMED) + + # Get the maximum correlation value and its location + _, max_val, _, max_loc = cv2.minMaxLoc(result) + if max_val >= threshold: + h, w = template_gray.shape + x, y = max_loc + center_x = x + w // 2 + center_y = y + h // 2 + return True, center_x, center_y + else: + return False, None, None diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..ea34af4 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,57 @@ +from datetime import datetime +from functools import wraps +import sys +import time + + +def log_error(message): + """Log an error message with a timestamp.""" + with open("error_log.txt", "a") as log_file: + log_file.write(f"{datetime.now()}: {message}\n") + + +def delete_last_line(): + print ("\033[A \033[A") + + +def retry(retries=5, wait=2): + """Decorator to retry a method call with specified retries and wait time.""" + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + nonlocal retries, wait + attempts = retries + while attempts > 0: + try: + return func(*args, **kwargs) + except Exception as e: + log_error(f"Error during {func.__name__}: {str(e)}") + time.sleep(wait) + attempts -= 1 + return False + + return wrapper + + return decorator + + +""" +def extract_information(self, image_path: str, trys=30, wait=1): + ""Tries to find an image and extract information from it."" + cropped_image = image.crop((x1, y1, x2, y2)) + gray_cropped_image = cropped_image.convert("L") + thresh_image = cv2.threshold( + np.array(gray_cropped_image), + 0, + 255, + cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV, + )[1] + result = pytesseract.image_to_string(thresh_image, config="--psm 7 digits") + try: + level = int(result.split()[0]) + return level + except (IndexError, ValueError): + log_error("Failed to read level from OCR result.") + return 1 +"""