Skip to content

Commit

Permalink
Merge pull request #35 from marmig0404/main
Browse files Browse the repository at this point in the history
Make project an importable library
Pbatch authored Jun 27, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 7ad9712 + 800ed98 commit 59db2b8
Showing 131 changed files with 429 additions and 47 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/pypi-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This workflow will install build a pypi distribution of this package
# For more information see: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/

name: Python Package

on:
push:
tags: # on tags with versions
- "v*.*.*"

jobs:
build-n-publish:
name: Build and publish Python distributions to PyPI
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@master
- uses: little-core-labs/[email protected]
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: "3.9"
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }} # MUST SET GITHUB REPO SECRET https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
**/__pycache__
.idea/
src/data/screenshots
clashroyalebuildabot/data/screenshots
.venv
.vscode
*.egg-info
23 changes: 23 additions & 0 deletions clashroyalebuildabot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Exports for clashroyalebuildabot
from .bot import PeteBot, RandomBot, StandardBot, TwoSixHogCycle, Action, Bot
from .data import constants
from .screen import Screen
from .state import (CardDetector, Detector, NumberDetector, OnnxDetector,
ScreenDetector, UnitDetector)

__all__ = [
"StandardBot",
"RandomBot",
"PeteBot",
"TwoSixHogCycle",
"constants",
"Detector",
"OnnxDetector",
"ScreenDetector",
"NumberDetector",
"UnitDetector",
"CardDetector",
"Screen",
"Action",
"Bot"
]
15 changes: 15 additions & 0 deletions clashroyalebuildabot/bot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Exports for bot submodule
from .two_six_hog_cycle import TwoSixHogCycle
from .pete import PeteBot
from .random import RandomBot
from .standard import StandardBot
from .action import Action
from .bot import Bot
__all__ = [
"TwoSixHogCycle",
"PeteBot",
"RandomBot",
"StandardBot",
"Action",
"Bot"
]
File renamed without changes.
8 changes: 4 additions & 4 deletions src/bot/bot.py → clashroyalebuildabot/bot/bot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import time

from src.bot.action import Action
from src.data.constants import (
from clashroyalebuildabot.bot.action import Action
from clashroyalebuildabot.data.constants import (
ALLY_TILES,
LEFT_PRINCESS_TILES,
RIGHT_PRINCESS_TILES,
@@ -17,8 +17,8 @@
TILE_INIT_Y,
DISPLAY_HEIGHT
)
from src.screen import Screen
from src.state.detector import Detector
from clashroyalebuildabot.screen import Screen
from clashroyalebuildabot.state.detector import Detector


class Bot:
3 changes: 3 additions & 0 deletions clashroyalebuildabot/bot/pete/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Exports for bot.pete submodule
from .pete_bot import PeteBot
__all__ = ["PeteBot"]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.bot.action import Action
from clashroyalebuildabot.bot.action import Action


class PeteAction(Action):
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import random
import time

from src.bot.bot import Bot
from src.bot.pete.pete_action import PeteAction
from src.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT
from clashroyalebuildabot.bot.bot import Bot
from clashroyalebuildabot.bot.pete.pete_action import PeteAction
from clashroyalebuildabot.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT


class PeteBot(Bot):
3 changes: 3 additions & 0 deletions clashroyalebuildabot/bot/random/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Exports for bot.random submodule
from .random_bot import RandomBot
__all__ = ["RandomBot"]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.bot.bot import Bot
from clashroyalebuildabot.bot.bot import Bot
import time
import random

3 changes: 3 additions & 0 deletions clashroyalebuildabot/bot/standard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Exports for bot.standard submodule
from .standard_bot import StandardBot
__all__ = ["StandardBot"]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.bot.action import Action
from clashroyalebuildabot.bot.action import Action


class StandardAction(Action):
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import random
import time

from src.bot.bot import Bot
from src.bot.standard.standard_action import StandardAction
from src.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT
from clashroyalebuildabot.bot.bot import Bot
from clashroyalebuildabot.bot.standard.standard_action import StandardAction
from clashroyalebuildabot.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT


class StandardBot(Bot):
3 changes: 3 additions & 0 deletions clashroyalebuildabot/bot/two_six_hog_cycle/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Exports for bot.two_six_hog_cycle submodule
from .two_six_hog_cycle_bot import TwoSixHogCycle
__all__ = ["TwoSixHogCycle"]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.bot.action import Action
from clashroyalebuildabot.bot.action import Action


class TwoSixHogCycleAction(Action):
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import random
import time

from src.bot.two_six_hog_cycle.two_six_hog_cycle_action import TwoSixHogCycleAction
from src.bot.bot import Bot
from src.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT
from clashroyalebuildabot.bot.two_six_hog_cycle.two_six_hog_cycle_action import TwoSixHogCycleAction
from clashroyalebuildabot.bot.bot import Bot
from clashroyalebuildabot.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT


class TwoSixHogCycle(Bot):
3 changes: 3 additions & 0 deletions clashroyalebuildabot/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Exports for data submodule
from . import constants
__all__ = ["constants"]
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/screen.py → clashroyalebuildabot/screen.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from PIL import Image
from ppadb.client import Client

from src.data.constants import SCREENSHOT_WIDTH, SCREENSHOT_HEIGHT
from clashroyalebuildabot.data.constants import SCREENSHOT_WIDTH, SCREENSHOT_HEIGHT


class Screen:
14 changes: 14 additions & 0 deletions clashroyalebuildabot/state/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Exports for state submodule
from .detector import Detector
from .onnx_detector import OnnxDetector
from .screen_detector import ScreenDetector
from .number_detector import NumberDetector
from .unit_detector import UnitDetector
from .card_detector import CardDetector

__all__ = ["Detector",
"OnnxDetector",
"ScreenDetector",
"NumberDetector",
"UnitDetector",
"CardDetector"]
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from PIL import Image
import numpy as np
from src.data.constants import CARD_CONFIG, DATA_DIR, MULTI_HASH_SCALE, MULTI_HASH_INTERCEPT, DECK_SIZE, HAND_SIZE
from clashroyalebuildabot.data.constants import CARD_CONFIG, DATA_DIR, MULTI_HASH_SCALE, MULTI_HASH_INTERCEPT, DECK_SIZE, HAND_SIZE
from scipy.optimize import linear_sum_assignment


10 changes: 5 additions & 5 deletions src/state/detector.py → clashroyalebuildabot/state/detector.py
Original file line number Diff line number Diff line change
@@ -2,11 +2,11 @@

from PIL import ImageDraw, ImageFont

from src.state.card_detector import CardDetector
from src.state.number_detector import NumberDetector
from src.state.unit_detector import UnitDetector
from src.state.screen_detector import ScreenDetector
from src.data.constants import DATA_DIR, SCREENSHOTS_DIR, CARD_CONFIG, DECK_SIZE
from clashroyalebuildabot.state.card_detector import CardDetector
from clashroyalebuildabot.state.number_detector import NumberDetector
from clashroyalebuildabot.state.unit_detector import UnitDetector
from clashroyalebuildabot.state.screen_detector import ScreenDetector
from clashroyalebuildabot.data.constants import DATA_DIR, SCREENSHOTS_DIR, CARD_CONFIG, DECK_SIZE


class Detector:
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import numpy as np
from PIL import Image

from src.data.constants import NUMBER_CONFIG, ELIXIR_BOUNDING_BOX, KING_HP, PRINCESS_HP, KING_LEVEL_2_X, \
from clashroyalebuildabot.data.constants import NUMBER_CONFIG, ELIXIR_BOUNDING_BOX, KING_HP, PRINCESS_HP, KING_LEVEL_2_X, \
NUMBER_MIN_CONFIDENCE, NUMBER_HEIGHT, NUMBER_WIDTH
from src.state.onnx_detector import OnnxDetector
from clashroyalebuildabot.state.onnx_detector import OnnxDetector


class NumberDetector(OnnxDetector):
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
import numpy as np
from PIL import Image

from src.data.constants import SCREEN_CONFIG, DATA_DIR
from clashroyalebuildabot.data.constants import SCREEN_CONFIG, DATA_DIR


class ScreenDetector:
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@
import numpy as np
from PIL import Image

from src.data.constants import UNITS, UNIT_Y_END, UNIT_Y_START, UNIT_SIZE, DATA_DIR
from src.state.onnx_detector import OnnxDetector
from clashroyalebuildabot.data.constants import UNITS, UNIT_Y_END, UNIT_Y_START, UNIT_SIZE, DATA_DIR
from clashroyalebuildabot.state.onnx_detector import OnnxDetector


class UnitDetector(OnnxDetector):
169 changes: 169 additions & 0 deletions example/custom_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""
custom_action.py
standard_action.py reimplemented as an import from a clashroyalebuildabot install
"""

from clashroyalebuildabot.bot import Action


class CustomAction(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'].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 k, v in state['units'].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 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'], 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'], 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 k, v in state['units'].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:
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'].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 = {'knight': self._calculate_knight_score,
'minions': self._calculate_minions_score,
'fireball': self._calculate_fireball_score,
'giant': self._calculate_giant_score,
'minipekka': self._calculate_minipekka_score,
'musketeer': self._calculate_musketeer_score,
'arrows': self._calculate_arrows_score,
'archers': self._calculate_archers_score
}
score_function = name_to_score[self.name]
score = score_function(state)
self.score = score
return score
57 changes: 57 additions & 0 deletions example/custom_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
custom_bot.py
standard_bot.py reimplemented as an import from a clashroyalebuildabot install
"""

import random
import time

from clashroyalebuildabot.bot import Bot
from custom_action import CustomAction
from clashroyalebuildabot.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT


class CustomBot(Bot):
def __init__(self, card_names, debug=False):
preset_deck = {'minions', 'archers', 'arrows', 'giant', 'minipekka', 'fireball', 'knight', '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, CustomAction, 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 k, v in self.state['units'].items():
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(1.0)
18 changes: 18 additions & 0 deletions example/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
A CustomBot implementation through import
"""
from custom_bot import CustomBot # see custom_bot.py


def main():
# Set required bot variables
card_names = ['minions', 'archers', 'arrows', 'giant',
'minipekka', 'fireball', 'knight', 'musketeer']
# Define an instance of CustomBot
bot = CustomBot(card_names, debug=True)
# and run!
bot.run()


if __name__ == '__main__':
main()
12 changes: 0 additions & 12 deletions main.py

This file was deleted.

12 changes: 7 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Pillow
numpy
onnxruntime
scipy
pure-python-adb
flatbuffers==2.0
numpy==1.23.0
onnxruntime==1.11.1
Pillow==9.1.1
protobuf==4.21.1
pure-python-adb==0.3.0.dev0
scipy==1.8.1
39 changes: 39 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
from setuptools import setup, find_packages

with open("README.md", "r", encoding="utf-8") as f:
LONG_DESCRIPTION = f.read()

# get github workflow env vars
try:
version = (os.environ['GIT_TAG_NAME']).replace('v', '')
except KeyError:
print('Defaulting to dev')
version = 'dev'

setup(
name='ClashRoyaleBuildABot',
version=version,
description='A platform for creating bots to play Clash Royale',
long_description=LONG_DESCRIPTION,
long_description_content_type='text/markdown',
keywords='machine-learning ai computer-vision adb clashroyale bluestacks yolov5',
author='pbatch',
url='https://github.com/Pbatch/ClashRoyaleBuildABot',
download_url='https://github.com/Pbatch/ClashRoyaleBuildABot/releases',
install_requires=[
"pure-python-adb",
"Pillow", "numpy", "scipy",
"onnxruntime",
],
packages=find_packages(),
python_requires='>=3.6',
zip_safe=False,
classifiers=[
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],
)

0 comments on commit 59db2b8

Please sign in to comment.