diff --git a/clashroyalebuildabot/actions/archers_action.py b/clashroyalebuildabot/actions/archers_action.py index 48f7429..357a09c 100644 --- a/clashroyalebuildabot/actions/archers_action.py +++ b/clashroyalebuildabot/actions/archers_action.py @@ -7,10 +7,9 @@ class ArchersAction(Action): def calculate_score(self, state): score = [0.5] if state.numbers["elixir"]["number"] == 10 else [0] - for v in state.enemies.values(): - for position in v["positions"]: - lhs = position.tile_x <= 8 and self.tile_x == 7 - rhs = position.tile_x > 8 and self.tile_x == 10 - if self.tile_y < position.tile_y <= 14 and (lhs or rhs): - score = [1, self.tile_y - position.tile_y] + for det in state.enemies: + lhs = det.position.tile_x <= 8 and self.tile_x == 7 + rhs = det.position.tile_x > 8 and self.tile_x == 10 + if self.tile_y < det.position.tile_y <= 14 and (lhs or rhs): + score = [1, self.tile_y - det.position.tile_y] return score diff --git a/clashroyalebuildabot/actions/arrows_action.py b/clashroyalebuildabot/actions/arrows_action.py index ae7689d..4c34d4d 100644 --- a/clashroyalebuildabot/actions/arrows_action.py +++ b/clashroyalebuildabot/actions/arrows_action.py @@ -5,4 +5,3 @@ class ArrowsAction(SpellAction): CARD = Cards.ARROWS RADIUS = 4 - MIN_TO_HIT = 5 diff --git a/clashroyalebuildabot/actions/bats_action.py b/clashroyalebuildabot/actions/bats_action.py new file mode 100644 index 0000000..956cf44 --- /dev/null +++ b/clashroyalebuildabot/actions/bats_action.py @@ -0,0 +1,6 @@ +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.overhead_action import OverheadAction + + +class BatsAction(OverheadAction): + CARD = Cards.BATS diff --git a/clashroyalebuildabot/actions/fireball_action.py b/clashroyalebuildabot/actions/fireball_action.py index 2f20077..c64135e 100644 --- a/clashroyalebuildabot/actions/fireball_action.py +++ b/clashroyalebuildabot/actions/fireball_action.py @@ -5,4 +5,3 @@ class FireballAction(SpellAction): CARD = Cards.FIREBALL RADIUS = 2.5 - MIN_TO_HIT = 3 diff --git a/clashroyalebuildabot/actions/knight_action.py b/clashroyalebuildabot/actions/knight_action.py index 54dd55a..cab6840 100644 --- a/clashroyalebuildabot/actions/knight_action.py +++ b/clashroyalebuildabot/actions/knight_action.py @@ -7,11 +7,10 @@ class KnightAction(Action): def calculate_score(self, state): score = [0.5] if state.numbers["elixir"]["number"] == 10 else [0] - for v in state.enemies.values(): - for position in v["positions"]: - lhs = position.tile_x <= 8 and self.tile_x == 8 - rhs = position.tile_x > 8 and self.tile_x == 9 + for det in state.enemies: + lhs = det.position.tile_x <= 8 and self.tile_x == 8 + rhs = det.position.tile_x > 8 and self.tile_x == 9 - if self.tile_y < position.tile_y <= 14 and (lhs or rhs): - score = [1, self.tile_y - position.tile_y] + if self.tile_y < det.position.tile_y <= 14 and (lhs or rhs): + score = [1, self.tile_y - det.position.tile_y] return score diff --git a/clashroyalebuildabot/actions/minions_action.py b/clashroyalebuildabot/actions/minions_action.py index 2e8c352..0b2e9c3 100644 --- a/clashroyalebuildabot/actions/minions_action.py +++ b/clashroyalebuildabot/actions/minions_action.py @@ -1,20 +1,6 @@ -import math - from clashroyalebuildabot import Cards -from clashroyalebuildabot.actions.action import Action +from clashroyalebuildabot.actions.overhead_action import OverheadAction -class MinionsAction(Action): +class MinionsAction(OverheadAction): CARD = Cards.MINIONS - - def calculate_score(self, state): - score = [0.5] if state.numbers["elixir"]["number"] == 10 else [0] - for v in state.enemies.values(): - for position in v["positions"]: - distance = math.hypot( - position.tile_x - self.tile_x, - position.tile_y - self.tile_y, - ) - if distance < 1: - score = [1, -distance] - return score diff --git a/clashroyalebuildabot/actions/musketeer_action.py b/clashroyalebuildabot/actions/musketeer_action.py index 9f29bf7..26d0ba8 100644 --- a/clashroyalebuildabot/actions/musketeer_action.py +++ b/clashroyalebuildabot/actions/musketeer_action.py @@ -8,14 +8,13 @@ class MusketeerAction(Action): CARD = Cards.MUSKETEER def calculate_score(self, state): - for v in state.enemies.values(): - for position in v["positions"]: - distance = math.hypot( - position.tile_x - self.tile_x, - position.tile_y - self.tile_y, - ) - if 5 < distance < 6: - return [1] - if distance < 5: - return [0] + for det in state.enemies: + distance = math.hypot( + det.position.tile_x - self.tile_x, + det.position.tile_y - self.tile_y, + ) + if 5 < distance < 6: + return [1] + if distance < 5: + return [0] return [0] diff --git a/clashroyalebuildabot/actions/overhead_action.py b/clashroyalebuildabot/actions/overhead_action.py new file mode 100644 index 0000000..37f00e4 --- /dev/null +++ b/clashroyalebuildabot/actions/overhead_action.py @@ -0,0 +1,20 @@ +import math + +from clashroyalebuildabot.actions.action import Action + + +class OverheadAction(Action): + """ + Play the card directly on top of enemy units + """ + + def calculate_score(self, state): + score = [0.5] if state.numbers["elixir"]["number"] == 10 else [0] + for det in state.enemies: + distance = math.hypot( + det.position.tile_x - self.tile_x, + det.position.tile_y - self.tile_y, + ) + if distance < 1: + score = [1, -distance] + return score diff --git a/clashroyalebuildabot/actions/spell_action.py b/clashroyalebuildabot/actions/spell_action.py index 78b17c2..01ed29f 100644 --- a/clashroyalebuildabot/actions/spell_action.py +++ b/clashroyalebuildabot/actions/spell_action.py @@ -1,27 +1,28 @@ import math from clashroyalebuildabot.actions.action import Action +from clashroyalebuildabot.namespaces.units import Units class SpellAction(Action): RADIUS = None - MIN_TO_HIT = None + MIN_SCORE = 5 + UNIT_TO_SCORE = {Units.SKELETON: 1} def calculate_score(self, state): - hit_units = 0 + hit_score = 0 max_distance = float("inf") - for v in state.enemies.values(): - for position in v["positions"]: - distance = math.hypot( - self.tile_x - position.tile_x, - self.tile_y - position.tile_y + 2, - ) - if distance <= self.RADIUS - 1: - hit_units += 1 - max_distance = min(max_distance, -distance) + for det in state.enemies: + distance = math.hypot( + self.tile_x - det.position.tile_x, + self.tile_y - det.position.tile_y + 2, + ) + if distance <= self.RADIUS - 1: + hit_score += self.UNIT_TO_SCORE.get(det.unit, 2) + max_distance = min(max_distance, -distance) return [ - 1 if hit_units >= self.MIN_TO_HIT else 0, - hit_units, + 1 if hit_score >= self.MIN_SCORE else 0, + hit_score, max_distance, ] diff --git a/clashroyalebuildabot/actions/zap_action.py b/clashroyalebuildabot/actions/zap_action.py index b700d92..9c4091b 100644 --- a/clashroyalebuildabot/actions/zap_action.py +++ b/clashroyalebuildabot/actions/zap_action.py @@ -5,4 +5,3 @@ class ZapAction(SpellAction): CARD = Cards.ZAP RADIUS = 2.5 - MIN_TO_HIT = 3 diff --git a/clashroyalebuildabot/bot/bot.py b/clashroyalebuildabot/bot/bot.py index 0ff74d4..fde07ad 100644 --- a/clashroyalebuildabot/bot/bot.py +++ b/clashroyalebuildabot/bot/bot.py @@ -196,6 +196,6 @@ def run(self): try: while True: self.step() - except (KeyboardInterrupt, Exception): - self.emulator.quit() + except KeyboardInterrupt: logger.info("Thanks for using CRBAB, see you next time!") + self.emulator.quit() diff --git a/clashroyalebuildabot/config.yaml b/clashroyalebuildabot/config.yaml index bd24a6e..5feed18 100644 --- a/clashroyalebuildabot/config.yaml +++ b/clashroyalebuildabot/config.yaml @@ -2,7 +2,7 @@ bot: # The logging level for the bot. # Use "DEBUG" for detailed debugging information, "INFO" for general information, # "WARNING" for warning messages, "ERROR" for error messages, and "CRITICAL" for critical issues. - log_level: "INFO" + log_level: "DEBUG" adb: # The IP address of your device or emulator. diff --git a/clashroyalebuildabot/detectors/detector.py b/clashroyalebuildabot/detectors/detector.py index ac999f6..085076b 100644 --- a/clashroyalebuildabot/detectors/detector.py +++ b/clashroyalebuildabot/detectors/detector.py @@ -1,5 +1,7 @@ import os +from loguru import logger + from clashroyalebuildabot.constants import MODELS_DIR from clashroyalebuildabot.debugger import Debugger from clashroyalebuildabot.detectors.card_detector import CardDetector @@ -35,14 +37,13 @@ def __init__(self, cards, debug=False): self.debugger = Debugger() def run(self, image): + logger.debug("Setting state...") cards, ready = self.card_detector.run(image) - units = self.unit_detector.run(image) + allies, enemies = self.unit_detector.run(image) numbers = self.number_detector.run(image) screen = self.screen_detector.run(image) - state = State( - units["enemy"], units["ally"], numbers, cards, ready, screen - ) + state = State(allies, enemies, numbers, cards, ready, screen) if self.debugger is not None: self.debugger.run(image, state) diff --git a/clashroyalebuildabot/detectors/unit_detector.py b/clashroyalebuildabot/detectors/unit_detector.py index 70a3cf2..601bdf7 100644 --- a/clashroyalebuildabot/detectors/unit_detector.py +++ b/clashroyalebuildabot/detectors/unit_detector.py @@ -14,8 +14,8 @@ from clashroyalebuildabot.constants import TILE_WIDTH from clashroyalebuildabot.detectors.onnx_detector import OnnxDetector from clashroyalebuildabot.detectors.side_detector import SideDetector -from clashroyalebuildabot.namespaces.state import Position -from clashroyalebuildabot.namespaces.units import Unit +from clashroyalebuildabot.namespaces.units import Position +from clashroyalebuildabot.namespaces.units import UnitDetection class UnitDetector(OnnxDetector): @@ -47,8 +47,7 @@ def _get_possible_ally_names(self): for card in self.cards: if card.units is None: continue - for unit_ in card.units: - unit = Unit(*unit_) + for unit in card.units: possible_ally_names.add(unit.name) return possible_ally_names @@ -76,24 +75,24 @@ def _preprocess(self, image): def _post_process(self, pred, height, image): pred[:, [1, 3]] *= self.UNIT_Y_END - self.UNIT_Y_START pred[:, [1, 3]] += self.UNIT_Y_START * height - clean_pred = {"ally": {}, "enemy": {}} + + allies = [] + enemies = [] for p in pred: l, t, r, b, conf, cls = p bbox = (round(l), round(t), round(r), round(b)) tile_x, tile_y = self._get_tile_xy(bbox) position = Position(bbox, conf, tile_x, tile_y) - name, category, target, transport = DETECTOR_UNITS[int(cls)] - side = self._calculate_side(image, bbox, name) - if name not in clean_pred[side]: - clean_pred[side][name] = { - "type": category, - "target": target, - "transport": transport, - "positions": [], - } + unit = DETECTOR_UNITS[int(cls)] + unit_detection = UnitDetection(unit, position) + + side = self._calculate_side(image, bbox, unit.name) + if side == "ally": + allies.append(unit_detection) + else: + enemies.append(unit_detection) - clean_pred[side][name]["positions"].append(position) - return clean_pred + return allies, enemies def run(self, image): height, width = image.height, image.width @@ -101,5 +100,5 @@ def run(self, image): pred = self._infer(np_image)[0] pred = pred[pred[:, 4] > self.MIN_CONF] pred = self.fix_bboxes(pred, width, height, padding) - pred = self._post_process(pred, height, image) - return pred + allies, enemies = self._post_process(pred, height, image) + return allies, enemies diff --git a/clashroyalebuildabot/namespaces/state.py b/clashroyalebuildabot/namespaces/state.py index b145cc3..4be83a0 100644 --- a/clashroyalebuildabot/namespaces/state.py +++ b/clashroyalebuildabot/namespaces/state.py @@ -3,21 +3,14 @@ from clashroyalebuildabot.namespaces.cards import Card from clashroyalebuildabot.namespaces.screens import Screen +from clashroyalebuildabot.namespaces.units import UnitDetection @dataclass class State: - enemies: Any - allies: Any + allies: List[UnitDetection] + enemies: List[UnitDetection] numbers: Any cards: Tuple[Card, Card, Card, Card] ready: List[int] screen: Screen - - -@dataclass -class Position: - bbox: Tuple[int, int, int, int] - conf: float - tile_x: int - tile_y: int diff --git a/clashroyalebuildabot/namespaces/units.py b/clashroyalebuildabot/namespaces/units.py index 54323cf..4668026 100644 --- a/clashroyalebuildabot/namespaces/units.py +++ b/clashroyalebuildabot/namespaces/units.py @@ -1,6 +1,6 @@ from dataclasses import asdict from dataclasses import dataclass -from typing import Literal, Optional +from typing import Literal, Optional, Tuple @dataclass(frozen=True) @@ -27,41 +27,55 @@ class Unit: transport: Optional[Literal[Transport.AIR, Transport.GROUND]] +@dataclass(frozen=True) +class Position: + bbox: Tuple[int, int, int, int] + conf: float + tile_x: int + tile_y: int + + +@dataclass(frozen=True) +class UnitDetection: + unit: Unit + position: Position + + @dataclass(frozen=True) class _UnitsNamespace: - ARCHER: Unit = ("archer", "troop", "both", "ground") - BARBARIAN: Unit = ("barbarian", "troop", "ground", "ground") - BARBARIAN_HUT: Unit = ("barbarian_hut", "building", None, None) - BOMBER: Unit = ("bomber", "troop", "ground", "ground") - BOMB_TOWER: Unit = ("bomb_tower", "building", "ground", "ground") - BRAWLER: Unit = ("brawler", "troop", "ground", "ground") - CANNON: Unit = ("cannon", "building", "ground", "ground") - DARK_PRINCE: Unit = ("dark_prince", "troop", "ground", "ground") - ELIXIR_COLLECTOR: Unit = ("elixir_collector", "building", None, None) - FURNACE: Unit = ("furnace", "building", None, None) - GIANT: Unit = ("giant", "troop", "ground", "ground") - GOBLIN: Unit = ("goblin", "troop", "ground", "ground") - GOBLIN_CAGE: Unit = ("goblin_cage", "building", None, None) - GOBLIN_HUT: Unit = ("goblin_hut", "building", None, None) - HUNGRY_DRAGON: Unit = ("hungry_dragon", "troop", "all", "air") - HUNTER: Unit = ("hunter", "troop", "all", "ground") - ICE_GOLEM: Unit = ("ice_golem", "troop", "buildings", "ground") - ICE_SPIRIT: Unit = ("ice_spirit", "troop", "all", "ground") - INFERNO_TOWER: Unit = ("inferno_tower", "building", None, None) - KNIGHT: Unit = ("knight", "troop", "ground", "ground") - MINION: Unit = ("minion", "troop", "both", "air") - MINIPEKKA: Unit = ("minipekka", "troop", "ground", "ground") - MORTAR: Unit = ("mortar", "building", "ground", "ground") - MUSKETEER: Unit = ("musketeer", "troop", "both", "ground") - PRINCE: Unit = ("prince", "troop", "ground", "ground") - ROYAL_HOG: Unit = ("royal_hog", "troop", "buildings", "ground") - SKELETON: Unit = ("skeleton", "troop", "ground", "ground") - SPEAR_GOBLIN: Unit = ("spear_goblin", "troop", "both", "ground") - TESLA: Unit = ("tesla", "building", "both", "ground") - TOMBSTONE: Unit = ("tombstone", "building", None, None) - VALKYRIE: Unit = ("valkyrie", "troop", "ground", "ground") - WALL_BREAKER: Unit = ("wall_breaker", "troop", "buildings", "ground") - X_BOW: Unit = ("x_bow", "building", "ground", "ground") + ARCHER: Unit = Unit("archer", "troop", "both", "ground") + BARBARIAN: Unit = Unit("barbarian", "troop", "ground", "ground") + BARBARIAN_HUT: Unit = Unit("barbarian_hut", "building", None, None) + BOMBER: Unit = Unit("bomber", "troop", "ground", "ground") + BOMB_TOWER: Unit = Unit("bomb_tower", "building", "ground", "ground") + BRAWLER: Unit = Unit("brawler", "troop", "ground", "ground") + CANNON: Unit = Unit("cannon", "building", "ground", "ground") + DARK_PRINCE: Unit = Unit("dark_prince", "troop", "ground", "ground") + ELIXIR_COLLECTOR: Unit = Unit("elixir_collector", "building", None, None) + FURNACE: Unit = Unit("furnace", "building", None, None) + GIANT: Unit = Unit("giant", "troop", "ground", "ground") + GOBLIN: Unit = Unit("goblin", "troop", "ground", "ground") + GOBLIN_CAGE: Unit = Unit("goblin_cage", "building", None, None) + GOBLIN_HUT: Unit = Unit("goblin_hut", "building", None, None) + HUNGRY_DRAGON: Unit = Unit("hungry_dragon", "troop", "all", "air") + HUNTER: Unit = Unit("hunter", "troop", "all", "ground") + ICE_GOLEM: Unit = Unit("ice_golem", "troop", "buildings", "ground") + ICE_SPIRIT: Unit = Unit("ice_spirit", "troop", "all", "ground") + INFERNO_TOWER: Unit = Unit("inferno_tower", "building", None, None) + KNIGHT: Unit = Unit("knight", "troop", "ground", "ground") + MINION: Unit = Unit("minion", "troop", "both", "air") + MINIPEKKA: Unit = Unit("minipekka", "troop", "ground", "ground") + MORTAR: Unit = Unit("mortar", "building", "ground", "ground") + MUSKETEER: Unit = Unit("musketeer", "troop", "both", "ground") + PRINCE: Unit = Unit("prince", "troop", "ground", "ground") + ROYAL_HOG: Unit = Unit("royal_hog", "troop", "buildings", "ground") + SKELETON: Unit = Unit("skeleton", "troop", "ground", "ground") + SPEAR_GOBLIN: Unit = Unit("spear_goblin", "troop", "both", "ground") + TESLA: Unit = Unit("tesla", "building", "both", "ground") + TOMBSTONE: Unit = Unit("tombstone", "building", None, None) + VALKYRIE: Unit = Unit("valkyrie", "troop", "ground", "ground") + WALL_BREAKER: Unit = Unit("wall_breaker", "troop", "buildings", "ground") + X_BOW: Unit = Unit("x_bow", "building", "ground", "ground") Units = _UnitsNamespace()