diff --git a/clashroyalebuildabot/bot/bot.py b/clashroyalebuildabot/bot/bot.py index ca7b7ff..738c0c8 100644 --- a/clashroyalebuildabot/bot/bot.py +++ b/clashroyalebuildabot/bot/bot.py @@ -71,7 +71,6 @@ 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): card = self.state["cards"][i + 1] if ( @@ -90,16 +89,16 @@ def get_actions(self): return actions def set_state(self): - # try: - screenshot = self.screen.take_screenshot() - self.state = self.detector.run(screenshot) - if self.auto_start and self.state["screen"] != "in_game": - self.screen.click( - *SCREEN_CONFIG[self.state["screen"]]["click_coordinates"] - ) - time.sleep(2) - # except Exception as e: - # logger.error(f"Error occurred while taking screenshot: {e}") + try: + screenshot = self.screen.take_screenshot() + self.state = self.detector.run(screenshot) + if self.auto_start and self.state["screen"] != "in_game": + self.screen.click( + *SCREEN_CONFIG[self.state["screen"]]["click_coordinates"] + ) + time.sleep(2) + except Exception as e: + logger.error(f"Error occurred while taking screenshot: {e}") def play_action(self, action): card_centre = self._get_card_centre(action.index) diff --git a/clashroyalebuildabot/bot/example/custom_action.py b/clashroyalebuildabot/bot/example/custom_action.py index d124206..9891974 100644 --- a/clashroyalebuildabot/bot/example/custom_action.py +++ b/clashroyalebuildabot/bot/example/custom_action.py @@ -134,14 +134,14 @@ def _calculate_musketeer_score(self, state): def calculate_score(self, state): name_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, + 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, } score_function = name_to_score.get(self.name) self.score = score_function(state) if score_function else [0] diff --git a/clashroyalebuildabot/bot/standard/__init__.py b/clashroyalebuildabot/bot/standard/__init__.py deleted file mode 100644 index fea5a15..0000000 --- a/clashroyalebuildabot/bot/standard/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Exports for bot.standard submodule -from .standard_bot import StandardBot - -__all__ = ["StandardBot"] diff --git a/clashroyalebuildabot/bot/standard/standard_action.py b/clashroyalebuildabot/bot/standard/standard_action.py deleted file mode 100644 index c1d53a5..0000000 --- a/clashroyalebuildabot/bot/standard/standard_action.py +++ /dev/null @@ -1,186 +0,0 @@ -from clashroyalebuildabot.bot.action import Action -from clashroyalebuildabot.namespaces.cards import Cards - - -class StandardAction(Action): - score = None - - @staticmethod - def _distance(x1, y1, x2, y2): - return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5 - - def _calculate_spell_score(self, units, 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 k, v in units.items(): - if k[:4] == "ally": - continue - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - # Assume the unit will move down a few spaces - tile_y -= 2 - - # 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 <= 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_knight_score(self, state): - """ - Only play the knight if a ground troop is on our side of the battlefield - Play the knight in the center, vertically aligned with the troop - """ - score = [0] if state["numbers"]["elixir"]["number"] != 10 else [0.5] - for k, v in state["units"]["enemy"].items(): - if k[:4] == "ally": - continue - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - if self.tile_y < tile_y <= 14 and v["transport"] == "ground": - if ( - tile_x > 8 - and self.tile_x == 9 - or tile_x <= 8 - and self.tile_x == 8 - ): - score = [1, self.tile_y - tile_y] - return score - - def _calculate_minions_score(self, state): - """ - Only play minions on top of enemy units - """ - score = [0] if state["numbers"]["elixir"]["number"] != 10 else [0.5] - for v in state["units"]["enemy"].values(): - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - distance = self._distance( - tile_x, tile_y, self.tile_x, self.tile_y - ) - if distance < 1: - score = [1, -distance] - return score - - def _calculate_fireball_score(self, state): - """ - Only play fireball if at least 3 units will be hit - Try to hit as many units as possible - """ - return self._calculate_spell_score( - state["units"]["enemy"], radius=2.5, min_to_hit=3 - ) - - def _calculate_arrows_score(self, state): - """ - Only play arrows if at least 5 units will be hit - Try to hit as many units as possible - """ - return self._calculate_spell_score( - state["units"]["enemy"], radius=4, min_to_hit=5 - ) - - def _calculate_archers_score(self, state): - """ - Only play the archers if there is a troop on our side of the battlefield - Play the archers in the center, vertically aligned with the troop - """ - score = [0] if state["numbers"]["elixir"]["number"] != 10 else [0.5] - for v in state["units"]["enemy"].values(): - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - if self.tile_y < tile_y <= 14: - if ( - tile_x > 8 - and self.tile_x == 10 - or tile_x <= 8 - and self.tile_x == 7 - ): - score = [1, self.tile_y - tile_y] - return score - - def _calculate_giant_score(self, state): - """ - Only place the giant when at 10 elixir - Place it as high up as possible - Try to target the lowest hp tower - """ - 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): - """ - Place minipekka on the bridge as high up as possible - Try to target the lowest hp tower - """ - left_hp, right_hp = [ - state["numbers"][f"{direction}_enemy_princess_hp"]["number"] - for direction in ["left", "right"] - ] - score = [0] - 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_musketeer_score(self, state): - """ - Place musketeer at 5-6 tiles away from enemies - That should be just within her range - """ - score = [0] - for k, v in state["units"]["enemy"].items(): - if k[:4] == "ally": - continue - for unit in v["positions"]: - tile_x, tile_y = unit["tile_xy"] - distance = self._distance( - tile_x, tile_y, self.tile_x, self.tile_y - ) - if 5 < distance < 6: - score = [1] - elif distance < 5: - score = [0] - return score - - def calculate_score(self, state): - name_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[self.name] - score = score_function(state) - self.score = score - return score diff --git a/clashroyalebuildabot/bot/standard/standard_bot.py b/clashroyalebuildabot/bot/standard/standard_bot.py deleted file mode 100644 index 145019d..0000000 --- a/clashroyalebuildabot/bot/standard/standard_bot.py +++ /dev/null @@ -1,72 +0,0 @@ -import random -import time - -from clashroyalebuildabot.bot.bot import Bot -from clashroyalebuildabot.bot.standard.standard_action import StandardAction -from clashroyalebuildabot.namespaces.cards import Cards -from clashroyalebuildabot.data.constants import DISPLAY_HEIGHT -from clashroyalebuildabot.data.constants import DISPLAY_WIDTH -from clashroyalebuildabot.data.constants import SCREENSHOT_HEIGHT -from clashroyalebuildabot.data.constants import SCREENSHOT_WIDTH - - -class StandardBot(Bot): - def __init__(self, card_names, debug=False): - preset_deck = { - Cards.MINIONS, - Cards.ARCHERS, - Cards.ARROWS, - Cards.GIANT, - Cards.MINIPEKKA, - Cards.FIREBALL, - Cards.KNIGHT, - Cards.MUSKETEER, - } - if set(card_names) != preset_deck: - raise ValueError( - f"You must use the preset deck with cards {preset_deck} for StandardBot" - ) - super().__init__(card_names, StandardAction, 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) - ) - # 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 - print( - f"Playing {action} with score {action.score} and sleeping for 1 second" - ) - time.sleep(0.5) 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 f5fd2c1..903a7c1 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 @@ -181,14 +181,14 @@ def _calculate_fireball_score(self, state): def calculate_score(self, state): name_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, + 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, } score_function = name_to_score[self.name] diff --git a/clashroyalebuildabot/namespaces/units.py b/clashroyalebuildabot/namespaces/units.py index 636dd09..2ff0285 100644 --- a/clashroyalebuildabot/namespaces/units.py +++ b/clashroyalebuildabot/namespaces/units.py @@ -14,18 +14,18 @@ class Unit: class _UnitsNamespace: ARCHER: Unit = ("archer", "troop", "both", "ground") BARBARIAN: Unit = ("barbarian", "troop", "ground", "ground") - BARBARIAN_HUT: Unit = ("barbarian_hut", "building") + 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") - FURNACE: Unit = ("furnace", "building") + FURNACE: Unit = ("furnace", "building", None, None) GIANT: Unit = ("giant", "troop", "ground", "ground") GOBLIN: Unit = ("goblin", "troop", "ground", "ground") - GOBLIN_CAGE: Unit = ("goblin_cage", "building") - GOBLIN_HUT: Unit = ("goblin_hut", "building") + 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") @@ -41,7 +41,7 @@ class _UnitsNamespace: SKELETON: Unit = ("skeleton", "troop", "ground", "ground") SPEAR_GOBLIN: Unit = ("spear_goblin", "troop", "both", "ground") TESLA: Unit = ("tesla", "building", "both", "ground") - TOMBSTONE: Unit = ("tombstone", "building") + 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") diff --git a/main.py b/main.py index 036a294..d9968f4 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ -from datetime import datetime import sys import threading import time +from datetime import datetime from clashroyalebuildabot.bot.example.custom_bot import CustomBot from clashroyalebuildabot.namespaces.cards import Cards