From a2ecaf3af4d02e55d0f48d6a40011066e0a302de Mon Sep 17 00:00:00 2001 From: peter Date: Wed, 19 Jun 2024 23:50:10 +0100 Subject: [PATCH] Clean up action class --- clashroyalebuildabot/__init__.py | 2 - clashroyalebuildabot/bot/__init__.py | 2 - clashroyalebuildabot/bot/action.py | 12 +- clashroyalebuildabot/bot/bot.py | 21 ++-- .../bot/example/custom_action.py | 20 +-- clashroyalebuildabot/bot/pete/__init__.py | 4 - clashroyalebuildabot/bot/pete/pete_action.py | 116 ------------------ clashroyalebuildabot/bot/pete/pete_bot.py | 60 --------- .../two_six_hog_cycle_action.py | 20 +-- clashroyalebuildabot/state/card_detector.py | 17 +-- clashroyalebuildabot/state/detector.py | 4 +- 11 files changed, 44 insertions(+), 234 deletions(-) delete mode 100644 clashroyalebuildabot/bot/pete/__init__.py delete mode 100644 clashroyalebuildabot/bot/pete/pete_action.py delete mode 100644 clashroyalebuildabot/bot/pete/pete_bot.py diff --git a/clashroyalebuildabot/__init__.py b/clashroyalebuildabot/__init__.py index 773e526..6c29d12 100644 --- a/clashroyalebuildabot/__init__.py +++ b/clashroyalebuildabot/__init__.py @@ -2,7 +2,6 @@ from . import constants from .bot import Action from .bot import Bot -from .bot import PeteBot from .bot import RandomBot from .bot import TwoSixHogCycle from .namespaces import Cards @@ -17,7 +16,6 @@ __all__ = [ "RandomBot", - "PeteBot", "TwoSixHogCycle", "constants", "Cards", diff --git a/clashroyalebuildabot/bot/__init__.py b/clashroyalebuildabot/bot/__init__.py index 24c5b97..2f5fa5e 100644 --- a/clashroyalebuildabot/bot/__init__.py +++ b/clashroyalebuildabot/bot/__init__.py @@ -1,13 +1,11 @@ # Exports for bot submodule from .action import Action from .bot import Bot -from .pete import PeteBot from .random import RandomBot from .two_six_hog_cycle import TwoSixHogCycle __all__ = [ "TwoSixHogCycle", - "PeteBot", "RandomBot", "Action", "Bot", diff --git a/clashroyalebuildabot/bot/action.py b/clashroyalebuildabot/bot/action.py index 1723a03..f6f63dd 100644 --- a/clashroyalebuildabot/bot/action.py +++ b/clashroyalebuildabot/bot/action.py @@ -1,15 +1,9 @@ class Action: - def __init__( - self, index, tile_x, tile_y, name, cost, type_, target, ready - ): + def __init__(self, index, tile_x, tile_y, card): self.index = index self.tile_x = tile_x self.tile_y = tile_y - self.name = name - self.cost = cost - self.type = type_ - self.target = target - self.ready = ready + self.card = card def __repr__(self): - return f"{self.name} at ({self.tile_x}, {self.tile_y})" + return f"{self.card.name} at ({self.tile_x}, {self.tile_y})" diff --git a/clashroyalebuildabot/bot/bot.py b/clashroyalebuildabot/bot/bot.py index 00e47e3..c8792a2 100644 --- a/clashroyalebuildabot/bot/bot.py +++ b/clashroyalebuildabot/bot/bot.py @@ -71,20 +71,15 @@ def get_actions(self): all_tiles = ALLY_TILES + LEFT_PRINCESS_TILES + RIGHT_PRINCESS_TILES valid_tiles = self._get_valid_tiles() actions = [] - for i in range(4): + for i in self.state["ready"]: card = self.state["cards"][i + 1] - if ( - int(self.state["numbers"]["elixir"]["number"]) >= card["cost"] - and card["ready"] - and card["name"] != "blank" - ): - tiles = all_tiles if card["target_anywhere"] else valid_tiles - actions.extend( - [ - self.action_class(i, x, y, *card.values()) - for (x, y) in tiles - ] - ) + if int(self.state["numbers"]["elixir"]["number"]) < card.cost: + continue + + tiles = all_tiles if card.target_anywhere else valid_tiles + actions.extend( + [self.action_class(i, x, y, card) for (x, y) in tiles] + ) return actions diff --git a/clashroyalebuildabot/bot/example/custom_action.py b/clashroyalebuildabot/bot/example/custom_action.py index 9891974..486459e 100644 --- a/clashroyalebuildabot/bot/example/custom_action.py +++ b/clashroyalebuildabot/bot/example/custom_action.py @@ -133,16 +133,16 @@ def _calculate_musketeer_score(self, state): return [0] def calculate_score(self, state): - name_to_score = { - Cards.KNIGHT.name: self._calculate_knight_score, - Cards.MINIONS.name: self._calculate_minions_score, - Cards.FIREBALL.name: self._calculate_fireball_score, - Cards.GIANT.name: self._calculate_giant_score, - Cards.MINIPEKKA.name: self._calculate_minipekka_score, - Cards.MUSKETEER.name: self._calculate_musketeer_score, - Cards.ARROWS.name: self._calculate_arrows_score, - Cards.ARCHERS.name: self._calculate_archers_score, + card_to_score = { + Cards.KNIGHT: self._calculate_knight_score, + Cards.MINIONS: self._calculate_minions_score, + Cards.FIREBALL: self._calculate_fireball_score, + Cards.GIANT: self._calculate_giant_score, + Cards.MINIPEKKA: self._calculate_minipekka_score, + Cards.MUSKETEER: self._calculate_musketeer_score, + Cards.ARROWS: self._calculate_arrows_score, + Cards.ARCHERS: self._calculate_archers_score, } - score_function = name_to_score.get(self.name) + score_function = card_to_score.get(self.card) self.score = score_function(state) if score_function else [0] return self.score diff --git a/clashroyalebuildabot/bot/pete/__init__.py b/clashroyalebuildabot/bot/pete/__init__.py deleted file mode 100644 index e9b159c..0000000 --- a/clashroyalebuildabot/bot/pete/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Exports for bot.pete submodule -from .pete_bot import PeteBot - -__all__ = ["PeteBot"] diff --git a/clashroyalebuildabot/bot/pete/pete_action.py b/clashroyalebuildabot/bot/pete/pete_action.py deleted file mode 100644 index 7d965ae..0000000 --- a/clashroyalebuildabot/bot/pete/pete_action.py +++ /dev/null @@ -1,116 +0,0 @@ -from clashroyalebuildabot.bot.action import Action -from clashroyalebuildabot.namespaces.cards import Cards - - -class PeteAction(Action): - RADII = {Cards.FIREBALL: 2.5, Cards.ARROWS: 4} - score = None - - @staticmethod - def _distance(x1, y1, x2, y2): - return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5 - - def _calculate_building_score(self, units): - score = [0, 0, 0] - - n_enemies = sum(len(v) for v in units["enemy"].values()) - - # Play defensively if the enemy has a unit in our half - if n_enemies != 0: - rhs = 0 - lhs = 0 - for v in units["enemy"].values(): - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - if tile_x > 8 and tile_y <= 17: - rhs += 1 - elif tile_x <= 8 and tile_y <= 17: - lhs += 1 - - if rhs > lhs: - score[0] = int(self.tile_x > 8) - else: - score[0] = int(self.tile_x <= 8) - score[1] = -self._distance(self.tile_x, self.tile_y, 8.5, 11) - - return score - - def _calculate_troop_score(self, units): - """ - Calculate the score for a troop card - - The score is defined as [A, B, C] - A is - 1 if the troop will 'attack' or 'defend' - 0 if the troop ignores 'defence' - 0.5 otherwise - B is the negative distance of the troop to the centre - """ - score = [0.5, 0, 0] - - # Play aggressively if the enemy has no units - n_enemies = sum(len(v) for k, v in units["enemy"].items()) - if self.target == "buildings" and n_enemies == 0: - score[0] = 1 - - # Play defensively if the enemy has a unit in our half - elif n_enemies != 0: - rhs = 0 - lhs = 0 - for k, v in units["enemy"].items(): - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - if tile_x > 8 and tile_y <= 17: - rhs += 1 - elif tile_x <= 8 and tile_y <= 17: - lhs += 1 - - if rhs > lhs: - score[0] = int(self.tile_x > 8) - else: - score[0] = int(self.tile_x <= 8) - score[1] = -self._distance(self.tile_x, self.tile_y, 8.5, 11) - - return score - - def _calculate_spell_score(self, units): - """ - Calculate the score for a spell card (either fireball or arrows) - - The score is defined as [A, B, C] - A is 2 if we'll hit 3 or more units, 0 otherwise - B is the number of units we hit - C is the negative distance to the furthest unit - """ - score = [0, 0, 0] - for v in units["enemy"].values(): - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - # Assume the unit will move down a space - tile_y -= 1 - - # Add 1 to the score if the spell will hit the unit - distance = self._distance( - tile_x, tile_y, self.tile_x, self.tile_y - ) - if distance <= self.RADII[self.name] - 1: - score[1] += 1 - score[2] = min(score[2], -distance) - - # Set score[0] to 2 if we think we'll hit 3 or more units - if score[1] >= 3: - score[0] = 2 - - return score - - def calculate_score(self, units): - if self.type == "spell": - score = self._calculate_spell_score(units) - elif self.type == "troop": - score = self._calculate_troop_score(units) - elif self.type == "building": - score = self._calculate_building_score(units) - else: - raise ValueError(f"Scoring for type {self.type} is not supported") - self.score = score - return score diff --git a/clashroyalebuildabot/bot/pete/pete_bot.py b/clashroyalebuildabot/bot/pete/pete_bot.py deleted file mode 100644 index 76a985d..0000000 --- a/clashroyalebuildabot/bot/pete/pete_bot.py +++ /dev/null @@ -1,60 +0,0 @@ -import random -import time - -from loguru import logger - -from clashroyalebuildabot.bot.bot import Bot -from clashroyalebuildabot.bot.pete.pete_action import PeteAction -from clashroyalebuildabot.constants import DISPLAY_HEIGHT -from clashroyalebuildabot.constants import DISPLAY_WIDTH -from clashroyalebuildabot.constants import SCREENSHOT_HEIGHT -from clashroyalebuildabot.constants import SCREENSHOT_WIDTH - - -class PeteBot(Bot): - def __init__(self, cards, debug=False): - super().__init__(cards, PeteAction, debug=debug) - - def _preprocess(self): - """ - Perform preprocessing on the state - - Estimate the tile of each unit to be the bottom of their bounding box - """ - for side in ["ally", "enemy"]: - for v in self.state["units"][side].values(): - for unit in v["positions"]: - bbox = unit["bounding_box"] - bbox[0] *= DISPLAY_WIDTH / SCREENSHOT_WIDTH - bbox[1] *= DISPLAY_HEIGHT / SCREENSHOT_HEIGHT - bbox[2] *= DISPLAY_WIDTH / SCREENSHOT_WIDTH - bbox[3] *= DISPLAY_HEIGHT / SCREENSHOT_HEIGHT - bbox_bottom = [((bbox[0] + bbox[2]) / 2), bbox[3]] - unit["tile_xy"] = self._get_nearest_tile(*bbox_bottom) - - def run(self): - while True: - # Set the state of the game - self.set_state() - # Obtain a list of playable actions - actions = self.get_actions() - if actions: - # Shuffle the actions (because action scores might be the same) - random.shuffle(actions) - # Preprocessing - self._preprocess() - # Get the best action - action = max( - actions, - key=lambda x: x.calculate_score(self.state["units"]), - ) - # Skip the action if it doesn't score high enough - if action.score[0] == 0: - continue - # Play the best action - self.play_action(action) - # Log the result - logger.info( - f"Playing {action} with score {action.score} and sleeping for 1 second" - ) - time.sleep(1.0) diff --git a/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_action.py b/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_action.py index 903a7c1..95144de 100644 --- a/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_action.py +++ b/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_action.py @@ -180,18 +180,18 @@ def _calculate_fireball_score(self, state): ) def calculate_score(self, state): - name_to_score = { - Cards.HOG_RIDER.name: self._calculate_hog_rider_score, - Cards.ICE_GOLEM.name: self._calculate_ice_golem_score, - Cards.FIREBALL.name: self._calculate_fireball_score, - Cards.ICE_SPIRIT.name: self._calculate_ice_spirit_score, - Cards.THE_LOG.name: self._calculate_log_score, - Cards.MUSKETEER.name: self._calculate_musketeer_score, - Cards.CANNON.name: self._calculate_cannon_score, - Cards.SKELETONS.name: self._calculate_ice_spirit_score, + card_to_score = { + Cards.HOG_RIDER: self._calculate_hog_rider_score, + Cards.ICE_GOLEM: self._calculate_ice_golem_score, + Cards.FIREBALL: self._calculate_fireball_score, + Cards.ICE_SPIRIT: self._calculate_ice_spirit_score, + Cards.THE_LOG: self._calculate_log_score, + Cards.MUSKETEER: self._calculate_musketeer_score, + Cards.CANNON: self._calculate_cannon_score, + Cards.SKELETONS: self._calculate_ice_spirit_score, } - score_function = name_to_score[self.name] + score_function = card_to_score[self.card] score = score_function(state) self.score = score return score diff --git a/clashroyalebuildabot/state/card_detector.py b/clashroyalebuildabot/state/card_detector.py index 522c350..b3b1d64 100644 --- a/clashroyalebuildabot/state/card_detector.py +++ b/clashroyalebuildabot/state/card_detector.py @@ -73,16 +73,19 @@ def _detect_cards(self, image): np.amin(np.abs(crop_hashes - self.card_hashes), axis=1), axis=1 ).T _, idx = linear_sum_assignment(hash_diffs) - cards = [self.cards[i].__dict__ for i in idx] + cards = [self.cards[i] for i in idx] + return cards, crops - def _detect_if_ready(self, cards, crops): - for card, crop in zip(cards, crops): + def _detect_if_ready(self, crops): + ready = [] + for i, crop in enumerate(crops[1:]): std = np.mean(np.std(np.array(crop), axis=2)) - card["ready"] = std > self.grey_std_threshold - return cards + if std > self.grey_std_threshold: + ready.append(i) + return ready def run(self, image): cards, crops = self._detect_cards(image) - cards = self._detect_if_ready(cards, crops) - return cards + ready = self._detect_if_ready(crops) + return cards, ready diff --git a/clashroyalebuildabot/state/detector.py b/clashroyalebuildabot/state/detector.py index 077d14c..68e9a35 100644 --- a/clashroyalebuildabot/state/detector.py +++ b/clashroyalebuildabot/state/detector.py @@ -34,10 +34,12 @@ def __init__(self, cards, debug=False): self.debugger = Debugger() def run(self, image): + cards, ready = self.card_detector.run(image) state = { "units": self.unit_detector.run(image), "numbers": self.number_detector.run(image), - "cards": self.card_detector.run(image), + "cards": cards, + "ready": ready, "screen": self.screen_detector.run(image), }