diff --git a/clashroyalebuildabot/__init__.py b/clashroyalebuildabot/__init__.py index 7478486..1eabef1 100644 --- a/clashroyalebuildabot/__init__.py +++ b/clashroyalebuildabot/__init__.py @@ -1,10 +1,7 @@ # Exports for clashroyalebuildabot from . import constants from . import debugger -from .bot import Action from .bot import Bot -from .bot import RandomBot -from .bot import TwoSixHogCycle from .detectors import CardDetector from .detectors import Detector from .detectors import NumberDetector @@ -18,8 +15,6 @@ from .namespaces import Units __all__ = [ - "RandomBot", - "TwoSixHogCycle", "constants", "Cards", "Units", @@ -31,6 +26,5 @@ "UnitDetector", "CardDetector", "Emulator", - "Action", "Bot", ] diff --git a/clashroyalebuildabot/actions/__init__.py b/clashroyalebuildabot/actions/__init__.py new file mode 100644 index 0000000..af8c024 --- /dev/null +++ b/clashroyalebuildabot/actions/__init__.py @@ -0,0 +1,3 @@ +from .action import Action + +__all__ = ["Action"] diff --git a/clashroyalebuildabot/actions/action.py b/clashroyalebuildabot/actions/action.py new file mode 100644 index 0000000..8ddc613 --- /dev/null +++ b/clashroyalebuildabot/actions/action.py @@ -0,0 +1,23 @@ +from abc import ABC +from abc import abstractmethod + +from clashroyalebuildabot.namespaces.cards import Card + + +class Action(ABC): + CARD: Card = None + + def __init__(self, index, tile_x, tile_y): + self.index = index + self.tile_x = tile_x + self.tile_y = tile_y + + if self.CARD is None: + raise ValueError("Each action must set the CARD attribute") + + def __repr__(self): + return f"{self.CARD.name} at ({self.tile_x}, {self.tile_y})" + + @abstractmethod + def calculate_score(self, state): + pass diff --git a/clashroyalebuildabot/actions/archers_action.py b/clashroyalebuildabot/actions/archers_action.py new file mode 100644 index 0000000..48f7429 --- /dev/null +++ b/clashroyalebuildabot/actions/archers_action.py @@ -0,0 +1,16 @@ +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.action import Action + + +class ArchersAction(Action): + CARD = Cards.ARCHERS + + 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] + return score diff --git a/clashroyalebuildabot/actions/arrows_action.py b/clashroyalebuildabot/actions/arrows_action.py new file mode 100644 index 0000000..ae7689d --- /dev/null +++ b/clashroyalebuildabot/actions/arrows_action.py @@ -0,0 +1,8 @@ +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.spell_action import SpellAction + + +class ArrowsAction(SpellAction): + CARD = Cards.ARROWS + RADIUS = 4 + MIN_TO_HIT = 5 diff --git a/clashroyalebuildabot/actions/fireball_action.py b/clashroyalebuildabot/actions/fireball_action.py new file mode 100644 index 0000000..2f20077 --- /dev/null +++ b/clashroyalebuildabot/actions/fireball_action.py @@ -0,0 +1,8 @@ +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.spell_action import SpellAction + + +class FireballAction(SpellAction): + CARD = Cards.FIREBALL + RADIUS = 2.5 + MIN_TO_HIT = 3 diff --git a/clashroyalebuildabot/actions/giant_action.py b/clashroyalebuildabot/actions/giant_action.py new file mode 100644 index 0000000..b886783 --- /dev/null +++ b/clashroyalebuildabot/actions/giant_action.py @@ -0,0 +1,19 @@ +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.action import Action + + +class GiantAction(Action): + CARD = Cards.GIANT + + def calculate_score(self, state): + score = [0] + left_hp, right_hp = ( + state.numbers[f"{direction}_enemy_princess_hp"]["number"] + for direction in ["left", "right"] + ) + if state.numbers["elixir"]["number"] == 10: + if self.tile_x == 3: + score = [1, self.tile_y, left_hp != -1, left_hp <= right_hp] + elif self.tile_x == 14: + score = [1, self.tile_y, right_hp != -1, right_hp <= left_hp] + return score diff --git a/clashroyalebuildabot/actions/knight_action.py b/clashroyalebuildabot/actions/knight_action.py new file mode 100644 index 0000000..54dd55a --- /dev/null +++ b/clashroyalebuildabot/actions/knight_action.py @@ -0,0 +1,17 @@ +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.action import Action + + +class KnightAction(Action): + CARD = Cards.KNIGHT + + 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 + + if self.tile_y < position.tile_y <= 14 and (lhs or rhs): + score = [1, self.tile_y - position.tile_y] + return score diff --git a/clashroyalebuildabot/actions/minions_action.py b/clashroyalebuildabot/actions/minions_action.py new file mode 100644 index 0000000..2e8c352 --- /dev/null +++ b/clashroyalebuildabot/actions/minions_action.py @@ -0,0 +1,20 @@ +import math + +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.action import Action + + +class MinionsAction(Action): + 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/minipekka_action.py b/clashroyalebuildabot/actions/minipekka_action.py new file mode 100644 index 0000000..0e9179d --- /dev/null +++ b/clashroyalebuildabot/actions/minipekka_action.py @@ -0,0 +1,19 @@ +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.action import Action + + +class MinipekkaAction(Action): + CARD = Cards.MINIPEKKA + + def calculate_score(self, state): + left_hp, right_hp = ( + state.numbers[f"{direction}_enemy_princess_hp"]["number"] + for direction in ["left", "right"] + ) + if self.tile_x in [3, 14]: + return ( + [1, self.tile_y, left_hp != -1, left_hp <= right_hp] + if self.tile_x == 3 + else [1, self.tile_y, right_hp != -1, right_hp <= left_hp] + ) + return [0] diff --git a/clashroyalebuildabot/actions/musketeer_action.py b/clashroyalebuildabot/actions/musketeer_action.py new file mode 100644 index 0000000..9f29bf7 --- /dev/null +++ b/clashroyalebuildabot/actions/musketeer_action.py @@ -0,0 +1,21 @@ +import math + +from clashroyalebuildabot import Cards +from clashroyalebuildabot.actions.action import Action + + +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] + return [0] diff --git a/clashroyalebuildabot/actions/spell_action.py b/clashroyalebuildabot/actions/spell_action.py new file mode 100644 index 0000000..78b17c2 --- /dev/null +++ b/clashroyalebuildabot/actions/spell_action.py @@ -0,0 +1,27 @@ +import math + +from clashroyalebuildabot.actions.action import Action + + +class SpellAction(Action): + RADIUS = None + MIN_TO_HIT = None + + def calculate_score(self, state): + hit_units = 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) + + return [ + 1 if hit_units >= self.MIN_TO_HIT else 0, + hit_units, + max_distance, + ] diff --git a/clashroyalebuildabot/bot/__init__.py b/clashroyalebuildabot/bot/__init__.py index 7d42ab5..fe95e6f 100644 --- a/clashroyalebuildabot/bot/__init__.py +++ b/clashroyalebuildabot/bot/__init__.py @@ -1,11 +1,5 @@ -from .bot import Action from .bot import Bot -from .random import RandomBot -from .two_six_hog_cycle import TwoSixHogCycle __all__ = [ - "TwoSixHogCycle", - "RandomBot", - "Action", "Bot", ] diff --git a/clashroyalebuildabot/bot/bot.py b/clashroyalebuildabot/bot/bot.py index 8b0f150..ba0f59a 100644 --- a/clashroyalebuildabot/bot/bot.py +++ b/clashroyalebuildabot/bot/bot.py @@ -1,4 +1,6 @@ import os +import random +import subprocess import sys import time @@ -25,30 +27,24 @@ from clashroyalebuildabot.namespaces import Screens -class Action: - def __init__(self, index, tile_x, tile_y, card): - self.index = index - self.tile_x = tile_x - self.tile_y = tile_y - self.card = card - - def __repr__(self): - return f"{self.card.name} at ({self.tile_x}, {self.tile_y})" - - class Bot: - def __init__( - self, cards, action_class=Action, auto_start=True, debug=False - ): - self.cards = cards - self.action_class = action_class + def __init__(self, actions, auto_start=True, debug=False): + self.actions = actions self.auto_start = auto_start self.debug = debug + + cards = [action.CARD for action in actions] + if len(cards) != 8: + raise ValueError(f"Must provide 8 cards but was given: {cards}") + self.cards_to_actions = dict(zip(cards, actions)) + + self.detector = Detector(cards=cards, debug=self.debug) self.emulator = Emulator() - self.detector = Detector(cards, debug=self.debug) self.state = None self._setup_logger() + self.end_of_game_clicked = False + self.pause_until = 0 @staticmethod def _setup_logger(): @@ -109,7 +105,7 @@ def get_actions(self): tiles = all_tiles if card.target_anywhere else valid_tiles actions.extend( - [self.action_class(i, x, y, card) for (x, y) in tiles] + [self.cards_to_actions[card](i, x, y) for (x, y) in tiles] ) return actions @@ -126,3 +122,94 @@ def play_action(self, action): tile_centre = self._get_tile_centre(action.tile_x, action.tile_y) self.emulator.click(*card_centre) self.emulator.click(*tile_centre) + + def _restart_game(self): + subprocess.run( + "adb shell am force-stop com.supercell.clashroyale", + shell=True, + check=True, + ) + time.sleep(1) + subprocess.run( + "adb shell am start -n com.supercell.clashroyale/com.supercell.titan.GameApp", + shell=True, + check=True, + ) + logger.info("Waiting 10 seconds.") + time.sleep(10) + self.end_of_game_clicked = False + + def _end_of_game(self): + if time.time() < self.pause_until: + time.sleep(1) + return + + self.set_state() + actions = self.get_actions() + logger.info(f"Actions after end of game: {actions}") + + if self.state.screen == Screens.LOBBY: + logger.debug("Lobby detected, resuming normal operation.") + return + + logger.info("Can't find Battle button, force game restart.") + self._restart_game() + + def step(self): + if self.end_of_game_clicked: + self._end_of_game() + return + + old_screen = self.state.screen if self.state else None + self.set_state() + new_screen = self.state.screen + if new_screen != old_screen: + logger.info(f"New screen state: {new_screen}") + + if new_screen == Screens.END_OF_GAME: + logger.info( + "End of game detected. Waiting 10 seconds for battle button" + ) + self.pause_until = time.time() + 10 + self.end_of_game_clicked = True + time.sleep(10) + return + + if new_screen == Screens.LOBBY: + logger.info("In the main menu. Waiting for 1 second") + time.sleep(1) + return + + actions = self.get_actions() + if not actions: + if self.debug: + logger.debug("No actions available. Waiting for 1 second") + time.sleep(1) + return + + random.shuffle(actions) + best_score = [0] + best_action = None + for action in actions: + score = action.calculate_score(self.state) + if score > best_score: + best_action = action + best_score = score + + if best_score[0] == 0: + time.sleep(1) + return + + self.play_action(best_action) + logger.info( + f"Playing {best_action} with score {best_score}. Waiting for 1 second" + ) + time.sleep(1) + + def run(self): + try: + while True: + self.step() + except (KeyboardInterrupt, Exception): + self.emulator.quit() + logger.info("Thanks for using CRBAB, see you next time!") diff --git a/clashroyalebuildabot/bot/example/custom_action.py b/clashroyalebuildabot/bot/example/custom_action.py deleted file mode 100644 index 063cb01..0000000 --- a/clashroyalebuildabot/bot/example/custom_action.py +++ /dev/null @@ -1,125 +0,0 @@ -import math - -from clashroyalebuildabot.bot.bot import Action -from clashroyalebuildabot.namespaces.cards import Cards - - -class CustomAction(Action): - score = None - - def _calculate_spell_score(self, state, radius, min_to_hit): - hit_units = 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 <= radius - 1: - hit_units += 1 - max_distance = min(max_distance, -distance) - - return [1 if hit_units >= min_to_hit else 0, hit_units, max_distance] - - def _calculate_unit_score(self, state, tile_x_conditions, score_if_met): - score = [0.5] if state.numbers["elixir"]["number"] == 10 else [0] - for v in state.enemies.values(): - for position in v["positions"]: - if self.tile_y < position.tile_y <= 14 and any( - condition(position.tile_x) - for condition in tile_x_conditions - ): - score = score_if_met(self.tile_y, position.tile_y) - return score - - def _calculate_knight_score(self, state): - return self._calculate_unit_score( - state, - [ - lambda x: x > 8 and self.tile_x == 9, - lambda x: x <= 8 and self.tile_x == 8, - ], - lambda my_y, enemy_y: [1, my_y - enemy_y], - ) - - def _calculate_minions_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 - - def _calculate_fireball_score(self, state): - return self._calculate_spell_score(state, radius=2.5, min_to_hit=3) - - def _calculate_arrows_score(self, state): - return self._calculate_spell_score(state, radius=4, min_to_hit=5) - - def _calculate_archers_score(self, state): - return self._calculate_unit_score( - state, - [ - lambda x: x > 8 and self.tile_x == 10, - lambda x: x <= 8 and self.tile_x == 7, - ], - lambda my_y, enemy_y: [1, my_y - enemy_y], - ) - - def _calculate_giant_score(self, state): - score = [0] - left_hp, right_hp = ( - state.numbers[f"{direction}_enemy_princess_hp"]["number"] - for direction in ["left", "right"] - ) - if state.numbers["elixir"]["number"] == 10: - if self.tile_x == 3: - score = [1, self.tile_y, left_hp != -1, left_hp <= right_hp] - elif self.tile_x == 14: - score = [1, self.tile_y, right_hp != -1, right_hp <= left_hp] - return score - - def _calculate_minipekka_score(self, state): - left_hp, right_hp = ( - state.numbers[f"{direction}_enemy_princess_hp"]["number"] - for direction in ["left", "right"] - ) - if self.tile_x in [3, 14]: - return ( - [1, self.tile_y, left_hp != -1, left_hp <= right_hp] - if self.tile_x == 3 - else [1, self.tile_y, right_hp != -1, right_hp <= left_hp] - ) - return [0] - - def _calculate_musketeer_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] - elif distance < 5: - return [0] - return [0] - - def calculate_score(self, state): - 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, - } - self.score = card_to_score[self.card](state) - return self.score diff --git a/clashroyalebuildabot/bot/example/custom_bot.py b/clashroyalebuildabot/bot/example/custom_bot.py deleted file mode 100644 index 00aa294..0000000 --- a/clashroyalebuildabot/bot/example/custom_bot.py +++ /dev/null @@ -1,114 +0,0 @@ -import random -import subprocess -import time - -from loguru import logger - -from clashroyalebuildabot.bot import Bot -from clashroyalebuildabot.bot.example.custom_action import CustomAction -from clashroyalebuildabot.namespaces.cards import Cards -from clashroyalebuildabot.namespaces.screens import Screens - - -class CustomBot(Bot): - PRESET_DECK = [ - Cards.MINIONS, - Cards.ARCHERS, - Cards.ARROWS, - Cards.GIANT, - Cards.MINIPEKKA, - Cards.FIREBALL, - Cards.KNIGHT, - Cards.MUSKETEER, - ] - - def __init__(self, cards=None, debug=False): - if cards is not None: - raise ValueError( - f"CustomBot uses a preset deck: {self.PRESET_DECK}." - "Use cards=None instead." - ) - super().__init__(self.PRESET_DECK, CustomAction, debug=debug) - self.end_of_game_clicked = False - self.pause_until = 0 - - def _restart_game(self): - subprocess.run( - "adb shell am force-stop com.supercell.clashroyale", shell=True - ) - time.sleep(1) - subprocess.run( - "adb shell am start -n com.supercell.clashroyale/com.supercell.titan.GameApp", - shell=True, - ) - logger.info("Waiting 10 seconds.") - time.sleep(10) - self.end_of_game_clicked = False - - def _end_of_game(self): - if time.time() < self.pause_until: - time.sleep(1) - return - - self.set_state() - actions = self.get_actions() - logger.info(f"Actions after end of game: {actions}") - - if self.state.screen == Screens.LOBBY: - logger.debug("Lobby detected, resuming normal operation.") - return - - logger.info("Can't find Battle button, force game restart.") - self._restart_game() - - def step(self): - if self.end_of_game_clicked: - self._end_of_game() - return - - old_screen = self.state.screen if self.state else None - self.set_state() - new_screen = self.state.screen - if new_screen != old_screen: - logger.info(f"New screen state: {new_screen}") - - if new_screen == "end_of_game": - logger.info( - "End of game detected. Waiting 10 seconds for battle button" - ) - self.pause_until = time.time() + 10 - self.end_of_game_clicked = True - time.sleep(10) - return - - if new_screen == "lobby": - logger.info("In the main menu. Waiting for 1 second") - time.sleep(1) - return - - actions = self.get_actions() - if not actions: - if self.debug: - logger.debug("No actions available. Waiting for 1 second") - time.sleep(1) - return - - random.shuffle(actions) - action = max(actions, key=lambda x: x.calculate_score(self.state)) - if action.score[0] == 0: - time.sleep(1) - return - - self.play_action(action) - logger.info( - f"Playing {action} with score {action.score}. Waiting for 1 second" - ) - time.sleep(1) - - def run(self): - try: - while True: - self.step() - except (KeyboardInterrupt, Exception): - self.emulator.quit() - logger.info("Thanks for using CRBAB, see you next time!") diff --git a/clashroyalebuildabot/bot/random/__init__.py b/clashroyalebuildabot/bot/random/__init__.py deleted file mode 100644 index 4d15a25..0000000 --- a/clashroyalebuildabot/bot/random/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Exports for bot.random submodule -from .random_bot import RandomBot - -__all__ = ["RandomBot"] diff --git a/clashroyalebuildabot/bot/random/random_bot.py b/clashroyalebuildabot/bot/random/random_bot.py deleted file mode 100644 index 8b60656..0000000 --- a/clashroyalebuildabot/bot/random/random_bot.py +++ /dev/null @@ -1,23 +0,0 @@ -import random -import time - -from loguru import logger - -from clashroyalebuildabot.bot.bot import Bot - - -class RandomBot(Bot): - 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: - # Choose a random action - action = random.choice(actions) - # Play the given action - self.play_action(action) - # Log the result - logger.info(f"Playing {action}") - time.sleep(3) diff --git a/clashroyalebuildabot/bot/two_six_hog_cycle/__init__.py b/clashroyalebuildabot/bot/two_six_hog_cycle/__init__.py deleted file mode 100644 index 7a65c82..0000000 --- a/clashroyalebuildabot/bot/two_six_hog_cycle/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Exports for bot.two_six_hog_cycle submodule -from .two_six_hog_cycle_bot import TwoSixHogCycle - -__all__ = ["TwoSixHogCycle"] 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 deleted file mode 100644 index 7a2209c..0000000 --- a/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_action.py +++ /dev/null @@ -1,191 +0,0 @@ -import math - -from clashroyalebuildabot.bot.bot import Action -from clashroyalebuildabot.namespaces.cards import Cards -from clashroyalebuildabot.namespaces.units import Transport - - -class TwoSixHogCycleAction(Action): - score = 0 - - def _calculate_hog_rider_score(self, state): - """ - If there are no enemy troops on our side of the arena and - the player has 7 elixir or more - Place hog rider on the bridge as high up as possible - Try to target the lowest hp tower - """ - for v in state.enemies.values(): - for position in v["positions"]: - if not self.tile_y < position.tile_y <= 14: - continue - if (position.tile_x > 8 and self.tile_x == 10) or ( - position.tile_x <= 8 and self.tile_x == 7 - ): - return [0] - - if state.numbers["elixir"]["number"] >= 7: - left_hp, right_hp = [ - state.numbers[f"{direction}_enemy_princess_hp"]["number"] - for direction in ["left", "right"] - ] - if self.tile_x == 3: - return [1, self.tile_y, left_hp != -1, left_hp <= right_hp] - - if self.tile_x == 14: - return [1, self.tile_y, right_hp != -1, right_hp <= left_hp] - - return [0] - - def _calculate_cannon_score(self, state): - """ - If there are ground troops place the cannon in the middle of the arena - """ - if (self.tile_x, self.tile_y) != (9, 10): - return [0] - - for v in state.enemies.values(): - for position in v["positions"]: - if ( - v["transport"] == Transport.GROUND - and position.tile_y >= 10 - ): - return [2] - - return [0] - - def _calculate_musketeer_score(self, state): - """ - If there are flying troops - Place musketeer at 7 tiles in front of the enemies - That should be just within her range and not too close to the enemy - """ - for v in state.enemies.values(): - for position in v["positions"]: - if ( - v["transport"] == Transport.AIR - and self.tile_y == position.tile_y - 7 - ): - return [2] - - return [0] - - def _calculate_ice_golem_score(self, state): - """ - If there is a ground troop on the bridge place the ice golem in the middle of the - arena one tile away from the enemy - """ - if self.tile_y != 4: - return [0] - - for v in state.enemies.values(): - for position in v["positions"]: - if ( - not (15 <= position.tile_y <= 18) - or v["transport"] != Transport.GROUND - ): - continue - - lhs = position.tile_x <= 8 and self.tile_x == 9 - rhs = position.tile_x > 8 and self.tile_x == 8 - if lhs or rhs: - return [2] - - return [0] - - def _calculate_ice_spirit_score(self, state): - """ - Place the ice spirit in the middle of the arena when a ground troop is on the bridge - """ - if self.tile_y != 10: - return [0] - - for v in state.enemies.values(): - for position in v["positions"]: - if ( - not (15 <= position.tile_y <= 18) - or v["transport"] != Transport.GROUND - ): - continue - - lhs = position.tile_x <= 8 and self.tile_x == 9 - rhs = position.tile_x > 8 and self.tile_x == 8 - if lhs or rhs: - return [2] - - return [0] - - def _calculate_spell_score(self, state, radius, min_to_hit): - """ - Calculate the score for a spell card (either fireball or arrows) - - The score is defined as [A, B, C] - A is 1 if we'll hit `min_to_hit` 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 state.enemies.values(): - for position in v["positions"]: - # Add 1 to the score if the spell will hit the unit - distance = math.hypot( - position.tile_x - self.tile_x, - position.tile_y - self.tile_y - 2, - ) - if distance <= radius - 1: - score[1] += 1 - score[2] = min(score[2], -distance) - - # Set score[0] to 1 if we think we'll hit enough units - if score[1] >= min_to_hit: - score[0] = 1 - - return score - - def _calculate_log_score(self, state): - """ - Calculate the score for the log card - """ - score = [0] - for v in state.enemies.values(): - for position in v["positions"]: - if position.tile_y <= 8 and v["transport"] == Transport.GROUND: - if ( - self.tile_y == position.tile_y - 4 - and self.tile_x == position.tile_x - ): - score = [1] - - return score - - def _calculate_fireball_score(self, state): - """ - Play the fireball card if it will hit flying units - """ - for v in state.enemies.values(): - for position in v["positions"]: - if ( - v["transport"] == Transport.AIR - and self.tile_y == position.tile_y - 4 - and self.tile_x == position.tile_x - ): - return [1] - - return self._calculate_spell_score( - state.units, radius=2.5, min_to_hit=3 - ) - - def calculate_score(self, state): - 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, - } - - self.score = card_to_score[self.card](state) - return self.score diff --git a/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_bot.py b/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_bot.py deleted file mode 100644 index 6e3cf2b..0000000 --- a/clashroyalebuildabot/bot/two_six_hog_cycle/two_six_hog_cycle_bot.py +++ /dev/null @@ -1,55 +0,0 @@ -import random -import time - -from loguru import logger - -from clashroyalebuildabot.bot.bot import Bot -from clashroyalebuildabot.bot.two_six_hog_cycle.two_six_hog_cycle_action import ( - TwoSixHogCycleAction, -) -from clashroyalebuildabot.namespaces.cards import Cards - - -class TwoSixHogCycle(Bot): - PRESET_DECK = { - Cards.HOG_RIDER, - Cards.THE_LOG, - Cards.FIREBALL, - Cards.ICE_SPIRIT, - Cards.ICE_GOLEM, - Cards.SKELETONS, - Cards.CANNON, - Cards.MUSKETEER, - } - - def __init__(self, cards=None, debug=True): - if cards is not None: - raise ValueError( - f"CustomBot uses a preset deck: {self.PRESET_DECK}." - "Use cards=None instead." - ) - super().__init__(self.PRESET_DECK, TwoSixHogCycleAction, debug=debug) - - 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) - # Get the best action - action = max( - actions, key=lambda x: x.calculate_score(self.state) - ) - # 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/main.py b/main.py index 26950e3..d3c8d4e 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,15 @@ import threading import time -from clashroyalebuildabot.bot.example.custom_bot import CustomBot +from clashroyalebuildabot.actions.archers_action import ArchersAction +from clashroyalebuildabot.actions.arrows_action import ArrowsAction +from clashroyalebuildabot.actions.fireball_action import FireballAction +from clashroyalebuildabot.actions.giant_action import GiantAction +from clashroyalebuildabot.actions.knight_action import KnightAction +from clashroyalebuildabot.actions.minions_action import MinionsAction +from clashroyalebuildabot.actions.minipekka_action import MinipekkaAction +from clashroyalebuildabot.actions.musketeer_action import MusketeerAction +from clashroyalebuildabot.bot import Bot start_time = datetime.now() @@ -20,7 +28,17 @@ def update_terminal_title(): def main(): - bot = CustomBot(debug=False) + actions = { + ArchersAction, + ArrowsAction, + FireballAction, + GiantAction, + KnightAction, + MinionsAction, + MinipekkaAction, + MusketeerAction, + } + bot = Bot(actions=actions, debug=False) bot.run()