Skip to content

Commit

Permalink
Swap to single bot format (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pbatch authored Jul 4, 2024
1 parent 3fcd566 commit 2030c19
Show file tree
Hide file tree
Showing 22 changed files with 306 additions and 548 deletions.
6 changes: 0 additions & 6 deletions clashroyalebuildabot/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,8 +15,6 @@
from .namespaces import Units

__all__ = [
"RandomBot",
"TwoSixHogCycle",
"constants",
"Cards",
"Units",
Expand All @@ -31,6 +26,5 @@
"UnitDetector",
"CardDetector",
"Emulator",
"Action",
"Bot",
]
3 changes: 3 additions & 0 deletions clashroyalebuildabot/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .action import Action

__all__ = ["Action"]
23 changes: 23 additions & 0 deletions clashroyalebuildabot/actions/action.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions clashroyalebuildabot/actions/archers_action.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions clashroyalebuildabot/actions/arrows_action.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions clashroyalebuildabot/actions/fireball_action.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions clashroyalebuildabot/actions/giant_action.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions clashroyalebuildabot/actions/knight_action.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions clashroyalebuildabot/actions/minions_action.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions clashroyalebuildabot/actions/minipekka_action.py
Original file line number Diff line number Diff line change
@@ -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]
21 changes: 21 additions & 0 deletions clashroyalebuildabot/actions/musketeer_action.py
Original file line number Diff line number Diff line change
@@ -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]
27 changes: 27 additions & 0 deletions clashroyalebuildabot/actions/spell_action.py
Original file line number Diff line number Diff line change
@@ -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,
]
6 changes: 0 additions & 6 deletions clashroyalebuildabot/bot/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
123 changes: 105 additions & 18 deletions clashroyalebuildabot/bot/bot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import random
import subprocess
import sys
import time

Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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!")
Loading

0 comments on commit 2030c19

Please sign in to comment.