Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dawn of heroes #61

Merged
merged 12 commits into from
Oct 14, 2024
34 changes: 29 additions & 5 deletions alune/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def _sanitize(self):
The user should be notified of any invalid configurations.
"""
self._sanitize_log_level()
self._sanitize_game_mode()
self._sanitize_traits()

def _sanitize_log_level(self):
Expand All @@ -79,23 +80,37 @@ def _sanitize_log_level(self):
logger.warning(f"The configured log level '{log_level}' does not exist. Using INFO instead.")
self._config["log_level"] = "INFO"

def _sanitize_game_mode(self):
"""
Sanitize the user configured game mode by checking against valid values.
"""
game_mode = self._config.get("game_mode", "normal")
if game_mode not in {"normal", "dawn of heroes"}:
logger.warning(f"The configured game mode '{game_mode}' does not exist. Playing 'normal' instead.")
self._config["game_mode"] = "normal"

def _sanitize_traits(self):
"""
Sanitize the user configured traits by checking against currently implemented traits.
"""
current_traits = [trait.name for trait in list(images.Trait)]
if self._config["game_mode"] == "dawn of heroes":
trait_class = images.DawnOfHeroesTrait
else:
trait_class = images.Trait

current_traits = [trait.name for trait in list(trait_class)]
configured_traits = self._config.get("traits", [])

allowed_traits = []
for trait in configured_traits:
if trait.upper() not in current_traits:
logger.warning(f"The configured trait '{trait}' does not exist. Skipping it.")
continue
allowed_traits.append(images.Trait[trait.upper()])
allowed_traits.append(trait_class[trait.upper()])

if len(allowed_traits) == 0:
logger.warning(f"No valid traits were configured. Falling back to {images.Trait.get_default_traits()}.")
allowed_traits = images.Trait.get_default_traits()
logger.warning(f"No valid traits were configured. Falling back to {trait_class.get_default_traits()}.")
allowed_traits = trait_class.get_default_traits()

self._config["traits"] = allowed_traits

Expand Down Expand Up @@ -137,7 +152,7 @@ def should_surrender(self) -> bool:

def get_surrender_delay(self) -> int:
"""
Get the surrender delay
Get the surrender delay.

Returns:
An random Integer between [1 and surrender_random_delay]
Expand All @@ -147,3 +162,12 @@ def get_surrender_delay(self) -> int:
if delay_upper_bound <= 0:
return 0
return _random.randint(1, delay_upper_bound)

def get_game_mode(self) -> str:
"""
Get the game mode we should play.

Returns:
The game mode name.
"""
return self._config["game_mode"]
3 changes: 3 additions & 0 deletions alune/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pathlib import Path
import sys
from time import sleep

from loguru import logger

Expand Down Expand Up @@ -101,4 +102,6 @@ def raise_and_exit(error: str, exit_code: int = 1) -> None:
exit_code: The relative or absolute path to the image to be found. Defaults to 1.
"""
logger.error(error)
logger.warning("Due to an error, we are exiting Alune in 10 seconds. You can find all logs in alune-output/logs.")
sleep(10)
sys.exit(exit_code)
46 changes: 45 additions & 1 deletion alune/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ def _generate_next_value_(name, start, count, last_values):
CAROUSEL = auto()
COLLAPSE_TOP_BAR = auto()
PHASE_3_2_FULL = auto()
NORMAL_GAME = auto()
DAWN_OF_HEROES = auto()


class Trait(StrEnum):
Expand Down Expand Up @@ -190,6 +192,45 @@ def get_default_traits(cls):
WITCHCRAFT = auto()


class DawnOfHeroesTrait(StrEnum):
"""
The same as ImageEnum, but images are intentionally in a different place and will
change with each set.
Specifically for dawn of heroes traits.
"""

# noinspection PyMethodParameters
# pylint: disable-next=no-self-argument,redefined-outer-name
def _generate_next_value_(name, start, count, last_values):
return helpers.get_resource_path(f"alune/images/traits/dawn_of_heroes/{name.lower()}.png")

@classmethod
def get_default_traits(cls):
"""
Gets a list of default traits the bot should use.

Returns:
A list of the traits to be played by default, if the user misconfigures.
"""
return [cls.DAWNBRINGER]

# Dawn of Heroes
ASSASSIN = auto()
BRAWLER = auto()
CANNONEER = auto()
CAVALIER = auto()
DAWNBRINGER = auto()
DRACONIC = auto()
FORGOTTEN = auto()
HELLION = auto()
NIGHTBRINGER = auto()
RANGER = auto()
REDEEMED = auto()
SENTINEL = auto()
SKIRMISHER = auto()
SPELLWEAVER = auto()


class ClickButton: # pylint: disable=too-few-public-methods
"""
A button which can and will be clicked.
Expand Down Expand Up @@ -249,7 +290,6 @@ class Button:
check = ImageButton(BoundingBox(555, 425, 725, 470))
check_surrender = ImageButton(BoundingBox(650, 420, 825, 470))
check_choice = ImageButton(BoundingBox(655, 423, 829, 472))
normal_game = ImageButton(BoundingBox(50, 250, 275, 580))
buy_xp = ImageButton(
click_box=BoundingBox(35, 593, 124, 682),
capture_area=BoundingBox(9, 550, 170, 708),
Expand All @@ -270,6 +310,10 @@ class Button:
click_box=BoundingBox(1155, 595, 1242, 682),
capture_area=BoundingBox(1128, 568, 1269, 709),
)
dawn_of_heroes_continue = ImageButton(
click_box=BoundingBox(968, 618, 1211, 668),
capture_area=BoundingBox(950, 610, 1230, 640),
)

# Buttons without an image.
store_card_one = ClickButton(BoundingBox(180, 47, 363, 272))
Expand Down
Binary file added alune/images/buttons/dawn_of_heroes_continue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/dawn_of_heroes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added alune/images/traits/dawn_of_heroes/assassin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/brawler.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/cannoneer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/cavalier.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/draconic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/forgotten.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/hellion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/ranger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/redeemed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/sentinel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added alune/images/traits/dawn_of_heroes/skirmisher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion alune/resources/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
# Valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
log_level: "INFO"

# The game mode the bot should play.
# Currently valid game modes: normal, dawn of heroes
# Please note that changing this may require changing traits as well.
game_mode: "normal"

# The traits you want the bot to roll for.
# The trait names are almost as written in-game, with spaces replaced by _ and . and : being ignored.
# For example: admin, star_guardian, mecha_prime, lasercorps
# For a full list of valid traits, visit https://github.com/TeamFightTacticsBots/Alune/tree/main/alune/images/traits
# Recommended dawn of heroes traits: dawnbringer
traits:
- witchcraft
- incantor
Expand All @@ -23,6 +29,6 @@ surrender_random_delay: 0

# Changing these below values manually can potentially break the bot, so don't!
# Version of the YAML.
version: 3
version: 5
# Version of the TFT set.
set: 12
109 changes: 99 additions & 10 deletions alune/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import dataclass

import cv2
from cv2.typing import MatLike
from loguru import logger
from numpy import ndarray

Expand Down Expand Up @@ -48,6 +49,52 @@ def get_button_on_screen(
return get_on_screen(image, button.image_path, button.capture_area, precision)


def get_image_from_path(path: str) -> MatLike | None:
"""
Get an image at a path.

Args:
path: The relative or absolute path to the image to be found.

Returns:
The image or none if it does not exist.
"""
image_to_find = cv2.imread(path, 0)

if image_to_find is None:
logger.warning(f"The image {path} does not exist on the system " f"or we do not have permission to read it.")
akshualy marked this conversation as resolved.
Show resolved Hide resolved
return None

return image_to_find


def get_match_template(
image: ndarray,
image_to_find: MatLike,
bounding_box: BoundingBox | None = None,
) -> ndarray:
"""
Searches an image for an image to find with an optional bounding box.

Args:
image: The image we should look at.
image_to_find: The image we should find.
bounding_box: The bounding box to cut the image down to
or none for the full image. Defaults to none.

Returns:
A numpy array with all matched results.
"""
crop = image
if bounding_box:
crop = image[
bounding_box.min_y : bounding_box.max_y,
bounding_box.min_x : bounding_box.max_x,
]

return cv2.matchTemplate(crop, image_to_find, cv2.TM_CCOEFF_NORMED)


def get_on_screen(
image: ndarray,
path: str,
Expand All @@ -67,19 +114,11 @@ def get_on_screen(
Returns:
The position of the image and it's width and height or None if it wasn't found
"""
image_to_find = cv2.imread(path, 0)
image_to_find = get_image_from_path(path)
if image_to_find is None:
logger.warning(f"The image {path} does not exist on the system " f"or we do not have permission to read it.")
return None

crop = image
if bounding_box:
crop = image[
bounding_box.min_y : bounding_box.max_y,
bounding_box.min_x : bounding_box.max_x,
]

search_result = cv2.matchTemplate(crop, image_to_find, cv2.TM_CCOEFF_NORMED)
search_result = get_match_template(image=image, image_to_find=image_to_find, bounding_box=bounding_box)

_, max_precision, _, max_location = cv2.minMaxLoc(search_result)
if max_precision < precision:
Expand All @@ -91,3 +130,53 @@ def get_on_screen(
height=image_to_find.shape[0],
width=image_to_find.shape[1],
)


def get_all_on_screen(
image: ndarray,
path: str,
bounding_box: BoundingBox | None = None,
precision: float = 0.9,
) -> list[ImageSearchResult]:
"""
Check if a given image is detected on screen in a specific window's area.

Args:
image: The image we should look at.
path: The relative or absolute path to the image to be found.
bounding_box: The bounding box to cut the image down to
or none for the full image. Defaults to none.
precision: The precision to be used when matching the image. Defaults to 0.9.

Returns:
All positions of the image and their width and height
"""
image_to_find = get_image_from_path(path)
if image_to_find is None:
return []

search_result = get_match_template(image=image, image_to_find=image_to_find, bounding_box=bounding_box)

to_find_height, to_find_width = image_to_find.shape[:2]
image_search_results = []
max_precision = 1
while max_precision > precision:
_, max_precision, _, max_location = cv2.minMaxLoc(search_result)
if max_precision > precision:
height_from = max_location[1] - to_find_height // 2
height_to = max_location[1] + to_find_height // 2 + 1
width_from = max_location[0] - to_find_width // 2
width_to = max_location[0] + to_find_width // 2 + 1
# Override the best result with empty pixels.
# Not doing this would result in the same location being matched multiple times across its width and height.
search_result[height_from:height_to, width_from:width_to] = 0
image_search_results.append(
ImageSearchResult(
x=max_location[0] + (bounding_box.min_x if bounding_box else 0),
y=max_location[1] + (bounding_box.min_y if bounding_box else 0),
height=to_find_height,
width=to_find_width,
)
)

return image_search_results
Loading