├── .all-contributorsrc ├── .flake8 ├── .github └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── CONTRIBUTING.md ├── ClashRoyaleBuildABot.spec ├── LICENSE.md ├── README.md ├── clashroyalebuildabot ├── __init__.py ├── actions │ ├── __init__.py │ ├── archers_action.py │ ├── arrows_action.py │ ├── baby_dragon_action.py │ ├── bats_action.py │ ├── cannon_action.py │ ├── fireball_action.py │ ├── generic │ │ ├── __init__.py │ │ ├── action.py │ │ ├── bridge_action.py │ │ ├── defense_action.py │ │ ├── king_action.py │ │ ├── overhead_action.py │ │ └── spell_action.py │ ├── giant_action.py │ ├── goblin_barrel_action.py │ ├── knight_action.py │ ├── minions_action.py │ ├── minipekka_action.py │ ├── musketeer_action.py │ ├── witch_action.py │ └── zap_action.py ├── bot │ ├── __init__.py │ └── bot.py ├── config.yaml ├── constants.py ├── detectors │ ├── __init__.py │ ├── card_detector.py │ ├── detector.py │ ├── number_detector.py │ ├── onnx_detector.py │ ├── screen_detector.py │ ├── side_detector.py │ └── unit_detector.py ├── emulator │ ├── __init__.py │ └── emulator.py ├── gui │ ├── animations.py │ ├── gameplay_widget.py │ ├── layout_setup.py │ ├── log_handler.py │ ├── main_window.py │ ├── styles.py │ └── utils.py ├── images │ ├── cards │ │ ├── archer_queen.jpg │ │ ├── archers.jpg │ │ ├── archers_ev1.jpg │ │ ├── arrows.jpg │ │ ├── baby_dragon.jpg │ │ ├── balloon.jpg │ │ ├── bandit.jpg │ │ ├── barbarian_barrel.jpg │ │ ├── barbarian_hut.jpg │ │ ├── barbarian_launcher.jpg │ │ ├── barbarians.jpg │ │ ├── barbarians_ev1.jpg │ │ ├── bats.jpg │ │ ├── bats_ev1.jpg │ │ ├── battle_healer.jpg │ │ ├── battle_ram.jpg │ │ ├── blank.jpg │ │ ├── bomb_tower.jpg │ │ ├── bomber.jpg │ │ ├── bowler.jpg │ │ ├── cannon.jpg │ │ ├── cannon_cart.jpg │ │ ├── card_champion_unknown.jpg │ │ ├── card_legendary_unknown.jpg │ │ ├── clone.jpg │ │ ├── dark_prince.jpg │ │ ├── dart_goblin.jpg │ │ ├── earthquake.jpg │ │ ├── electro_dragon.jpg │ │ ├── electro_giant.jpg │ │ ├── electro_spirit.jpg │ │ ├── electro_wizard.jpg │ │ ├── elite_barbarians.jpg │ │ ├── elixir_collector.jpg │ │ ├── elixir_golem.jpg │ │ ├── executioner.jpg │ │ ├── fire_spirit.jpg │ │ ├── fire_spirits.jpg │ │ ├── fireball.jpg │ │ ├── firecracker.jpg │ │ ├── firecracker_ev1.jpg │ │ ├── fisherman.jpg │ │ ├── flying_machine.jpg │ │ ├── freeze.jpg │ │ ├── furnace.jpg │ │ ├── giant.jpg │ │ ├── giant_skeleton.jpg │ │ ├── giant_snowball.jpg │ │ ├── goblin_barrel.jpg │ │ ├── goblin_cage.jpg │ │ ├── goblin_drill.jpg │ │ ├── goblin_gang.jpg │ │ ├── goblin_giant.jpg │ │ ├── goblin_hut.jpg │ │ ├── goblins.jpg │ │ ├── golden_knight.jpg │ │ ├── golem.jpg │ │ ├── graveyard.jpg │ │ ├── guards.jpg │ │ ├── heal.jpg │ │ ├── heal_spirit.jpg │ │ ├── hog_rider.jpg │ │ ├── hunter.jpg │ │ ├── ice_golem.jpg │ │ ├── ice_spirit.jpg │ │ ├── ice_spirit_ev1.jpg │ │ ├── ice_wizard.jpg │ │ ├── inferno_dragon.jpg │ │ ├── inferno_tower.jpg │ │ ├── knight.jpg │ │ ├── knight_ev1.jpg │ │ ├── lava_hound.jpg │ │ ├── lightning.jpg │ │ ├── little_prince.jpg │ │ ├── lumberjack.jpg │ │ ├── magic_archer.jpg │ │ ├── mega_knight.jpg │ │ ├── mega_minion.jpg │ │ ├── mighty_miner.jpg │ │ ├── miner.jpg │ │ ├── minion_horde.jpg │ │ ├── minions.jpg │ │ ├── minipekka.jpg │ │ ├── mirror.jpg │ │ ├── monk.jpg │ │ ├── mortar.jpg │ │ ├── mortar_ev1.jpg │ │ ├── mother_witch.jpg │ │ ├── musketeer.jpg │ │ ├── night_witch.jpg │ │ ├── party_hut.jpg │ │ ├── party_rocket.jpg │ │ ├── pekka.jpg │ │ ├── phoenix.jpg │ │ ├── poison.jpg │ │ ├── prince.jpg │ │ ├── princess.jpg │ │ ├── rage.jpg │ │ ├── raging_prince.jpg │ │ ├── ram_rider.jpg │ │ ├── rascals.jpg │ │ ├── rocket.jpg │ │ ├── royal_delivery.jpg │ │ ├── royal_ghost.jpg │ │ ├── royal_giant.jpg │ │ ├── royal_giant_ev1.jpg │ │ ├── royal_hogs.jpg │ │ ├── royal_recruits.jpg │ │ ├── royal_recruits_ev1.jpg │ │ ├── santa_hog_rider.jpg │ │ ├── skeleton_army.jpg │ │ ├── skeleton_barrel.jpg │ │ ├── skeleton_dragons.jpg │ │ ├── skeleton_king.jpg │ │ ├── skeletons.jpg │ │ ├── skeletons_ev1.jpg │ │ ├── sparky.jpg │ │ ├── spear_goblins.jpg │ │ ├── super_archers.jpg │ │ ├── super_hog_rider.jpg │ │ ├── super_ice_golem.jpg │ │ ├── super_lava_hound.jpg │ │ ├── super_magic_archer.jpg │ │ ├── super_mini_pekka.jpg │ │ ├── super_witch.jpg │ │ ├── terry.jpg │ │ ├── tesla.jpg │ │ ├── the_log.jpg │ │ ├── three_musketeers.jpg │ │ ├── tombstone.jpg │ │ ├── tornado.jpg │ │ ├── valkyrie.jpg │ │ ├── wall_breakers.jpg │ │ ├── warmth.jpg │ │ ├── witch.jpg │ │ ├── wizard.jpg │ │ ├── x_bow.jpg │ │ ├── zap.jpg │ │ └── zappies.jpg │ ├── icon.ico │ ├── logo.png │ └── screen │ │ ├── end_of_game.jpg │ │ ├── in_game.jpg │ │ └── lobby.jpg ├── models │ ├── side.onnx │ └── units_M_480x352.onnx ├── namespaces │ ├── __init__.py │ ├── cards.py │ ├── numbers.py │ ├── screens.py │ ├── state.py │ └── units.py ├── utils │ ├── error_handling.py │ ├── git_utils.py │ └── logger.py └── visualizer.py ├── error_handling ├── __init__.py └── wikify_error.py ├── main.py └── pyproject.toml /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "link-discord", 10 | "name": "Link", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/50463727?v=4", 12 | "profile": "https://github.com/link-discord", 13 | "contributions": [ 14 | "data" 15 | ] 16 | }, 17 | { 18 | "login": "Emgimeer-Bazder", 19 | "name": "Emgimeer-Bazder", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/17608446?v=4", 21 | "profile": "http://www.pazder.ca", 22 | "contributions": [ 23 | "bug" 24 | ] 25 | }, 26 | { 27 | "login": "GavriloviciEduard", 28 | "name": "Gavrilovici Eduard", 29 | "avatar_url": "https://avatars.githubusercontent.com/u/33176335?v=4", 30 | "profile": "https://github.com/GavriloviciEduard", 31 | "contributions": [ 32 | "doc", 33 | "code" 34 | ] 35 | }, 36 | { 37 | "login": "Nyantad", 38 | "name": "Nyantad", 39 | "avatar_url": "https://avatars.githubusercontent.com/u/68382673?v=4", 40 | "profile": "https://github.com/Nyantad", 41 | "contributions": [ 42 | "bug" 43 | ] 44 | }, 45 | { 46 | "login": "OwenKruse", 47 | "name": "OwenKruse", 48 | "avatar_url": "https://avatars.githubusercontent.com/u/91492770?v=4", 49 | "profile": "https://github.com/OwenKruse", 50 | "contributions": [ 51 | "doc", 52 | "data", 53 | "code", 54 | "example" 55 | ] 56 | }, 57 | { 58 | "login": "marmig0404", 59 | "name": "Martin Miglio", 60 | "avatar_url": "https://avatars.githubusercontent.com/u/10036276?v=4", 61 | "profile": "http://martinmiglio.dev/?utm_source=github_bio&utm_medium=Social", 62 | "contributions": [ 63 | "code", 64 | "doc", 65 | "a11y", 66 | "example", 67 | "userTesting" 68 | ] 69 | }, 70 | { 71 | "login": "ankushsethi", 72 | "name": "Ankush Sethi", 73 | "avatar_url": "https://avatars.githubusercontent.com/u/22005886?v=4", 74 | "profile": "https://github.com/ankushsethi", 75 | "contributions": [ 76 | "bug" 77 | ] 78 | }, 79 | { 80 | "login": "adl212", 81 | "name": "adl212", 82 | "avatar_url": "https://avatars.githubusercontent.com/u/64753570?v=4", 83 | "profile": "https://github.com/adl212", 84 | "contributions": [ 85 | "bug", 86 | "data" 87 | ] 88 | }, 89 | { 90 | "login": "Chi-EEE", 91 | "name": "Chi Huu Huynh", 92 | "avatar_url": "https://avatars.githubusercontent.com/u/73843190?v=4", 93 | "profile": "https://github.com/Chi-EEE", 94 | "contributions": [ 95 | "code", 96 | "maintenance" 97 | ] 98 | }, 99 | { 100 | "login": "hexiro", 101 | "name": "nathan lodge", 102 | "avatar_url": "https://avatars.githubusercontent.com/u/42787085?v=4", 103 | "profile": "https://hexiro.me", 104 | "contributions": [ 105 | "bug" 106 | ] 107 | }, 108 | { 109 | "login": "BassCoder2808", 110 | "name": "Vedant Jolly", 111 | "avatar_url": "https://avatars.githubusercontent.com/u/65075935?v=4", 112 | "profile": "http://basscoder2808.github.io/", 113 | "contributions": [ 114 | "bug" 115 | ] 116 | }, 117 | { 118 | "login": "HorridModz", 119 | "name": "HorridModz", 120 | "avatar_url": "https://avatars.githubusercontent.com/u/105762560?v=4", 121 | "profile": "https://gameguardian.net/forum/profile/1234241-horridmodz/", 122 | "contributions": [ 123 | "doc" 124 | ] 125 | }, 126 | { 127 | "login": "BjornGrylls", 128 | "name": "BjornGrylls", 129 | "avatar_url": "https://avatars.githubusercontent.com/u/35100000?v=4", 130 | "profile": "https://github.com/BjornGrylls", 131 | "contributions": [ 132 | "mentoring" 133 | ] 134 | }, 135 | { 136 | "login": "IIgorrrrr", 137 | "name": "Iiro Heinonen", 138 | "avatar_url": "https://avatars.githubusercontent.com/u/103566403?v=4", 139 | "profile": "https://github.com/IIgorrrrr", 140 | "contributions": [ 141 | "bug" 142 | ] 143 | }, 144 | { 145 | "login": "carlos-dubon", 146 | "name": "Carlos Dubón", 147 | "avatar_url": "https://avatars.githubusercontent.com/u/69093659?v=4", 148 | "profile": "http://carlosdubon.dev", 149 | "contributions": [ 150 | "bug" 151 | ] 152 | }, 153 | { 154 | "login": "Leviaria", 155 | "name": "Leviaria", 156 | "avatar_url": "https://avatars.githubusercontent.com/u/113382526?v=4", 157 | "profile": "https://github.com/Leviaria", 158 | "contributions": [ 159 | "code", 160 | "example", 161 | "tutorial", 162 | "doc" 163 | ] 164 | }, 165 | { 166 | "login": "nikita9091239", 167 | "name": "nikita9091239", 168 | "avatar_url": "https://avatars.githubusercontent.com/u/83181206?v=4", 169 | "profile": "https://github.com/nikita9091239", 170 | "contributions": [ 171 | "bug" 172 | ] 173 | }, 174 | { 175 | "login": "FikryCoder", 176 | "name": "Fikry 🎯", 177 | "avatar_url": "https://avatars.githubusercontent.com/u/111184562?v=4", 178 | "profile": "https://github.com/FikryCoder", 179 | "contributions": [ 180 | "research" 181 | ] 182 | }, 183 | { 184 | "login": "EnderBenjy", 185 | "name": "EnderBenjy", 186 | "avatar_url": "https://avatars.githubusercontent.com/u/68610598?v=4", 187 | "profile": "https://github.com/EnderBenjy", 188 | "contributions": [ 189 | "code", 190 | "design" 191 | ] 192 | } 193 | ], 194 | "contributorsPerLine": 7, 195 | "projectName": "ClashRoyaleBuildABot", 196 | "projectOwner": "Pbatch", 197 | "repoType": "github", 198 | "repoHost": "https://github.com", 199 | "skipCi": true, 200 | "commitConvention": "angular", 201 | "commitType": "docs" 202 | } 203 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, F403, F401 3 | max-line-length = 79 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.10", "3.11"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set Up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | cache: 'pip' 21 | - name: Install dependencies 22 | run: | 23 | pip install .[cpu] 24 | pip install .[gpu] 25 | - name: Verify installation 26 | run: | 27 | python -m pip check 28 | - name: Clean up build artifacts 29 | run: | 30 | rm -rf build 31 | rm -rf *.egg-info 32 | rm -rf dist 33 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | linting: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.10", "3.11"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set Up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | cache: 'pip' 21 | - name: Install dependencies 22 | run: pip install .[cpu] 23 | - name: Run isort 24 | run: isort --check . 25 | - name: Run flake8 26 | run: flake8 27 | - name: Run black 28 | run: black . --check --verbose --diff 29 | - name: Run pylint 30 | run: pylint --persistent n --fail-under 10 -sn clashroyalebuildabot 31 | - name: Clean up build artifacts 32 | run: | 33 | rm -rf build 34 | rm -rf *.egg-info 35 | rm -rf dist 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .venv 3 | .vscode 4 | *.egg-info 5 | dist/ 6 | build/ 7 | __pycache__/ 8 | *.pyc 9 | clashroyalebuildabot/debug 10 | clashroyalebuildabot/emulator/platform-tools 11 | env/ 12 | crbab-venv/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.6.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-case-conflict 7 | - id: check-executables-have-shebangs 8 | - id: check-merge-conflict 9 | - id: check-shebang-scripts-are-executable 10 | - id: check-symlinks 11 | - id: check-yaml 12 | - id: debug-statements 13 | - id: destroyed-symlinks 14 | - id: end-of-file-fixer 15 | files: \.(py|sh|rst|yml|yaml)$ 16 | - id: mixed-line-ending 17 | - id: trailing-whitespace 18 | files: \.(py|sh|rst|yml|yaml)$ 19 | - repo: https://github.com/psf/black 20 | rev: 24.4.2 21 | hooks: 22 | - id: black 23 | language_version: python3.10 24 | - repo: https://github.com/PyCQA/flake8 25 | rev: 5.0.4 26 | hooks: 27 | - id: flake8 28 | - repo: https://github.com/pycqa/isort 29 | rev: 5.13.2 30 | hooks: 31 | - id: isort 32 | name: isort (python) 33 | args: ["--profile", "black", "--filter-files"] 34 | ci: 35 | autofix_prs: true 36 | autoupdate_commit_msg: '[pre-commit.ci] autoupdate' 37 | autoupdate_schedule: monthly 38 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list= 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist=PyQt6 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | fail-on= 40 | 41 | # Specify a score threshold under which the program will exit with error. 42 | fail-under=10 43 | 44 | # Interpret the stdin as a python script, whose filename needs to be passed as 45 | # the module_or_package argument. 46 | #from-stdin= 47 | 48 | # Files or directories to be skipped. They should be base names, not paths. 49 | ignore=CVS 50 | 51 | # Add files or directories matching the regular expressions patterns to the 52 | # ignore-list. The regex matches against paths and can be in Posix or Windows 53 | # format. Because '\\' represents the directory delimiter on Windows systems, 54 | # it can't be used as an escape character. 55 | ignore-paths= 56 | 57 | # Files or directories matching the regular expression patterns are skipped. 58 | # The regex matches against base names, not paths. The default value ignores 59 | # Emacs file locks 60 | ignore-patterns=^\.# 61 | 62 | # List of module names for which member attributes should not be checked 63 | # (useful for modules/projects where namespaces are manipulated during runtime 64 | # and thus existing member attributes cannot be deduced by static analysis). It 65 | # supports qualified module names, as well as Unix pattern matching. 66 | ignored-modules= 67 | 68 | # Python code to execute, usually for sys.path manipulation such as 69 | # pygtk.require(). 70 | #init-hook= 71 | 72 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 73 | # number of processors available to use, and will cap the count on Windows to 74 | # avoid hangs. 75 | jobs=1 76 | 77 | # Control the amount of potential inferred values when inferring a single 78 | # object. This can help the performance when dealing with large functions or 79 | # complex, nested conditions. 80 | limit-inference-results=100 81 | 82 | # List of plugins (as comma separated values of python module names) to load, 83 | # usually to register additional checkers. 84 | load-plugins= 85 | 86 | # Pickle collected data for later comparisons. 87 | persistent=yes 88 | 89 | # Minimum Python version to use for version dependent checks. Will default to 90 | # the version used to run pylint. 91 | py-version=3.10 92 | 93 | # Discover python modules and packages in the file system subtree. 94 | recursive=no 95 | 96 | # Add paths to the list of the source roots. Supports globbing patterns. The 97 | # source root is an absolute path or a path relative to the current working 98 | # directory used to determine a package namespace for modules located under the 99 | # source root. 100 | source-roots= 101 | 102 | # When enabled, pylint would attempt to guess common misconfiguration and emit 103 | # user-friendly hints instead of false-positive error messages. 104 | suggestion-mode=yes 105 | 106 | # Allow loading of arbitrary C extensions. Extensions are imported into the 107 | # active Python interpreter and may run arbitrary code. 108 | unsafe-load-any-extension=no 109 | 110 | # In verbose mode, extra non-checker-related info will be displayed. 111 | #verbose= 112 | 113 | 114 | [BASIC] 115 | 116 | # Naming style matching correct argument names. 117 | argument-naming-style=snake_case 118 | 119 | # Regular expression matching correct argument names. Overrides argument- 120 | # naming-style. If left empty, argument names will be checked with the set 121 | # naming style. 122 | #argument-rgx= 123 | 124 | # Naming style matching correct attribute names. 125 | attr-naming-style=snake_case 126 | 127 | # Regular expression matching correct attribute names. Overrides attr-naming- 128 | # style. If left empty, attribute names will be checked with the set naming 129 | # style. 130 | #attr-rgx= 131 | 132 | # Bad variable names which should always be refused, separated by a comma. 133 | bad-names=foo, 134 | bar, 135 | baz, 136 | toto, 137 | tutu, 138 | tata 139 | 140 | # Bad variable names regexes, separated by a comma. If names match any regex, 141 | # they will always be refused 142 | bad-names-rgxs= 143 | 144 | # Naming style matching correct class attribute names. 145 | class-attribute-naming-style=any 146 | 147 | # Regular expression matching correct class attribute names. Overrides class- 148 | # attribute-naming-style. If left empty, class attribute names will be checked 149 | # with the set naming style. 150 | #class-attribute-rgx= 151 | 152 | # Naming style matching correct class constant names. 153 | class-const-naming-style=UPPER_CASE 154 | 155 | # Regular expression matching correct class constant names. Overrides class- 156 | # const-naming-style. If left empty, class constant names will be checked with 157 | # the set naming style. 158 | #class-const-rgx= 159 | 160 | # Naming style matching correct class names. 161 | class-naming-style=PascalCase 162 | 163 | # Regular expression matching correct class names. Overrides class-naming- 164 | # style. If left empty, class names will be checked with the set naming style. 165 | #class-rgx= 166 | 167 | # Naming style matching correct constant names. 168 | const-naming-style=UPPER_CASE 169 | 170 | # Regular expression matching correct constant names. Overrides const-naming- 171 | # style. If left empty, constant names will be checked with the set naming 172 | # style. 173 | #const-rgx= 174 | 175 | # Minimum line length for functions/classes that require docstrings, shorter 176 | # ones are exempt. 177 | docstring-min-length=-1 178 | 179 | # Naming style matching correct function names. 180 | function-naming-style=snake_case 181 | 182 | # Regular expression matching correct function names. Overrides function- 183 | # naming-style. If left empty, function names will be checked with the set 184 | # naming style. 185 | #function-rgx= 186 | 187 | # Good variable names which should always be accepted, separated by a comma. 188 | good-names=i, 189 | j, 190 | k, 191 | ex, 192 | Run, 193 | _ 194 | 195 | # Good variable names regexes, separated by a comma. If names match any regex, 196 | # they will always be accepted 197 | good-names-rgxs= 198 | 199 | # Include a hint for the correct naming format with invalid-name. 200 | include-naming-hint=no 201 | 202 | # Naming style matching correct inline iteration names. 203 | inlinevar-naming-style=any 204 | 205 | # Regular expression matching correct inline iteration names. Overrides 206 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 207 | # with the set naming style. 208 | #inlinevar-rgx= 209 | 210 | # Naming style matching correct method names. 211 | method-naming-style=snake_case 212 | 213 | # Regular expression matching correct method names. Overrides method-naming- 214 | # style. If left empty, method names will be checked with the set naming style. 215 | #method-rgx= 216 | 217 | # Naming style matching correct module names. 218 | module-naming-style=snake_case 219 | 220 | # Regular expression matching correct module names. Overrides module-naming- 221 | # style. If left empty, module names will be checked with the set naming style. 222 | #module-rgx= 223 | 224 | # Colon-delimited sets of names that determine each other's naming style when 225 | # the name regexes allow several styles. 226 | name-group= 227 | 228 | # Regular expression which should only match function or class names that do 229 | # not require a docstring. 230 | no-docstring-rgx=^_ 231 | 232 | # List of decorators that produce properties, such as abc.abstractproperty. Add 233 | # to this list to register other decorators that produce valid properties. 234 | # These decorators are taken in consideration only for invalid-name. 235 | property-classes=abc.abstractproperty 236 | 237 | # Regular expression matching correct type alias names. If left empty, type 238 | # alias names will be checked with the set naming style. 239 | #typealias-rgx= 240 | 241 | # Regular expression matching correct type variable names. If left empty, type 242 | # variable names will be checked with the set naming style. 243 | #typevar-rgx= 244 | 245 | # Naming style matching correct variable names. 246 | variable-naming-style=snake_case 247 | 248 | # Regular expression matching correct variable names. Overrides variable- 249 | # naming-style. If left empty, variable names will be checked with the set 250 | # naming style. 251 | #variable-rgx= 252 | 253 | 254 | [CLASSES] 255 | 256 | # Warn about protected attribute access inside special methods 257 | check-protected-access-in-special-methods=no 258 | 259 | # List of method names used to declare (i.e. assign) instance attributes. 260 | defining-attr-methods=__init__, 261 | __new__, 262 | setUp, 263 | asyncSetUp, 264 | __post_init__ 265 | 266 | # List of member names, which should be excluded from the protected access 267 | # warning. 268 | exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit 269 | 270 | # List of valid names for the first argument in a class method. 271 | valid-classmethod-first-arg=cls 272 | 273 | # List of valid names for the first argument in a metaclass class method. 274 | valid-metaclass-classmethod-first-arg=mcs 275 | 276 | 277 | [DESIGN] 278 | 279 | # List of regular expressions of class ancestor names to ignore when counting 280 | # public methods (see R0903) 281 | exclude-too-few-public-methods= 282 | 283 | # List of qualified class names to ignore when counting class parents (see 284 | # R0901) 285 | ignored-parents= 286 | 287 | # Maximum number of arguments for function / method. 288 | max-args=5 289 | 290 | # Maximum number of attributes for a class (see R0902). 291 | max-attributes=7 292 | 293 | # Maximum number of boolean expressions in an if statement (see R0916). 294 | max-bool-expr=5 295 | 296 | # Maximum number of branch for function / method body. 297 | max-branches=12 298 | 299 | # Maximum number of locals for function / method body. 300 | max-locals=15 301 | 302 | # Maximum number of parents for a class (see R0901). 303 | max-parents=7 304 | 305 | # Maximum number of public methods for a class (see R0904). 306 | max-public-methods=20 307 | 308 | # Maximum number of return / yield for function / method body. 309 | max-returns=6 310 | 311 | # Maximum number of statements in function / method body. 312 | max-statements=50 313 | 314 | # Minimum number of public methods for a class (see R0903). 315 | min-public-methods=2 316 | 317 | 318 | [EXCEPTIONS] 319 | 320 | # Exceptions that will emit a warning when caught. 321 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 322 | 323 | 324 | [FORMAT] 325 | 326 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 327 | expected-line-ending-format= 328 | 329 | # Regexp for a line that is allowed to be longer than the limit. 330 | ignore-long-lines=^\s*(# )??$ 331 | 332 | # Number of spaces of indent required inside a hanging or continued line. 333 | indent-after-paren=4 334 | 335 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 336 | # tab). 337 | indent-string=' ' 338 | 339 | # Maximum number of characters on a single line. 340 | max-line-length=100 341 | 342 | # Maximum number of lines in a module. 343 | max-module-lines=1000 344 | 345 | # Allow the body of a class to be on the same line as the declaration if body 346 | # contains single statement. 347 | single-line-class-stmt=no 348 | 349 | # Allow the body of an if to be on the same line as the test if there is no 350 | # else. 351 | single-line-if-stmt=no 352 | 353 | 354 | [IMPORTS] 355 | 356 | # List of modules that can be imported at any level, not just the top level 357 | # one. 358 | allow-any-import-level= 359 | 360 | # Allow explicit reexports by alias from a package __init__. 361 | allow-reexport-from-package=no 362 | 363 | # Allow wildcard imports from modules that define __all__. 364 | allow-wildcard-with-all=no 365 | 366 | # Deprecated modules which should not be used, separated by a comma. 367 | deprecated-modules= 368 | 369 | # Output a graph (.gv or any supported image format) of external dependencies 370 | # to the given file (report RP0402 must not be disabled). 371 | ext-import-graph= 372 | 373 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 374 | # external) dependencies to the given file (report RP0402 must not be 375 | # disabled). 376 | import-graph= 377 | 378 | # Output a graph (.gv or any supported image format) of internal dependencies 379 | # to the given file (report RP0402 must not be disabled). 380 | int-import-graph= 381 | 382 | # Force import order to recognize a module as part of the standard 383 | # compatibility libraries. 384 | known-standard-library= 385 | 386 | # Force import order to recognize a module as part of a third party library. 387 | known-third-party=enchant 388 | 389 | # Couples of modules and preferred modules, separated by a comma. 390 | preferred-modules= 391 | 392 | 393 | [LOGGING] 394 | 395 | # The type of string formatting that logging methods do. `old` means using % 396 | # formatting, `new` is for `{}` formatting. 397 | logging-format-style=old 398 | 399 | # Logging modules to check that the string format arguments are in logging 400 | # function parameter format. 401 | logging-modules=logging 402 | 403 | 404 | [MESSAGES CONTROL] 405 | 406 | # Only show warnings with the listed confidence levels. Leave empty to show 407 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 408 | # UNDEFINED. 409 | confidence=HIGH, 410 | CONTROL_FLOW, 411 | INFERENCE, 412 | INFERENCE_FAILURE, 413 | UNDEFINED 414 | 415 | # Disable the message, report, category or checker with the given id(s). You 416 | # can either give multiple identifiers separated by comma (,) or put this 417 | # option multiple times (only on the command line, not in the configuration 418 | # file where it should appear only once). You can also use "--disable=all" to 419 | # disable everything first and then re-enable specific checks. For example, if 420 | # you want to run only the similarities checker, you can use "--disable=all 421 | # --enable=similarities". If you want to run only the classes checker, but have 422 | # no Warning level messages displayed, use "--disable=all --enable=classes 423 | # --disable=W". 424 | disable=raw-checker-failed, 425 | bad-inline-option, 426 | locally-disabled, 427 | file-ignored, 428 | suppressed-message, 429 | useless-suppression, 430 | deprecated-pragma, 431 | use-symbolic-message-instead, 432 | use-implicit-booleaness-not-comparison-to-string, 433 | use-implicit-booleaness-not-comparison-to-zero, 434 | C0103, # invalid-name 435 | C0114, # missing-module-docstring 436 | C0115, # missing-class-docstring 437 | C0116, # missing-function-docstring 438 | R0801, # duplicate-code 439 | R0902, # too-many-instance-attributes 440 | R0903, # too-few-public-methods 441 | R0913, # too-many-arguments 442 | R0914, # too-many-locals 443 | R0915, # too-many-statements 444 | W0718, # broad-exception-caught 445 | 446 | # Enable the message, report, category or checker with the given id(s). You can 447 | # either give multiple identifier separated by comma (,) or put this option 448 | # multiple time (only on the command line, not in the configuration file where 449 | # it should appear only once). See also the "--disable" option for examples. 450 | enable= 451 | 452 | 453 | [METHOD_ARGS] 454 | 455 | # List of qualified names (i.e., library.method) which require a timeout 456 | # parameter e.g. 'requests.api.get,requests.api.post' 457 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 458 | 459 | 460 | [MISCELLANEOUS] 461 | 462 | # List of note tags to take in consideration, separated by a comma. 463 | notes=FIXME, 464 | XXX, 465 | TODO 466 | 467 | # Regular expression of note tags to take in consideration. 468 | notes-rgx= 469 | 470 | 471 | [REFACTORING] 472 | 473 | # Maximum number of nested blocks for function / method body 474 | max-nested-blocks=5 475 | 476 | # Complete name of functions that never returns. When checking for 477 | # inconsistent-return-statements if a never returning function is called then 478 | # it will be considered as an explicit return statement and no message will be 479 | # printed. 480 | never-returning-functions=sys.exit,argparse.parse_error 481 | 482 | # Let 'consider-using-join' be raised when the separator to join on would be 483 | # non-empty (resulting in expected fixes of the type: ``"- " + " - 484 | # ".join(items)``) 485 | suggest-join-with-non-empty-separator=yes 486 | 487 | 488 | [REPORTS] 489 | 490 | # Python expression which should return a score less than or equal to 10. You 491 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 492 | # 'convention', and 'info' which contain the number of messages in each 493 | # category, as well as 'statement' which is the total number of statements 494 | # analyzed. This score is used by the global evaluation report (RP0004). 495 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 496 | 497 | # Template used to display messages. This is a python new-style format string 498 | # used to format the message information. See doc for all details. 499 | msg-template= 500 | 501 | # Set the output format. Available formats are: text, parseable, colorized, 502 | # json2 (improved json format), json (old json format) and msvs (visual 503 | # studio). You can also give a reporter class, e.g. 504 | # mypackage.mymodule.MyReporterClass. 505 | #output-format= 506 | 507 | # Tells whether to display a full report or only the messages. 508 | reports=no 509 | 510 | # Activate the evaluation score. 511 | score=yes 512 | 513 | 514 | [SIMILARITIES] 515 | 516 | # Comments are removed from the similarity computation 517 | ignore-comments=yes 518 | 519 | # Docstrings are removed from the similarity computation 520 | ignore-docstrings=yes 521 | 522 | # Imports are removed from the similarity computation 523 | ignore-imports=yes 524 | 525 | # Signatures are removed from the similarity computation 526 | ignore-signatures=yes 527 | 528 | # Minimum lines number of a similarity. 529 | min-similarity-lines=4 530 | 531 | 532 | [SPELLING] 533 | 534 | # Limits count of emitted suggestions for spelling mistakes. 535 | max-spelling-suggestions=4 536 | 537 | # Spelling dictionary name. No available dictionaries : You need to install 538 | # both the python package and the system dependency for enchant to work. 539 | spelling-dict= 540 | 541 | # List of comma separated words that should be considered directives if they 542 | # appear at the beginning of a comment and should not be checked. 543 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 544 | 545 | # List of comma separated words that should not be checked. 546 | spelling-ignore-words= 547 | 548 | # A path to a file that contains the private dictionary; one word per line. 549 | spelling-private-dict-file= 550 | 551 | # Tells whether to store unknown words to the private dictionary (see the 552 | # --spelling-private-dict-file option) instead of raising a message. 553 | spelling-store-unknown-words=no 554 | 555 | 556 | [STRING] 557 | 558 | # This flag controls whether inconsistent-quotes generates a warning when the 559 | # character used as a quote delimiter is used inconsistently within a module. 560 | check-quote-consistency=no 561 | 562 | # This flag controls whether the implicit-str-concat should generate a warning 563 | # on implicit string concatenation in sequences defined over several lines. 564 | check-str-concat-over-line-jumps=no 565 | 566 | 567 | [TYPECHECK] 568 | 569 | # List of decorators that produce context managers, such as 570 | # contextlib.contextmanager. Add to this list to register other decorators that 571 | # produce valid context managers. 572 | contextmanager-decorators=contextlib.contextmanager 573 | 574 | # List of members which are set dynamically and missed by pylint inference 575 | # system, and so shouldn't trigger E1101 when accessed. Python regular 576 | # expressions are accepted. 577 | generated-members=cv2 578 | 579 | # Tells whether to warn about missing members when the owner of the attribute 580 | # is inferred to be None. 581 | ignore-none=yes 582 | 583 | # This flag controls whether pylint should warn about no-member and similar 584 | # checks whenever an opaque object is returned when inferring. The inference 585 | # can return multiple potential results while evaluating a Python object, but 586 | # some branches might not be evaluated, which results in partial inference. In 587 | # that case, it might be useful to still emit no-member and other checks for 588 | # the rest of the inferred objects. 589 | ignore-on-opaque-inference=yes 590 | 591 | # List of symbolic message names to ignore for Mixin members. 592 | ignored-checks-for-mixins=no-member, 593 | not-async-context-manager, 594 | not-context-manager, 595 | attribute-defined-outside-init 596 | 597 | # List of class names for which member attributes should not be checked (useful 598 | # for classes with dynamically set attributes). This supports the use of 599 | # qualified names. 600 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 601 | 602 | # Show a hint with possible names when a member name was not found. The aspect 603 | # of finding the hint is based on edit distance. 604 | missing-member-hint=yes 605 | 606 | # The minimum edit distance a name should have in order to be considered a 607 | # similar match for a missing member name. 608 | missing-member-hint-distance=1 609 | 610 | # The total number of similar names that should be taken in consideration when 611 | # showing a hint for a missing member. 612 | missing-member-max-choices=1 613 | 614 | # Regex pattern to define which classes are considered mixins. 615 | mixin-class-rgx=.*[Mm]ixin 616 | 617 | # List of decorators that change the signature of a decorated function. 618 | signature-mutators= 619 | 620 | 621 | [VARIABLES] 622 | 623 | # List of additional names supposed to be defined in builtins. Remember that 624 | # you should avoid defining new builtins when possible. 625 | additional-builtins= 626 | 627 | # Tells whether unused global variables should be treated as a violation. 628 | allow-global-unused-variables=yes 629 | 630 | # List of names allowed to shadow builtins 631 | allowed-redefined-builtins= 632 | 633 | # List of strings which can identify a callback function by name. A callback 634 | # name must start or end with one of those strings. 635 | callbacks=cb_, 636 | _cb 637 | 638 | # A regular expression matching the name of dummy variables (i.e. expected to 639 | # not be used). 640 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 641 | 642 | # Argument names that match this expression will be ignored. 643 | ignored-argument-names=_.*|^ignored_|^unused_ 644 | 645 | # Tells whether we should check for unused import in __init__ files. 646 | init-import=no 647 | 648 | # List of qualified module names which can have objects that can redefine 649 | # builtins. 650 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 651 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome all! 4 | Thank you for your eagerness is contributing to the project, 5 | hopefully we can build something really special together. 6 | 7 | If you have ideas for the project, 8 | or are interested in a task in the roadmap (see the README), 9 | then kick off proceedings by saying something in Issues. 10 | Make sure you check that your idea isn't already being discussed! 11 | 12 | If we can agree that the idea is right for the project, 13 | I'll assign you the issue, 14 | and you can get started. 15 | 16 | Once your MR is finished, 17 | I'll take a quick look, 18 | ask any questions, 19 | and merge it into the project. 20 | 21 | I will try my best to use the all-contributors bot to give you credit, 22 | if I forget please say something. 23 | 24 | Have fun coding :) -------------------------------------------------------------------------------- /ClashRoyaleBuildABot.spec: -------------------------------------------------------------------------------- 1 | a = Analysis( 2 | ['main.py'], 3 | binaries=[], 4 | datas=[ 5 | ('clashroyalebuildabot', 'clashroyalebuildabot'), 6 | ('clashroyalebuildabot/images/cards', 'clashroyalebuildabot/images/cards'), 7 | ('clashroyalebuildabot/images/screen', 'clashroyalebuildabot/images/screen'), 8 | ('clashroyalebuildabot/gui', 'clashroyalebuildabot/gui'), 9 | ('clashroyalebuildabot/models', 'clashroyalebuildabot/models'), 10 | ('clashroyalebuildabot/namespaces', 'clashroyalebuildabot/namespaces'), 11 | ('clashroyalebuildabot/utils', 'clashroyalebuildabot/utils') 12 | ], 13 | hiddenimports=[], 14 | hookspath=[], 15 | hooksconfig={}, 16 | runtime_hooks=[], 17 | excludes=[], 18 | noarchive=False, 19 | optimize=0, 20 | ) 21 | 22 | pyz = PYZ(a.pure) 23 | 24 | exe = EXE( 25 | pyz, 26 | a.scripts, 27 | a.binaries, 28 | a.datas, 29 | [], 30 | name='ClashRoyaleBot', 31 | debug=False, 32 | bootloader_ignore_signals=False, 33 | strip=False, 34 | upx=True, 35 | upx_exclude=[], 36 | runtime_tmpdir=None, 37 | console=True, 38 | disable_windowed_traceback=False, 39 | argv_emulation=False, 40 | target_arch=None, 41 | codesign_identity=None, 42 | entitlements_file=None, 43 | icon=['clashroyalebuildabot\\images\\icon.ico'], 44 | ) 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Peter Batchelor] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 📌 This bot is under active development and may experience minor issues. 4 | Please report any bugs on Discord, and we will fix them promptly. 5 | 6 | 7 | 8 | 11 | 14 | 15 |
9 | GUI 10 | 12 | ClashRoyaleBuildABot 13 |
16 | 17 | Welcome to Clash Royale Build-A-Bot! This project provides an advanced state generator that accurately returns detailed information using cutting-edge technologies. 18 | 19 | [![Discord](https://img.shields.io/badge/Discord-00c8d6?logo=discord&logoColor=white&style=flat)](https://discord.gg/K4UfbsfcMa) 20 | [![Python 3.10.0](https://img.shields.io/badge/python-3.10.0-00c8d6?style=flat&logo=python&logoColor=white)](https://www.python.org/downloads/release/python-3100/) 21 | [![All Contributors](https://img.shields.io/badge/all_contributors-17-00c8d6?style=flat)](#contributors-) 22 | 23 | [**Getting Started**](#getting-started-with-your-clash-royale-bot) | 24 | [**Data**](#data) | 25 | [**Contributors**](#contributors-) 26 | 27 |
28 | 29 | --- 30 | 31 | ## Getting Started with Your Clash Royale Bot 🛠️ 32 | 33 | Follow these steps to set up your environment: 34 | 39 | 1. **Install and Configure the Emulator:** Follow our [Emulator Setup Guide](https://github.com/Pbatch/ClashRoyaleBuildABot/wiki/Emulator-Setup-Guide). 40 | 2. **Install Python, PIP, and Git:** Go through our [Python, PIP & Git Setup Guide](https://github.com/Pbatch/ClashRoyaleBuildABot/wiki/Python,-PIP,-&-Git-Setup-Guide). 41 | 3. **Install and Configure Your Bot:** Use our [Bot Installation Guide](https://github.com/Pbatch/ClashRoyaleBuildABot/wiki/Bot-Installation-Setup-Guide). 42 | 43 | Need help? Join our [Discord server](https://discord.gg/K4UfbsfcMa). 44 | 45 | --- 46 | 47 | ## Current Features ⚙️ 48 | 49 | Our bot is equipped with a variety of functionalities designed for educational and research purposes. We will be updating the comprehensive list of features soon, as the bot has expanded significantly. 50 | 51 | --- 52 | 53 | ## Upcoming Features 🚀 54 | 55 | We're continuously working to enhance the bot. Here’s what you can expect in future updates: 56 | 57 | - **Improve Error Handling**: [#252](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/252) 58 | - **Open Chests**: [#250](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/250) 59 | - **Mobile Tutorial**: Add a tutorial for using the bot on your phone. [#249](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/249) 60 | - **Unit Detection Counts**: Integrate unit detection counts into the GUI. [#248](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/248) 61 | - **Deck Swapping**: Ability to swap decks directly from the GUI. [#247](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/247) 62 | - **Synthetic Data Generation**: Enhance data generation capabilities. [#246](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/246) 63 | - **HP Detector Fix**: Improve the HP detector functionality. [#245](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/245) 64 | - **Ignore Regions**: Add ignore regions to the object detector. [#244](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/244) 65 | - **Tracking Algorithm**: Develop a new tracking algorithm. [#146](https://github.com/Pbatch/ClashRoyaleBuildABot/issues/146) 66 | 67 | --- 68 | 69 | ## Legal Disclaimer ⚖️ 70 | 71 | This open-source bot is intended solely for **educational and research purposes**. It is not affiliated with, authorized, or endorsed by Supercell Oy. **The use of this bot in connection with Supercell Oy's Clash Royale game is strictly prohibited.** Such use may violate Supercell Oy's Terms of Service and may result in penalties such as account suspension or termination. 72 | 73 | The author and contributors of this project do not assume any responsibility for your use of this bot. While this bot primarily affects only the account it is run on, it may still harm the competitive gameplay experience of other players. By using this software, you acknowledge that you are solely responsible for your actions and any consequences that may arise from your use. 74 | 75 | **This software is provided "as is" without any warranties of any kind.** 76 | 77 | --- 78 | 79 | ## Creative Minds Behind This ✨ 80 | 81 | A big thank you to all contributors! You are welcome to modify and distribute this project under the terms of the applicable open-source license, provided you give appropriate credit to the original authors. If you develop this project further, please ensure to acknowledge the original project and provide the corresponding credits. 82 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
Link
Link

🔣
Emgimeer-Bazder
Emgimeer-Bazder

🐛
Gavrilovici Eduard
Gavrilovici Eduard

📖 💻
Nyantad
Nyantad

🐛
OwenKruse
OwenKruse

📖 🔣 💻 💡
Martin Miglio
Martin Miglio

💻 📖 ️️️️♿️ 💡 📓
Ankush Sethi
Ankush Sethi

🐛
adl212
adl212

🐛 🔣
Chi Huu Huynh
Chi Huu Huynh

💻 🚧
nathan lodge
nathan lodge

🐛
Vedant Jolly
Vedant Jolly

🐛
HorridModz
HorridModz

📖
BjornGrylls
BjornGrylls

🧑‍🏫
Iiro Heinonen
Iiro Heinonen

🐛
Carlos Dubón
Carlos Dubón

🐛
Leviaria
Leviaria

💻 💡 📖
nikita9091239
nikita9091239

🐛
Fikry 🎯
Fikry 🎯

🔬
EnderBenjy
EnderBenjy

💻 🎨
116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /clashroyalebuildabot/__init__.py: -------------------------------------------------------------------------------- 1 | # Exports for clashroyalebuildabot 2 | from . import constants 3 | from .bot import Bot 4 | from .detectors import CardDetector 5 | from .detectors import Detector 6 | from .detectors import NumberDetector 7 | from .detectors import OnnxDetector 8 | from .detectors import ScreenDetector 9 | from .detectors import UnitDetector 10 | from .emulator import Emulator 11 | from .namespaces import Cards 12 | from .namespaces import Screens 13 | from .namespaces import State 14 | from .namespaces import Units 15 | from .visualizer import Visualizer 16 | 17 | __all__ = [ 18 | "constants", 19 | "Visualizer", 20 | "Cards", 21 | "Units", 22 | "State", 23 | "Detector", 24 | "OnnxDetector", 25 | "ScreenDetector", 26 | "NumberDetector", 27 | "UnitDetector", 28 | "CardDetector", 29 | "Emulator", 30 | "Bot", 31 | ] 32 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/__init__.py: -------------------------------------------------------------------------------- 1 | from .archers_action import ArchersAction 2 | from .arrows_action import ArrowsAction 3 | from .baby_dragon_action import BabyDragonAction 4 | from .bats_action import BatsAction 5 | from .cannon_action import CannonAction 6 | from .fireball_action import FireballAction 7 | from .giant_action import GiantAction 8 | from .goblin_barrel_action import GoblinBarrelAction 9 | from .knight_action import KnightAction 10 | from .minions_action import MinionsAction 11 | from .minipekka_action import MinipekkaAction 12 | from .musketeer_action import MusketeerAction 13 | from .witch_action import WitchAction 14 | from .zap_action import ZapAction 15 | 16 | __all__ = [ 17 | "ArchersAction", 18 | "ArrowsAction", 19 | "BatsAction", 20 | "CannonAction", 21 | "FireballAction", 22 | "GiantAction", 23 | "GoblinBarrelAction", 24 | "BabyDragonAction", 25 | "KnightAction", 26 | "MinionsAction", 27 | "MinipekkaAction", 28 | "MusketeerAction", 29 | "WitchAction", 30 | "ZapAction", 31 | ] 32 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/archers_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.action import Action 3 | 4 | 5 | class ArchersAction(Action): 6 | CARD = Cards.ARCHERS 7 | 8 | def calculate_score(self, state): 9 | score = [0.5] if state.numbers.elixir.number == 10 else [0] 10 | for det in state.enemies: 11 | lhs = det.position.tile_x <= 8 and self.tile_x == 7 12 | rhs = det.position.tile_x > 8 and self.tile_x == 10 13 | if self.tile_y < det.position.tile_y <= 14 and (lhs or rhs): 14 | score = [1, self.tile_y - det.position.tile_y] 15 | return score 16 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/arrows_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.spell_action import SpellAction 3 | 4 | 5 | class ArrowsAction(SpellAction): 6 | CARD = Cards.ARROWS 7 | RADIUS = 4 8 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/baby_dragon_action.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from clashroyalebuildabot import Cards 4 | from clashroyalebuildabot.actions.generic.action import Action 5 | 6 | 7 | class BabyDragonAction(Action): 8 | CARD = Cards.BABY_DRAGON 9 | 10 | def calculate_score(self, state): 11 | for det in state.enemies: 12 | distance = math.hypot( 13 | det.position.tile_x - self.tile_x, 14 | det.position.tile_y - self.tile_y, 15 | ) 16 | if 5 < distance < 6: 17 | return [1] 18 | if distance < 5: 19 | return [0] 20 | return [0] 21 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/bats_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.overhead_action import OverheadAction 3 | 4 | 5 | class BatsAction(OverheadAction): 6 | CARD = Cards.BATS 7 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/cannon_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.defense_action import DefenseAction 3 | 4 | 5 | class CannonAction(DefenseAction): 6 | CARD = Cards.CANNON 7 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/fireball_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.spell_action import SpellAction 3 | 4 | 5 | class FireballAction(SpellAction): 6 | CARD = Cards.FIREBALL 7 | RADIUS = 2.5 8 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/generic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/actions/generic/__init__.py -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/generic/action.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from abc import abstractmethod 3 | 4 | from clashroyalebuildabot.namespaces.cards import Card 5 | 6 | 7 | class Action(ABC): 8 | CARD: Card = None 9 | 10 | def __init__(self, index, tile_x, tile_y): 11 | self.index = index 12 | self.tile_x = tile_x 13 | self.tile_y = tile_y 14 | 15 | def __repr__(self): 16 | return f"{self.CARD.name} at ({self.tile_x}, {self.tile_y})" 17 | 18 | @abstractmethod 19 | def calculate_score(self, state): 20 | pass 21 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/generic/bridge_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot.actions.generic.action import Action 2 | 3 | 4 | class BridgeAction(Action): 5 | """ 6 | If you have 10 elixir, 7 | play this card at the bridge on the side of the weakest tower 8 | """ 9 | 10 | def calculate_score(self, state): 11 | if (self.tile_x, self.tile_y) not in {(3, 15), (14, 15)}: 12 | return [0] 13 | 14 | if state.numbers.elixir.number != 10: 15 | return [0] 16 | 17 | left_hp = state.numbers.left_enemy_princess_hp.number 18 | right_hp = state.numbers.right_enemy_princess_hp.number 19 | if self.tile_x == 3: 20 | score = [1, left_hp > 0, left_hp <= right_hp] 21 | else: 22 | # self.tile_x == 14 23 | score = [1, right_hp > 0, right_hp <= left_hp] 24 | 25 | return score 26 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/generic/defense_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot.actions.generic.action import Action 2 | 3 | 4 | class DefenseAction(Action): 5 | """ 6 | If there are enemies on our side, 7 | play the card in a defensive position 8 | """ 9 | 10 | def calculate_score(self, state): 11 | if (self.tile_x, self.tile_y) not in {(8, 9), (9, 9)}: 12 | return [0] 13 | 14 | lhs = 0 15 | rhs = 0 16 | for det in state.enemies: 17 | if det.position.tile_y > 16: 18 | continue 19 | 20 | if det.position.tile_x >= 9: 21 | rhs += 1 22 | else: 23 | lhs += 1 24 | 25 | if lhs == rhs == 0: 26 | return [0] 27 | 28 | if lhs >= rhs and self.tile_x == 9: 29 | return [0] 30 | 31 | return [1] 32 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/generic/king_action.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from clashroyalebuildabot.actions.generic.action import Action 4 | 5 | 6 | class KingAction(Action): 7 | """ 8 | Play the card behind the king, on the side of the closest enemy 9 | """ 10 | 11 | def calculate_score(self, state): 12 | if (self.tile_x, self.tile_y) not in {(8, 0), (9, 0)}: 13 | return [0] 14 | 15 | min_distance = float("inf") 16 | for det in state.enemies: 17 | distance = math.hypot( 18 | det.position.tile_x - self.tile_x, 19 | det.position.tile_y - self.tile_y, 20 | ) 21 | min_distance = min(min_distance, distance) 22 | 23 | return [0.5, -min_distance] 24 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/generic/overhead_action.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from clashroyalebuildabot.actions.generic.action import Action 4 | 5 | 6 | class OverheadAction(Action): 7 | """ 8 | Play the card directly on top of enemy units 9 | """ 10 | 11 | def calculate_score(self, state): 12 | score = [0.5] if state.numbers.elixir.number == 10 else [0] 13 | for det in state.enemies: 14 | distance = math.hypot( 15 | det.position.tile_x - self.tile_x, 16 | det.position.tile_y - self.tile_y, 17 | ) 18 | if distance < 1: 19 | score = [1, -distance] 20 | return score 21 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/generic/spell_action.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from clashroyalebuildabot.actions.generic.action import Action 4 | from clashroyalebuildabot.namespaces.units import Units 5 | 6 | 7 | class SpellAction(Action): 8 | """ 9 | Play the spell to hit as many enemy units as possible 10 | """ 11 | 12 | RADIUS = None 13 | MIN_SCORE = 5 14 | UNIT_TO_SCORE = {Units.SKELETON: 1} 15 | 16 | def calculate_score(self, state): 17 | hit_score = 0 18 | max_distance = float("inf") 19 | for det in state.enemies: 20 | distance = math.hypot( 21 | self.tile_x - det.position.tile_x, 22 | self.tile_y - det.position.tile_y + 2, 23 | ) 24 | if distance <= self.RADIUS - 1: 25 | hit_score += self.UNIT_TO_SCORE.get(det.unit, 2) 26 | max_distance = min(max_distance, -distance) 27 | 28 | return [ 29 | 1 if hit_score >= self.MIN_SCORE else 0, 30 | hit_score, 31 | max_distance, 32 | ] 33 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/giant_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.action import Action 3 | 4 | 5 | class GiantAction(Action): 6 | CARD = Cards.GIANT 7 | 8 | def calculate_score(self, state): 9 | if state.numbers.elixir.number != 10: 10 | return [0] 11 | 12 | left_hp = state.numbers.left_enemy_princess_hp.number 13 | right_hp = state.numbers.right_enemy_princess_hp.number 14 | 15 | if (self.tile_x, self.tile_y) == (3, 15): 16 | return [1, left_hp > 0, left_hp <= right_hp] 17 | 18 | if (self.tile_x, self.tile_y) == (14, 15): 19 | return [1, right_hp > 0, right_hp <= left_hp] 20 | 21 | return [0] 22 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/goblin_barrel_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.action import Action 3 | 4 | 5 | class GoblinBarrelAction(Action): 6 | CARD = Cards.GOBLIN_BARREL 7 | 8 | def calculate_score(self, state): 9 | left_hp = state.numbers.left_enemy_princess_hp.number 10 | right_hp = state.numbers.right_enemy_princess_hp.number 11 | 12 | if (self.tile_x, self.tile_y) == (3, 25) and left_hp > 0: 13 | return [1, left_hp <= right_hp] 14 | 15 | if (self.tile_x, self.tile_y) == (14, 25) and right_hp > 0: 16 | return [1, right_hp <= left_hp] 17 | 18 | if (self.tile_x, self.tile_y) in {(8, 27), (9, 27), (8, 28), (9, 28)}: 19 | return [0.5] 20 | 21 | return [0] 22 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/knight_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.defense_action import DefenseAction 3 | 4 | 5 | class KnightAction(DefenseAction): 6 | CARD = Cards.KNIGHT 7 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/minions_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.overhead_action import OverheadAction 3 | 4 | 5 | class MinionsAction(OverheadAction): 6 | CARD = Cards.MINIONS 7 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/minipekka_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.bridge_action import BridgeAction 3 | 4 | 5 | class MinipekkaAction(BridgeAction): 6 | CARD = Cards.MINIPEKKA 7 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/musketeer_action.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from clashroyalebuildabot import Cards 4 | from clashroyalebuildabot.actions.generic.action import Action 5 | 6 | 7 | class MusketeerAction(Action): 8 | CARD = Cards.MUSKETEER 9 | 10 | def calculate_score(self, state): 11 | for det in state.enemies: 12 | distance = math.hypot( 13 | det.position.tile_x - self.tile_x, 14 | det.position.tile_y - self.tile_y, 15 | ) 16 | if 5 < distance < 6: 17 | return [1] 18 | if distance < 5: 19 | return [0] 20 | return [0] 21 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/witch_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.king_action import KingAction 3 | 4 | 5 | class WitchAction(KingAction): 6 | CARD = Cards.WITCH 7 | -------------------------------------------------------------------------------- /clashroyalebuildabot/actions/zap_action.py: -------------------------------------------------------------------------------- 1 | from clashroyalebuildabot import Cards 2 | from clashroyalebuildabot.actions.generic.spell_action import SpellAction 3 | 4 | 5 | class ZapAction(SpellAction): 6 | CARD = Cards.ZAP 7 | RADIUS = 2.5 8 | -------------------------------------------------------------------------------- /clashroyalebuildabot/bot/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot import Bot 2 | 3 | __all__ = [ 4 | "Bot", 5 | ] 6 | -------------------------------------------------------------------------------- /clashroyalebuildabot/bot/bot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import threading 3 | import time 4 | 5 | import keyboard 6 | from loguru import logger 7 | 8 | from clashroyalebuildabot.constants import ALL_TILES 9 | from clashroyalebuildabot.constants import ALLY_TILES 10 | from clashroyalebuildabot.constants import DISPLAY_CARD_DELTA_X 11 | from clashroyalebuildabot.constants import DISPLAY_CARD_HEIGHT 12 | from clashroyalebuildabot.constants import DISPLAY_CARD_INIT_X 13 | from clashroyalebuildabot.constants import DISPLAY_CARD_WIDTH 14 | from clashroyalebuildabot.constants import DISPLAY_CARD_Y 15 | from clashroyalebuildabot.constants import DISPLAY_HEIGHT 16 | from clashroyalebuildabot.constants import LEFT_PRINCESS_TILES 17 | from clashroyalebuildabot.constants import RIGHT_PRINCESS_TILES 18 | from clashroyalebuildabot.constants import TILE_HEIGHT 19 | from clashroyalebuildabot.constants import TILE_INIT_X 20 | from clashroyalebuildabot.constants import TILE_INIT_Y 21 | from clashroyalebuildabot.constants import TILE_WIDTH 22 | from clashroyalebuildabot.detectors.detector import Detector 23 | from clashroyalebuildabot.emulator.emulator import Emulator 24 | from clashroyalebuildabot.namespaces import Screens 25 | from clashroyalebuildabot.visualizer import Visualizer 26 | from error_handling import WikifiedError 27 | 28 | pause_event = threading.Event() 29 | pause_event.set() 30 | is_paused_logged = False 31 | is_resumed_logged = True 32 | 33 | 34 | class Bot: 35 | is_paused_logged = False 36 | is_resumed_logged = True 37 | 38 | def __init__(self, actions, config): 39 | self.actions = actions 40 | self.auto_start = config["bot"]["auto_start_game"] 41 | self.end_of_game_clicked = False 42 | self.should_run = True 43 | 44 | cards = [action.CARD for action in actions] 45 | if len(cards) != 8: 46 | raise WikifiedError( 47 | "005", f"Must provide 8 cards but {len(cards)} was given" 48 | ) 49 | self.cards_to_actions = dict(zip(cards, actions)) 50 | 51 | self.visualizer = Visualizer(**config["visuals"]) 52 | self.emulator = Emulator(**config["adb"]) 53 | self.detector = Detector(cards=cards) 54 | self.state = None 55 | self.play_action_delay = config.get("ingame", {}).get("play_action", 1) 56 | 57 | keyboard_thread = threading.Thread( 58 | target=self._handle_keyboard_shortcut, daemon=True 59 | ) 60 | keyboard_thread.start() 61 | 62 | if config["bot"]["load_deck"]: 63 | self.emulator.load_deck(cards) 64 | 65 | @staticmethod 66 | def _log_and_wait(prefix, delay): 67 | suffix = "" 68 | if delay > 1: 69 | suffix = "s" 70 | message = f"{prefix}. Waiting for {delay} second{suffix}." 71 | logger.info(message) 72 | time.sleep(delay) 73 | 74 | @staticmethod 75 | def _handle_keyboard_shortcut(): 76 | while True: 77 | keyboard.wait("ctrl+p") 78 | Bot.pause_or_resume() 79 | 80 | @staticmethod 81 | def pause_or_resume(): 82 | if pause_event.is_set(): 83 | logger.info("Bot paused.") 84 | pause_event.clear() 85 | Bot.is_paused_logged = True 86 | Bot.is_resumed_logged = False 87 | else: 88 | logger.info("Bot resumed.") 89 | pause_event.set() 90 | Bot.is_resumed_logged = True 91 | Bot.is_paused_logged = False 92 | 93 | @staticmethod 94 | def _get_nearest_tile(x, y): 95 | tile_x = round(((x - TILE_INIT_X) / TILE_WIDTH) - 0.5) 96 | tile_y = round( 97 | ((DISPLAY_HEIGHT - TILE_INIT_Y - y) / TILE_HEIGHT) - 0.5 98 | ) 99 | return tile_x, tile_y 100 | 101 | @staticmethod 102 | def _get_tile_centre(tile_x, tile_y): 103 | x = TILE_INIT_X + (tile_x + 0.5) * TILE_WIDTH 104 | y = DISPLAY_HEIGHT - TILE_INIT_Y - (tile_y + 0.5) * TILE_HEIGHT 105 | return x, y 106 | 107 | @staticmethod 108 | def _get_card_centre(card_n): 109 | x = ( 110 | DISPLAY_CARD_INIT_X 111 | + DISPLAY_CARD_WIDTH / 2 112 | + card_n * DISPLAY_CARD_DELTA_X 113 | ) 114 | y = DISPLAY_CARD_Y + DISPLAY_CARD_HEIGHT / 2 115 | return x, y 116 | 117 | def _get_valid_tiles(self): 118 | tiles = ALLY_TILES 119 | if self.state.numbers.left_enemy_princess_hp.number == 0: 120 | tiles += LEFT_PRINCESS_TILES 121 | if self.state.numbers.right_enemy_princess_hp.number == 0: 122 | tiles += RIGHT_PRINCESS_TILES 123 | return tiles 124 | 125 | def get_actions(self): 126 | if not self.state: 127 | return [] 128 | valid_tiles = self._get_valid_tiles() 129 | actions = [] 130 | for i in self.state.ready: 131 | card = self.state.cards[i + 1] 132 | if self.state.numbers.elixir.number < card.cost: 133 | continue 134 | 135 | tiles = ALL_TILES if card.target_anywhere else valid_tiles 136 | card_actions = [ 137 | self.cards_to_actions[card](i, x, y) for (x, y) in tiles 138 | ] 139 | actions.extend(card_actions) 140 | 141 | return actions 142 | 143 | def set_state(self): 144 | screenshot = self.emulator.take_screenshot() 145 | self.state = self.detector.run(screenshot) 146 | self.visualizer.run(screenshot, self.state) 147 | 148 | def play_action(self, action): 149 | card_centre = self._get_card_centre(action.index) 150 | tile_centre = self._get_tile_centre(action.tile_x, action.tile_y) 151 | self.emulator.click(*card_centre) 152 | self.emulator.click(*tile_centre) 153 | 154 | def _handle_play_pause_in_step(self): 155 | if not pause_event.is_set(): 156 | if not Bot.is_paused_logged: 157 | logger.info("Bot paused.") 158 | Bot.is_paused_logged = True 159 | time.sleep(0.1) 160 | return 161 | if not Bot.is_resumed_logged: 162 | logger.info("Bot resumed.") 163 | Bot.is_resumed_logged = True 164 | 165 | def step(self): 166 | self._handle_play_pause_in_step() 167 | old_screen = self.state.screen if self.state else None 168 | self.set_state() 169 | new_screen = self.state.screen 170 | if new_screen != old_screen: 171 | logger.info(f"New screen state: {new_screen}") 172 | 173 | if new_screen == Screens.UNKNOWN: 174 | self._log_and_wait("Unknown screen", 2) 175 | return 176 | 177 | if new_screen == Screens.END_OF_GAME: 178 | if not self.end_of_game_clicked: 179 | self.emulator.click(*self.state.screen.click_xy) 180 | self.end_of_game_clicked = True 181 | self._log_and_wait("Clicked END_OF_GAME screen", 2) 182 | return 183 | 184 | self.end_of_game_clicked = False 185 | 186 | if self.auto_start and new_screen == Screens.LOBBY: 187 | self.emulator.click(*self.state.screen.click_xy) 188 | self.end_of_game_clicked = False 189 | self._log_and_wait("Starting game", 2) 190 | return 191 | 192 | self._handle_game_step() 193 | 194 | def _handle_game_step(self): 195 | actions = self.get_actions() 196 | if not actions: 197 | self._log_and_wait("No actions available", self.play_action_delay) 198 | return 199 | 200 | random.shuffle(actions) 201 | best_score = [0] 202 | best_action = None 203 | for action in actions: 204 | score = action.calculate_score(self.state) 205 | if score > best_score: 206 | best_action = action 207 | best_score = score 208 | 209 | if best_score[0] == 0: 210 | self._log_and_wait( 211 | "No good actions available", self.play_action_delay 212 | ) 213 | return 214 | 215 | self.play_action(best_action) 216 | self._log_and_wait( 217 | f"Playing {best_action} with score {best_score}", 218 | self.play_action_delay, 219 | ) 220 | 221 | def run(self): 222 | try: 223 | while self.should_run: 224 | if not pause_event.is_set(): 225 | time.sleep(0.1) 226 | continue 227 | 228 | self.step() 229 | logger.info("Thanks for using CRBAB, see you next time!") 230 | except KeyboardInterrupt: 231 | logger.info("Thanks for using CRBAB, see you next time!") 232 | 233 | def stop(self): 234 | self.should_run = False 235 | -------------------------------------------------------------------------------- /clashroyalebuildabot/config.yaml: -------------------------------------------------------------------------------- 1 | bot: 2 | # The logging level for the bot. 3 | # Use "DEBUG" for detailed debugging information, "INFO" for general information, 4 | # "WARNING" for warning messages, "ERROR" for error messages, and "CRITICAL" for critical issues. 5 | log_level: "DEBUG" 6 | 7 | # Create a deck code when the game starts 8 | load_deck: False 9 | auto_start_game: False 10 | enable_gui: True 11 | 12 | adb: 13 | # The IP address of your device or emulator. 14 | # Use 127.0.0.1 for a local emulator. If you're connecting to a physical device, 15 | # make sure to use the device's IP address. 16 | ip: "127.0.0.1" 17 | 18 | # The serial number of your device 19 | # Use adb devices to obtain it 20 | device_serial: emulator-5554 21 | 22 | visuals: 23 | # show_images displays the bot's current view in a GUI. 24 | # save_images saves the images seen by the bot. 25 | # save_labels saves the images along with the labels and the bot's perspective. 26 | save_labels: False 27 | save_images: False 28 | show_images: False 29 | 30 | ingame: 31 | # The delay between actions. 32 | # The default is 1. For faster action execution on high-performance PCs, you can set this to 0.5. 33 | # For lower-performance PCs, consider using 1.25 to 1.5. 34 | play_action: 1 35 | -------------------------------------------------------------------------------- /clashroyalebuildabot/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from clashroyalebuildabot.namespaces import Units 4 | 5 | # Directories 6 | SRC_DIR = os.path.dirname(__file__) 7 | DEBUG_DIR = os.path.join(SRC_DIR, "debug") 8 | MODELS_DIR = os.path.join(SRC_DIR, "models") 9 | IMAGES_DIR = os.path.join(SRC_DIR, "images") 10 | EMULATOR_DIR = os.path.join(SRC_DIR, "emulator") 11 | ADB_DIR = os.path.join(EMULATOR_DIR, "platform-tools") 12 | ADB_PATH = os.path.normpath(os.path.join(ADB_DIR, "adb")) 13 | SCREENSHOTS_DIR = os.path.join(DEBUG_DIR, "screenshots") 14 | LABELS_DIR = os.path.join(DEBUG_DIR, "labels") 15 | 16 | # Display dimensions 17 | DISPLAY_WIDTH = 720 18 | DISPLAY_HEIGHT = 1280 19 | 20 | # Screenshot dimensions 21 | SCREENSHOT_WIDTH = 368 22 | SCREENSHOT_HEIGHT = 652 23 | 24 | # Playable tiles 25 | TILE_HEIGHT = 27.6 26 | TILE_WIDTH = 34 27 | N_HEIGHT_TILES = 15 28 | N_WIDE_TILES = 18 29 | TILE_INIT_X = 52 30 | TILE_INIT_Y = 296 31 | ALLY_TILES = [[x, 0] for x in range(N_WIDE_TILES // 3, 2 * N_WIDE_TILES // 3)] 32 | ALLY_TILES += [ 33 | [x, y] for x in range(N_WIDE_TILES) for y in range(1, N_HEIGHT_TILES) 34 | ] 35 | ENEMY_TILES = [[x, 31 - y] for x, y in ALLY_TILES] 36 | ALL_TILES = ALLY_TILES + ENEMY_TILES 37 | LEFT_PRINCESS_TILES = [[3, N_HEIGHT_TILES], [3, N_HEIGHT_TILES + 1]] 38 | LEFT_PRINCESS_TILES += [ 39 | [x, y] 40 | for x in range(N_WIDE_TILES // 2) 41 | for y in range(N_HEIGHT_TILES + 2, N_HEIGHT_TILES + 6) 42 | ] 43 | RIGHT_PRINCESS_TILES = [[14, N_HEIGHT_TILES], [14, N_HEIGHT_TILES + 1]] 44 | RIGHT_PRINCESS_TILES += [ 45 | [x, y] 46 | for x in range(N_WIDE_TILES // 2, N_WIDE_TILES) 47 | for y in range(N_HEIGHT_TILES + 2, N_HEIGHT_TILES + 6) 48 | ] 49 | DISPLAY_CARD_Y = 1067 50 | DISPLAY_CARD_INIT_X = 164 51 | DISPLAY_CARD_WIDTH = 117 52 | DISPLAY_CARD_HEIGHT = 147 53 | DISPLAY_CARD_DELTA_X = 136 54 | 55 | # Cards 56 | CARD_Y = 543 57 | CARD_INIT_X = 84 58 | CARD_WIDTH = 61 59 | CARD_HEIGHT = 73 60 | CARD_DELTA_X = 69 61 | CARD_CONFIG = [ 62 | (21, 609, 47, 642), 63 | (CARD_INIT_X, CARD_Y, CARD_INIT_X + CARD_WIDTH, CARD_Y + CARD_HEIGHT), 64 | ( 65 | CARD_INIT_X + CARD_DELTA_X, 66 | CARD_Y, 67 | CARD_INIT_X + CARD_WIDTH + CARD_DELTA_X, 68 | CARD_Y + CARD_HEIGHT, 69 | ), 70 | ( 71 | CARD_INIT_X + 2 * CARD_DELTA_X, 72 | CARD_Y, 73 | CARD_INIT_X + CARD_WIDTH + 2 * CARD_DELTA_X, 74 | CARD_Y + CARD_HEIGHT, 75 | ), 76 | ( 77 | CARD_INIT_X + 3 * CARD_DELTA_X, 78 | CARD_Y, 79 | CARD_INIT_X + CARD_WIDTH + 3 * CARD_DELTA_X, 80 | CARD_Y + CARD_HEIGHT, 81 | ), 82 | ] 83 | 84 | # Numbers 85 | HP_WIDTH = 40 86 | HP_HEIGHT = 8 87 | LEFT_PRINCESS_HP_X = 74 88 | RIGHT_PRINCESS_HP_X = 266 89 | ALLY_PRINCESS_HP_Y = 403 90 | ENEMY_PRINCESS_HP_Y = 95 91 | ELIXIR_BOUNDING_BOX = (100, 628, 350, 643) 92 | ALLY_HP_LHS_COLOUR = (111, 208, 252) 93 | ALLY_HP_RHS_COLOUR = (63, 79, 112) 94 | ENEMY_HP_LHS_COLOUR = (224, 35, 93) 95 | ENEMY_HP_RHS_COLOUR = (90, 49, 68) 96 | NUMBER_CONFIG = { 97 | "right_ally_princess_hp": [ 98 | RIGHT_PRINCESS_HP_X, 99 | ALLY_PRINCESS_HP_Y, 100 | ALLY_HP_LHS_COLOUR, 101 | ALLY_HP_RHS_COLOUR, 102 | ], 103 | "left_ally_princess_hp": [ 104 | LEFT_PRINCESS_HP_X, 105 | ALLY_PRINCESS_HP_Y, 106 | ALLY_HP_LHS_COLOUR, 107 | ALLY_HP_RHS_COLOUR, 108 | ], 109 | "right_enemy_princess_hp": [ 110 | RIGHT_PRINCESS_HP_X, 111 | ENEMY_PRINCESS_HP_Y, 112 | ENEMY_HP_LHS_COLOUR, 113 | ENEMY_HP_RHS_COLOUR, 114 | ], 115 | "left_enemy_princess_hp": [ 116 | LEFT_PRINCESS_HP_X, 117 | ENEMY_PRINCESS_HP_Y, 118 | ENEMY_HP_LHS_COLOUR, 119 | ENEMY_HP_RHS_COLOUR, 120 | ], 121 | } 122 | 123 | # Units 124 | DETECTOR_UNITS = [ 125 | Units.ARCHER, 126 | Units.ARCHER_QUEEN, 127 | Units.BALLOON, 128 | Units.BANDIT, 129 | Units.BARBARIAN, 130 | Units.BARBARIAN_HUT, 131 | Units.BAT, 132 | Units.BATTLE_HEALER, 133 | Units.BATTLE_RAM, 134 | Units.BOMB_TOWER, 135 | Units.BOMBER, 136 | Units.BOWLER, 137 | Units.BRAWLER, 138 | Units.CANNON, 139 | Units.CANNON_CART, 140 | Units.DARK_PRINCE, 141 | Units.DART_GOBLIN, 142 | Units.ELECTRO_DRAGON, 143 | Units.ELECTRO_GIANT, 144 | Units.ELECTRO_SPIRIT, 145 | Units.ELECTRO_WIZARD, 146 | Units.ELITE_BARBARIAN, 147 | Units.ELIXIR_COLLECTOR, 148 | Units.ELIXIR_GOLEM_LARGE, 149 | Units.ELIXIR_GOLEM_MEDIUM, 150 | Units.ELIXIR_GOLEM_SMALL, 151 | Units.EXECUTIONER, 152 | Units.FIRE_SPIRIT, 153 | Units.FIRE_CRACKER, 154 | Units.FISHERMAN, 155 | Units.FLYING_MACHINE, 156 | Units.FURNACE, 157 | Units.GIANT, 158 | Units.GIANT_SKELETON, 159 | Units.GIANT_SNOWBALL, 160 | Units.GOBLIN, 161 | Units.GOBLIN_CAGE, 162 | Units.GOBLIN_DRILL, 163 | Units.GOBLIN_HUT, 164 | Units.GOLDEN_KNIGHT, 165 | Units.GOLEM, 166 | Units.GOLEMITE, 167 | Units.GUARD, 168 | Units.HEAL_SPIRIT, 169 | Units.HOG, 170 | Units.HOG_RIDER, 171 | Units.BABY_DRAGON, 172 | Units.HUNTER, 173 | Units.ICE_GOLEM, 174 | Units.ICE_SPIRIT, 175 | Units.ICE_WIZARD, 176 | Units.INFERNO_DRAGON, 177 | Units.INFERNO_TOWER, 178 | Units.KNIGHT, 179 | Units.LAVA_HOUND, 180 | Units.LAVA_PUP, 181 | Units.LITTLE_PRINCE, 182 | Units.LUMBERJACK, 183 | Units.MAGIC_ARCHER, 184 | Units.MEGA_KNIGHT, 185 | Units.MEGA_MINION, 186 | Units.MIGHTY_MINER, 187 | Units.MINER, 188 | Units.MINION, 189 | Units.MINIPEKKA, 190 | Units.MONK, 191 | Units.MORTAR, 192 | Units.MOTHER_WITCH, 193 | Units.MUSKETEER, 194 | Units.NIGHT_WITCH, 195 | Units.PEKKA, 196 | Units.PHOENIX_EGG, 197 | Units.PHOENIX_LARGE, 198 | Units.PHOENIX_SMALL, 199 | Units.PRINCE, 200 | Units.PRINCESS, 201 | Units.RAM_RIDER, 202 | Units.RASCAL_BOY, 203 | Units.RASCAL_GIRL, 204 | Units.ROYAL_GHOST, 205 | Units.ROYAL_GIANT, 206 | Units.ROYAL_GUARDIAN, 207 | Units.ROYAL_HOG, 208 | Units.ROYAL_RECRUIT, 209 | Units.SKELETON, 210 | Units.SKELETON_DRAGON, 211 | Units.SKELETON_KING, 212 | Units.SPARKY, 213 | Units.SPEAR_GOBLIN, 214 | Units.TESLA, 215 | Units.TOMBSTONE, 216 | Units.VALKYRIE, 217 | Units.WALL_BREAKER, 218 | Units.WITCH, 219 | Units.WIZARD, 220 | Units.X_BOW, 221 | Units.ZAPPY, 222 | ] 223 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/__init__.py: -------------------------------------------------------------------------------- 1 | # Exports for state submodule 2 | from .card_detector import CardDetector 3 | from .detector import Detector 4 | from .number_detector import NumberDetector 5 | from .onnx_detector import OnnxDetector 6 | from .screen_detector import ScreenDetector 7 | from .unit_detector import UnitDetector 8 | 9 | __all__ = [ 10 | "Detector", 11 | "OnnxDetector", 12 | "ScreenDetector", 13 | "NumberDetector", 14 | "UnitDetector", 15 | "CardDetector", 16 | ] 17 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/card_detector.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | from PIL import Image 5 | from scipy.optimize import linear_sum_assignment 6 | 7 | from clashroyalebuildabot.constants import CARD_CONFIG 8 | from clashroyalebuildabot.constants import IMAGES_DIR 9 | from clashroyalebuildabot.namespaces.cards import Cards 10 | from error_handling import WikifiedError 11 | 12 | 13 | class CardDetector: 14 | HAND_SIZE = 5 15 | MULTI_HASH_SCALE = 0.355 16 | MULTI_HASH_INTERCEPT = 163 17 | 18 | def __init__(self, cards, hash_size=8, grey_std_threshold=5): 19 | self.cards = cards 20 | self.hash_size = hash_size 21 | self.grey_std_threshold = grey_std_threshold 22 | 23 | self.cards.extend([Cards.BLANK for _ in range(5)]) 24 | self.card_hashes = self._calculate_card_hashes() 25 | 26 | def _calculate_multi_hash(self, image): 27 | gray_image = self._calculate_hash(image) 28 | light_image = ( 29 | self.MULTI_HASH_SCALE * gray_image + self.MULTI_HASH_INTERCEPT 30 | ) 31 | dark_image = ( 32 | gray_image - self.MULTI_HASH_INTERCEPT 33 | ) / self.MULTI_HASH_SCALE 34 | multi_hash = np.vstack([gray_image, light_image, dark_image]).astype( 35 | np.float32 36 | ) 37 | return multi_hash 38 | 39 | def _calculate_hash(self, image): 40 | return np.array( 41 | image.resize( 42 | (self.hash_size, self.hash_size), Image.Resampling.BILINEAR 43 | ).convert("L"), 44 | dtype=np.float32, 45 | ).ravel() 46 | 47 | def _calculate_card_hashes(self): 48 | card_hashes = np.zeros( 49 | ( 50 | len(self.cards), 51 | 3, 52 | self.hash_size * self.hash_size, 53 | self.HAND_SIZE, 54 | ), 55 | dtype=np.float32, 56 | ) 57 | try: 58 | for i, card in enumerate(self.cards): 59 | path = os.path.join(IMAGES_DIR, "cards", f"{card.name}.jpg") 60 | pil_image = Image.open(path) 61 | 62 | multi_hash = self._calculate_multi_hash(pil_image) 63 | card_hashes[i] = np.tile( 64 | np.expand_dims(multi_hash, axis=2), (1, 1, self.HAND_SIZE) 65 | ) 66 | except Exception as e: 67 | raise WikifiedError( 68 | "005", "Can't load cards and their images." 69 | ) from e 70 | return card_hashes 71 | 72 | def _detect_cards(self, image): 73 | crops = [image.crop(position) for position in CARD_CONFIG] 74 | crop_hashes = np.array( 75 | [self._calculate_hash(crop) for crop in crops] 76 | ).T 77 | hash_diffs = np.mean( 78 | np.amin(np.abs(crop_hashes - self.card_hashes), axis=1), axis=1 79 | ).T 80 | _, idx = linear_sum_assignment(hash_diffs) 81 | cards = [self.cards[i] for i in idx] 82 | 83 | return cards, crops 84 | 85 | def _detect_if_ready(self, crops): 86 | ready = [] 87 | for i, crop in enumerate(crops[1:]): 88 | std = np.mean(np.std(np.array(crop), axis=2)) 89 | if std > self.grey_std_threshold: 90 | ready.append(i) 91 | return ready 92 | 93 | def run(self, image): 94 | cards, crops = self._detect_cards(image) 95 | ready = self._detect_if_ready(crops) 96 | return cards, ready 97 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/detector.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | import os 3 | import time 4 | 5 | from loguru import logger 6 | 7 | from clashroyalebuildabot.constants import MODELS_DIR 8 | from clashroyalebuildabot.detectors.card_detector import CardDetector 9 | from clashroyalebuildabot.detectors.number_detector import NumberDetector 10 | from clashroyalebuildabot.detectors.screen_detector import ScreenDetector 11 | from clashroyalebuildabot.detectors.unit_detector import UnitDetector 12 | from clashroyalebuildabot.namespaces import State 13 | from error_handling import WikifiedError 14 | 15 | 16 | class Detector: 17 | DECK_SIZE = 8 18 | 19 | def __init__(self, cards): 20 | if len(cards) != self.DECK_SIZE: 21 | raise WikifiedError( 22 | "005", f"You must specify all {self.DECK_SIZE} of your cards" 23 | ) 24 | 25 | self.cards = deepcopy(cards) 26 | 27 | self.card_detector = CardDetector(self.cards) 28 | self.number_detector = NumberDetector() 29 | self.unit_detector = UnitDetector( 30 | os.path.join(MODELS_DIR, "units_M_480x352.onnx"), self.cards 31 | ) 32 | self.screen_detector = ScreenDetector() 33 | 34 | def run(self, image): 35 | logger.debug("Setting state...") 36 | retries = 3 37 | for attempt in range(retries): 38 | try: 39 | cards, ready = self.card_detector.run(image) 40 | allies, enemies = self.unit_detector.run(image) 41 | numbers = self.number_detector.run(image) 42 | screen = self.screen_detector.run(image) 43 | 44 | state = State(allies, enemies, numbers, cards, ready, screen) 45 | return state 46 | except Exception as e: 47 | logger.error( 48 | f"Detection failed on attempt {attempt + 1}: {str(e)}" 49 | ) 50 | if attempt < retries - 1: 51 | time.sleep(1) 52 | 53 | logger.error("All detection attempts failed. Returning default state.") 54 | return State([], [], [], [], False, None) 55 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/number_detector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import ImageFilter 3 | 4 | from clashroyalebuildabot.constants import ELIXIR_BOUNDING_BOX 5 | from clashroyalebuildabot.constants import HP_HEIGHT 6 | from clashroyalebuildabot.constants import HP_WIDTH 7 | from clashroyalebuildabot.constants import NUMBER_CONFIG 8 | from clashroyalebuildabot.namespaces.numbers import NumberDetection 9 | from clashroyalebuildabot.namespaces.numbers import Numbers 10 | 11 | 12 | class NumberDetector: 13 | @staticmethod 14 | def _calculate_elixir(image, window_size=10, threshold=50): 15 | crop = image.crop(ELIXIR_BOUNDING_BOX) 16 | std = np.array(crop).std(axis=(0, 2)) 17 | rolling_std = np.convolve( 18 | std, np.ones(window_size) / window_size, mode="valid" 19 | ) 20 | change_points = np.nonzero(rolling_std < threshold)[0] 21 | if len(change_points) == 0: 22 | elixir = 10 23 | else: 24 | elixir = (change_points[0] + window_size) * 10 // crop.width 25 | return elixir 26 | 27 | @staticmethod 28 | def _calculate_hp(image, bbox, lhs_colour, rhs_colour, threshold=30): 29 | crop = np.array( 30 | image.crop(bbox).filter(ImageFilter.SMOOTH_MORE), dtype=np.float32 31 | ) 32 | 33 | means = np.array( 34 | [ 35 | np.mean(np.abs(crop - colour), axis=2) 36 | for colour in [lhs_colour, rhs_colour] 37 | ] 38 | ) 39 | best_row = np.argmin(np.sum(np.min(means, axis=0), axis=1)) 40 | means = means[:, best_row, :] 41 | sides = np.argmin(means, axis=0) 42 | avg_min_dist = np.mean(np.where(sides, means[1], means[0])) 43 | 44 | if avg_min_dist > threshold: 45 | hp = 0.0 46 | else: 47 | change_point = np.argmin(np.cumsum(2 * sides - 1)) 48 | hp = change_point / (HP_WIDTH - 1) 49 | 50 | return hp 51 | 52 | def run(self, image): 53 | pred = {} 54 | for name, (x, y, lhs_colour, rhs_colour) in NUMBER_CONFIG.items(): 55 | bbox = (x, y, x + HP_WIDTH, y + HP_HEIGHT) 56 | hp = self._calculate_hp(image, bbox, lhs_colour, rhs_colour) 57 | pred[name] = NumberDetection(bbox, hp) 58 | 59 | elixir = self._calculate_elixir(image) 60 | pred["elixir"] = NumberDetection(ELIXIR_BOUNDING_BOX, elixir) 61 | 62 | numbers = Numbers(**pred) 63 | 64 | return numbers 65 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/onnx_detector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import onnxruntime as ort 3 | 4 | 5 | class OnnxDetector: 6 | def __init__(self, model_path): 7 | self.model_path = model_path 8 | 9 | providers = list( 10 | set(ort.get_available_providers()) 11 | & {"CUDAExecutionProvider", "CPUExecutionProvider"} 12 | ) 13 | self.sess = ort.InferenceSession( 14 | self.model_path, 15 | providers=providers, 16 | ) 17 | self.output_name = self.sess.get_outputs()[0].name 18 | 19 | input_ = self.sess.get_inputs()[0] 20 | self.input_name = input_.name 21 | self.model_height, self.model_width = input_.shape[2:] 22 | 23 | def resize(self, x): 24 | ratio = x.height / x.width 25 | if ratio > self.model_height / self.model_width: 26 | height = self.model_height 27 | width = int(self.model_height / ratio) 28 | else: 29 | width = self.model_width 30 | height = int(self.model_width * ratio) 31 | 32 | x = x.resize((width, height)) 33 | return x 34 | 35 | def pad(self, x): 36 | height, width = x.shape[:2] 37 | dx = self.model_width - width 38 | dy = self.model_height - height 39 | pad_right = dx // 2 40 | pad_left = dx - pad_right 41 | pad_bottom = dy // 2 42 | pad_top = dy - pad_bottom 43 | padding = [pad_left, pad_right, pad_top, pad_bottom] 44 | x = np.pad( 45 | x, 46 | ((pad_top, pad_bottom), (pad_left, pad_right), (0, 0)), 47 | mode="constant", 48 | constant_values=114, 49 | ) 50 | return x, padding 51 | 52 | def resize_pad_transpose_and_scale(self, image): 53 | image = self.resize(image) 54 | image = np.array(image, dtype=np.float16) 55 | image, padding = self.pad(image) 56 | image = image.transpose(2, 0, 1) 57 | image /= 255 58 | return image, padding 59 | 60 | def fix_bboxes(self, x, width, height, padding): 61 | x[:, [0, 2]] -= padding[0] 62 | x[:, [1, 3]] -= padding[2] 63 | x[..., [0, 2]] *= width / (self.model_width - padding[0] - padding[1]) 64 | x[..., [1, 3]] *= height / ( 65 | self.model_height - padding[2] - padding[3] 66 | ) 67 | return x 68 | 69 | def _infer(self, x): 70 | return self.sess.run([self.output_name], {self.input_name: x})[0] 71 | 72 | def run(self, image): 73 | raise NotImplementedError 74 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/screen_detector.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | from PIL import Image 5 | 6 | from clashroyalebuildabot.constants import IMAGES_DIR 7 | from clashroyalebuildabot.namespaces import Screens 8 | from clashroyalebuildabot.namespaces.screens import Screen 9 | 10 | 11 | class ScreenDetector: 12 | def __init__(self, hash_size=8, threshold=30): 13 | self.hash_size = hash_size 14 | self.threshold = threshold 15 | self.screen_hashes = self._calculate_screen_hashes() 16 | 17 | def _image_hash(self, image): 18 | crop = image.resize( 19 | (self.hash_size, self.hash_size), Image.Resampling.BILINEAR 20 | ) 21 | hash_ = np.array(crop, dtype=np.float32).flatten() 22 | return hash_ 23 | 24 | def _calculate_screen_hashes(self): 25 | screen_hashes = {} 26 | for screen in Screens.__dict__.values(): 27 | if screen.ltrb is None: 28 | continue 29 | path = os.path.join(IMAGES_DIR, "screen", f"{screen.name}.jpg") 30 | image = Image.open(path) 31 | screen_hashes[screen] = self._image_hash(image) 32 | return screen_hashes 33 | 34 | def run(self, image: Image) -> Screen: 35 | current_screen = Screens.UNKNOWN 36 | best_diff = self.threshold 37 | 38 | for screen in Screens.__dict__.values(): 39 | if screen.ltrb is None: 40 | continue 41 | # screen.ltb are dimensions scaled to 720x1280 so we scale them : 42 | treated_ltrb = ( 43 | int(screen.ltrb[0] * image.size[0] / 720), 44 | int(screen.ltrb[1] * image.size[1] / 1280), 45 | int(screen.ltrb[2] * image.size[0] / 720), 46 | int(screen.ltrb[3] * image.size[1] / 1280), 47 | ) 48 | hash_ = self._image_hash(image.crop(treated_ltrb)) 49 | target_hash = self.screen_hashes[screen] 50 | 51 | diff = np.mean(np.abs(hash_ - target_hash)) 52 | if diff < best_diff: 53 | best_diff = diff 54 | current_screen = screen 55 | 56 | return current_screen 57 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/side_detector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import Image 3 | 4 | from clashroyalebuildabot.detectors.onnx_detector import OnnxDetector 5 | 6 | 7 | class SideDetector(OnnxDetector): 8 | SIDE_SIZE = 16 9 | 10 | def _preprocess(self, image): 11 | image = image.resize( 12 | (self.SIDE_SIZE, self.SIDE_SIZE), Image.Resampling.BICUBIC 13 | ) 14 | image = np.array(image, dtype=np.float32) / 255 15 | return np.expand_dims(image, axis=0) 16 | 17 | @staticmethod 18 | def _post_process(pred): 19 | return ("ally", "enemy")[np.argmax(pred[0])] 20 | 21 | def run(self, image): 22 | image = self._preprocess(image) 23 | pred = self._infer(image) 24 | return self._post_process(pred) 25 | -------------------------------------------------------------------------------- /clashroyalebuildabot/detectors/unit_detector.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | from clashroyalebuildabot.constants import DETECTOR_UNITS 6 | from clashroyalebuildabot.constants import DISPLAY_HEIGHT 7 | from clashroyalebuildabot.constants import DISPLAY_WIDTH 8 | from clashroyalebuildabot.constants import MODELS_DIR 9 | from clashroyalebuildabot.constants import SCREENSHOT_HEIGHT 10 | from clashroyalebuildabot.constants import SCREENSHOT_WIDTH 11 | from clashroyalebuildabot.constants import TILE_HEIGHT 12 | from clashroyalebuildabot.constants import TILE_INIT_X 13 | from clashroyalebuildabot.constants import TILE_INIT_Y 14 | from clashroyalebuildabot.constants import TILE_WIDTH 15 | from clashroyalebuildabot.detectors.onnx_detector import OnnxDetector 16 | from clashroyalebuildabot.detectors.side_detector import SideDetector 17 | from clashroyalebuildabot.namespaces.units import Position 18 | from clashroyalebuildabot.namespaces.units import UnitDetection 19 | 20 | 21 | class UnitDetector(OnnxDetector): 22 | MIN_CONF = 0.3 23 | UNIT_Y_START = 0.05 24 | UNIT_Y_END = 0.80 25 | 26 | def __init__(self, model_path, cards): 27 | super().__init__(model_path) 28 | self.cards = cards 29 | 30 | self.side_detector = SideDetector( 31 | os.path.join(MODELS_DIR, "side.onnx") 32 | ) 33 | self.possible_ally_names = self._get_possible_ally_names() 34 | 35 | @staticmethod 36 | def _get_tile_xy(bbox): 37 | x = (bbox[0] + bbox[2]) * DISPLAY_WIDTH / (2 * SCREENSHOT_WIDTH) 38 | y = bbox[3] * DISPLAY_HEIGHT / SCREENSHOT_HEIGHT 39 | tile_x = round(((x - TILE_INIT_X) / TILE_WIDTH) - 0.5) 40 | tile_y = round( 41 | ((DISPLAY_HEIGHT - TILE_INIT_Y - y) / TILE_HEIGHT) - 0.5 42 | ) 43 | return tile_x, tile_y 44 | 45 | def _get_possible_ally_names(self): 46 | possible_ally_names = set() 47 | for card in self.cards: 48 | for unit in card.units: 49 | possible_ally_names.add(unit.name) 50 | return possible_ally_names 51 | 52 | def _calculate_side(self, image, bbox, name): 53 | if name not in self.possible_ally_names: 54 | side = "enemy" 55 | else: 56 | crop = image.crop(bbox) 57 | side = self.side_detector.run(crop) 58 | return side 59 | 60 | def _preprocess(self, image): 61 | image = image.crop( 62 | ( 63 | 0, 64 | self.UNIT_Y_START * image.height, 65 | image.width, 66 | self.UNIT_Y_END * image.height, 67 | ) 68 | ) 69 | image, padding = self.resize_pad_transpose_and_scale(image) 70 | image = np.expand_dims(image, axis=0) 71 | return image, padding 72 | 73 | def _post_process(self, pred, height, image): 74 | pred[:, [1, 3]] *= self.UNIT_Y_END - self.UNIT_Y_START 75 | pred[:, [1, 3]] += self.UNIT_Y_START * height 76 | 77 | allies = [] 78 | enemies = [] 79 | for p in pred: 80 | l, t, r, b, conf, cls = p 81 | bbox = (round(l), round(t), round(r), round(b)) 82 | tile_x, tile_y = self._get_tile_xy(bbox) 83 | position = Position(bbox, conf, tile_x, tile_y) 84 | unit = DETECTOR_UNITS[int(cls)] 85 | unit_detection = UnitDetection(unit, position) 86 | 87 | side = self._calculate_side(image, bbox, unit.name) 88 | if side == "ally": 89 | allies.append(unit_detection) 90 | else: 91 | enemies.append(unit_detection) 92 | 93 | return allies, enemies 94 | 95 | def run(self, image): 96 | height, width = image.height, image.width 97 | np_image, padding = self._preprocess(image) 98 | pred = self._infer(np_image)[0] 99 | pred = pred[pred[:, 4] > self.MIN_CONF] 100 | pred = self.fix_bboxes(pred, width, height, padding) 101 | allies, enemies = self._post_process(pred, height, image) 102 | return allies, enemies 103 | -------------------------------------------------------------------------------- /clashroyalebuildabot/emulator/__init__.py: -------------------------------------------------------------------------------- 1 | from .emulator import Emulator 2 | 3 | __all__ = ["Emulator"] 4 | -------------------------------------------------------------------------------- /clashroyalebuildabot/emulator/emulator.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=consider-using-with 2 | 3 | import base64 4 | import os 5 | import platform 6 | import subprocess 7 | import threading 8 | import time 9 | import zipfile 10 | 11 | import av 12 | from loguru import logger 13 | from PIL.Image import Image 14 | import requests 15 | from tqdm import tqdm 16 | 17 | from clashroyalebuildabot.constants import ADB_DIR 18 | from clashroyalebuildabot.constants import ADB_PATH 19 | from clashroyalebuildabot.constants import EMULATOR_DIR 20 | from clashroyalebuildabot.constants import SCREENSHOT_HEIGHT 21 | from clashroyalebuildabot.constants import SCREENSHOT_WIDTH 22 | from error_handling import WikifiedError 23 | 24 | 25 | class Emulator: 26 | def __init__(self, device_serial, ip): 27 | self.device_serial = device_serial 28 | self.ip = ip 29 | 30 | self.frame_thread = None 31 | self.video_thread = None 32 | self.frame = None 33 | self.codec = av.codec.CodecContext.create("h264", "r") 34 | self.os_name = platform.system().lower() 35 | 36 | self._install_adb() 37 | self._restart_server() 38 | self.device_serial = self._get_valid_device_serial() 39 | self.width, self.height = self._get_width_and_height() 40 | self._start_recording() 41 | self._start_updating_frame() 42 | 43 | def _get_valid_device_serial(self): 44 | try: 45 | logger.info( 46 | f"Trying to connect to device '{self.device_serial}' from config." 47 | ) 48 | self._run_command(["get-state"]) 49 | logger.info( 50 | f"Successfully connected to the device '{self.device_serial}' from config." 51 | ) 52 | return self.device_serial 53 | except Exception as e: 54 | logger.warning( 55 | f"Device '{self.device_serial}' not found or not accessible: {str(e)}" 56 | ) 57 | logger.warning( 58 | "Trying to find a connected device via adb devices..." 59 | ) 60 | 61 | try: 62 | devices_output = subprocess.check_output( 63 | [ADB_PATH, "devices"] 64 | ).decode("utf-8") 65 | available_devices = [ 66 | line.split()[0] 67 | for line in devices_output.splitlines() 68 | if "\tdevice" in line 69 | ] 70 | 71 | if not available_devices: 72 | raise WikifiedError( 73 | "006", "No connected devices found" 74 | ) from e 75 | 76 | fallback_device_serial = available_devices[0] 77 | logger.info( 78 | f"Using the first available device: {fallback_device_serial}" 79 | ) 80 | return fallback_device_serial 81 | except subprocess.CalledProcessError as adb_error: 82 | logger.error( 83 | f"Failed to execute adb devices: {str(adb_error)}" 84 | ) 85 | raise WikifiedError( 86 | "006", "Could not find a valid device to connect to." 87 | ) from adb_error 88 | 89 | def _start_recording(self): 90 | cmd = ( 91 | f"""#!/bin/bash 92 | while true; do 93 | screenrecord --output-format=h264 --time-limit "179" """ 94 | f"""--size "{self.width}x{self.height}" --bit-rate "5M" - 95 | done\n""" 96 | ) 97 | cmd = base64.standard_b64encode(cmd.encode("utf-8")).decode("utf-8") 98 | cmd = ["echo", cmd, "|", "base64", "-d", "|", "sh"] 99 | cmd = " ".join(cmd) + "\n" 100 | self.video_thread = subprocess.Popen( 101 | [ADB_PATH, "-s", self.device_serial, "shell", cmd], 102 | stderr=subprocess.DEVNULL, 103 | stdout=subprocess.PIPE, 104 | stdin=subprocess.DEVNULL, 105 | bufsize=0, 106 | ) 107 | 108 | def _install_adb(self): 109 | if os.path.isdir(ADB_DIR): 110 | return 111 | 112 | basename = f"platform-tools-latest-{self.os_name}.zip" 113 | zip_path = os.path.join(EMULATOR_DIR, basename) 114 | adb_url = f"https://dl.google.com/android/repository/{basename}" 115 | 116 | response = requests.get(adb_url, stream=True, timeout=60) 117 | response.raise_for_status() 118 | total_size = int(response.headers.get("content-length", 0)) 119 | chunk_size = 1024 * 1024 # 1 MB chunks 120 | total_size_mb = total_size / chunk_size 121 | 122 | with open(zip_path, "wb") as file: 123 | with tqdm( 124 | unit="MB", 125 | desc="Downloading ADB", 126 | unit_scale=True, 127 | unit_divisor=1024, 128 | total=total_size_mb, 129 | ) as pbar: 130 | for chunk in response.iter_content(chunk_size=chunk_size): 131 | if chunk: 132 | size = file.write(chunk) 133 | pbar.update(size / chunk_size) 134 | with zipfile.ZipFile(zip_path, "r") as zip_ref: 135 | zip_ref.extractall(EMULATOR_DIR) 136 | os.remove(zip_path) 137 | 138 | def _run_command(self, command): 139 | command = [ADB_PATH, "-s", self.device_serial, *command] 140 | logger.debug(" ".join(command)) 141 | try: 142 | start_time = time.time() 143 | result = subprocess.run( 144 | command, 145 | cwd=EMULATOR_DIR, 146 | capture_output=True, 147 | check=True, 148 | text=True, 149 | ) 150 | end_time = time.time() 151 | logger.debug( 152 | f"Command executed in {end_time - start_time} seconds" 153 | ) 154 | except subprocess.CalledProcessError as e: 155 | logger.error(str(e)) 156 | logger.error(f"stdout: {e.stdout}") 157 | logger.error(f"stderr: {e.stderr}") 158 | raise WikifiedError("007", "ADB command failed.") from e 159 | 160 | if result.returncode != 0: 161 | logger.error(f"Error executing command: {result.stderr}") 162 | raise WikifiedError( 163 | "007", "ADB command failed." 164 | ) from RuntimeError(result.stderr) 165 | 166 | return result.stdout 167 | 168 | def _restart_server(self): 169 | self._run_command( 170 | [ 171 | "kill-server", 172 | ] 173 | ) 174 | self._run_command(["start-server"]) 175 | 176 | def _update_frame(self): 177 | logger.debug("Starting to update frames...") 178 | for line in iter(self.video_thread.stdout.readline, b""): 179 | try: 180 | last_frame = self._get_last_frame(line) 181 | if not last_frame: 182 | continue 183 | 184 | self.frame = last_frame.reformat( 185 | width=SCREENSHOT_WIDTH, 186 | height=SCREENSHOT_HEIGHT, 187 | format="rgb24", 188 | ).to_image() 189 | 190 | except av.AVError as av_error: 191 | logger.error(f"Error while decoding video stream: {av_error}") 192 | except Exception as e: 193 | logger.error(f"Unexpected error in frame update: {str(e)}") 194 | 195 | def _get_last_frame(self, line): 196 | if not line: 197 | return None 198 | 199 | if self.os_name == "windows": 200 | line = line.replace(b"\r\n", b"\n") 201 | 202 | packets = self.codec.parse(line) 203 | if not packets: 204 | return None 205 | 206 | frames = self.codec.decode(packets[-1]) 207 | if not frames: 208 | return None 209 | return frames[-1] 210 | 211 | def _start_updating_frame(self): 212 | self.frame_thread = threading.Thread(target=self._update_frame) 213 | self.frame_thread.daemon = True 214 | self.frame_thread.start() 215 | 216 | def _get_width_and_height(self): 217 | window_size = self._run_command(["shell", "wm", "size"]) 218 | window_size = window_size.replace("Physical size: ", "") 219 | width, height = tuple(int(i) for i in window_size.split("x")) 220 | return width, height 221 | 222 | def stop_game(self): 223 | self._run_command( 224 | ["shell", "am", "force-stop", "com.supercell.clashroyale"] 225 | ) 226 | 227 | def start_game(self): 228 | self._run_command( 229 | [ 230 | "shell", 231 | "am", 232 | "start", 233 | "-n", 234 | "com.supercell.clashroyale/com.supercell.titan.GameApp", 235 | ] 236 | ) 237 | 238 | def click(self, x, y): 239 | self._run_command(["shell", "input", "tap", str(x), str(y)]) 240 | 241 | def take_screenshot(self) -> Image: 242 | logger.debug("Starting to take screenshot...") 243 | while self.frame is None: 244 | time.sleep(0.01) 245 | continue 246 | 247 | screenshot, self.frame = self.frame, None 248 | 249 | return screenshot 250 | 251 | def load_deck(self, cards): 252 | id_str = ";".join([str(card.id_) for card in cards]) 253 | slot_str = ";".join("0" for _ in range(len(cards))) 254 | url = "&".join( 255 | [ 256 | f"https://link.clashroyale.com/en/?clashroyale://copyDeck?deck={id_str}", 257 | f"slots={slot_str}", 258 | "tt=159000000", 259 | "l=Royals", 260 | "id=JR2RU0L90", 261 | ] 262 | ) 263 | 264 | self._run_command( 265 | [ 266 | "shell", 267 | "am", 268 | "start", 269 | "-n", 270 | "com.android.chrome/com.google.android.apps.chrome.Main", 271 | "-d", 272 | f"'{url}'", 273 | ] 274 | ) 275 | input("Press a key when you've finished copying the deck ") 276 | -------------------------------------------------------------------------------- /clashroyalebuildabot/gui/animations.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QEasingCurve 2 | from PyQt6.QtCore import QPropertyAnimation 3 | from PyQt6.QtCore import Qt 4 | from PyQt6.QtWidgets import QGraphicsDropShadowEffect 5 | 6 | 7 | def start_play_button_animation(self): 8 | self.glow_effect = QGraphicsDropShadowEffect(self) 9 | self.glow_effect.setBlurRadius( 10 | 10 11 | ) # Initial blur radius for the glow effect 12 | self.glow_effect.setColor(Qt.GlobalColor.cyan) 13 | self.glow_effect.setOffset( 14 | 0, 0 15 | ) # Center the shadow to create the glow effect 16 | self.start_stop_button.setGraphicsEffect(self.glow_effect) 17 | 18 | _start_glow_animation(self) 19 | 20 | 21 | def _start_glow_animation(self): 22 | """Create a glow effect animation.""" 23 | self.glow_animation = QPropertyAnimation(self.glow_effect, b"blurRadius") 24 | self.glow_animation.setStartValue(0) 25 | self.glow_animation.setEndValue(25) 26 | self.glow_animation.setDuration(2000) 27 | self.glow_animation.setEasingCurve(QEasingCurve.Type.SineCurve) 28 | self.glow_animation.setLoopCount(-1) 29 | self.glow_animation.start() 30 | -------------------------------------------------------------------------------- /clashroyalebuildabot/gui/gameplay_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtGui import QImage 2 | from PyQt6.QtGui import QPixmap 3 | from PyQt6.QtWidgets import QLabel 4 | from PyQt6.QtWidgets import QVBoxLayout 5 | from PyQt6.QtWidgets import QWidget 6 | 7 | 8 | class ImageStreamWindow(QWidget): 9 | def __init__(self): 10 | super().__init__() 11 | 12 | self.image = QLabel(self) 13 | self.inactiveIndicator = QLabel(self) 14 | self.inactiveIndicator.setText( 15 | "The visualizer is disabled. Enable it in the Settings tab." 16 | ) 17 | self.inactiveIndicator.setStyleSheet( 18 | " ".join( 19 | [ 20 | "background-color: #FFA500;", 21 | "color: white;", 22 | "padding: 5px;", 23 | "height: fit-content;", 24 | "width: fit-content;", 25 | ] 26 | ) 27 | ) 28 | self.inactiveIndicator.setMaximumHeight(30) 29 | layout = QVBoxLayout() 30 | layout.addWidget(self.inactiveIndicator) 31 | layout.addWidget(self.image) 32 | self.setLayout(layout) 33 | 34 | def update_frame(self, annotated_image): 35 | height, width, _ = annotated_image.shape 36 | bytes_per_line = 3 * width 37 | q_image = QImage( 38 | annotated_image.data.tobytes(), 39 | width, 40 | height, 41 | bytes_per_line, 42 | QImage.Format.Format_RGB888, 43 | ) 44 | 45 | pixmap = QPixmap.fromImage(q_image) 46 | self.image.setPixmap(pixmap) 47 | 48 | def update_active_state(self, active): 49 | if not active: 50 | self.inactiveIndicator.show() 51 | else: 52 | self.inactiveIndicator.hide() 53 | self.image.clear() 54 | -------------------------------------------------------------------------------- /clashroyalebuildabot/gui/layout_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from PyQt6.QtCore import Qt 4 | from PyQt6.QtGui import QFont 5 | from PyQt6.QtGui import QPixmap 6 | from PyQt6.QtWidgets import QCheckBox 7 | from PyQt6.QtWidgets import QComboBox 8 | from PyQt6.QtWidgets import QDoubleSpinBox 9 | from PyQt6.QtWidgets import QFormLayout 10 | from PyQt6.QtWidgets import QFrame 11 | from PyQt6.QtWidgets import QGridLayout 12 | from PyQt6.QtWidgets import QGroupBox 13 | from PyQt6.QtWidgets import QHBoxLayout 14 | from PyQt6.QtWidgets import QLabel 15 | from PyQt6.QtWidgets import QLineEdit 16 | from PyQt6.QtWidgets import QPushButton 17 | from PyQt6.QtWidgets import QTabWidget 18 | from PyQt6.QtWidgets import QTextEdit 19 | from PyQt6.QtWidgets import QVBoxLayout 20 | from PyQt6.QtWidgets import QWidget 21 | 22 | from clashroyalebuildabot.constants import IMAGES_DIR 23 | from clashroyalebuildabot.gui.gameplay_widget import ImageStreamWindow 24 | from clashroyalebuildabot.gui.utils import save_config 25 | 26 | 27 | def setup_top_bar(main_window): 28 | top_bar = QFrame() 29 | top_bar.setStyleSheet("background-color: #1E272E;") 30 | top_bar_layout = QHBoxLayout(top_bar) 31 | 32 | logo_text_layout = QHBoxLayout() 33 | 34 | logo_label = QLabel() 35 | 36 | logo_pixmap = QPixmap(os.path.join(IMAGES_DIR, "logo.png")).scaled( 37 | 120, 38 | 120, 39 | Qt.AspectRatioMode.KeepAspectRatio, 40 | Qt.TransformationMode.SmoothTransformation, 41 | ) 42 | logo_label.setPixmap(logo_pixmap) 43 | logo_label.setFixedSize(120, 120) 44 | logo_text_layout.addWidget(logo_label) 45 | 46 | text_layout = QVBoxLayout() 47 | text_layout.setAlignment(Qt.AlignmentFlag.AlignTop) 48 | 49 | server_name = QLabel("Clash Royale Build-A-Bot") 50 | server_name.setStyleSheet( 51 | "font-weight: bold; font-size: 16pt; color: white;" 52 | ) 53 | text_layout.addWidget(server_name) 54 | 55 | url = "https://github.com/Pbatch/ClashRoyaleBuildABot" 56 | server_details = QLabel(f'{url}') 57 | server_details.setOpenExternalLinks(True) 58 | server_details.setStyleSheet("color: #57A6FF;") 59 | text_layout.addWidget(server_details) 60 | 61 | main_window.server_id_label = QLabel("Status: Stopped") 62 | main_window.server_id_label.setStyleSheet("color: #999;") 63 | text_layout.addWidget(main_window.server_id_label) 64 | 65 | logo_text_layout.addLayout(text_layout) 66 | 67 | top_bar_layout.addLayout(logo_text_layout) 68 | 69 | right_layout = QVBoxLayout() 70 | right_layout.setAlignment(Qt.AlignmentFlag.AlignRight) 71 | 72 | button_layout = QHBoxLayout() 73 | button_layout.setAlignment(Qt.AlignmentFlag.AlignRight) 74 | 75 | main_window.play_pause_button = QPushButton("⏸️") 76 | main_window.play_pause_button.setFont(QFont("Arial", 18)) 77 | main_window.play_pause_button.setStyleSheet( 78 | """ 79 | QPushButton { 80 | background-color: #4B6EAF; 81 | color: white; 82 | min-width: 32px; 83 | max-width: 32px; 84 | min-height: 32px; 85 | max-height: 32px; 86 | border-radius: 5px; 87 | } 88 | QPushButton:hover { 89 | background-color: #5C7EBF; 90 | } 91 | """ 92 | ) 93 | main_window.play_pause_button.clicked.connect( 94 | main_window.toggle_pause_resume_and_display 95 | ) 96 | 97 | button_layout.addWidget(main_window.play_pause_button) 98 | main_window.play_pause_button.hide() 99 | 100 | main_window.start_stop_button = QPushButton("▶") 101 | main_window.start_stop_button.setFont(QFont("Arial", 18)) 102 | main_window.start_stop_button.setStyleSheet( 103 | """ 104 | QPushButton { 105 | background-color: #4B6EAF; 106 | color: white; 107 | min-width: 32px; 108 | max-width: 32px; 109 | min-height: 32px; 110 | max-height: 32px; 111 | border-radius: 5px; 112 | } 113 | QPushButton:hover { 114 | background-color: #5C7EBF; 115 | } 116 | """ 117 | ) 118 | main_window.start_stop_button.clicked.connect( 119 | main_window.toggle_start_stop 120 | ) 121 | 122 | button_layout.addWidget(main_window.start_stop_button) 123 | 124 | top_bar_layout.addStretch() 125 | top_bar_layout.addLayout(right_layout) 126 | top_bar_layout.addLayout(button_layout) 127 | 128 | return top_bar 129 | 130 | 131 | def setup_tabs(main_window): 132 | tab_widget = QTabWidget() 133 | tab_widget.setStyleSheet( 134 | """ 135 | QTabWidget::pane { border: 0; } 136 | QTabBar::tab { 137 | background: #333; 138 | color: white; 139 | padding: 8px; 140 | margin-bottom: -1px; 141 | } 142 | QTabBar::tab:selected { 143 | background: #1E272E; 144 | border-bottom: 2px solid #57A6FF; 145 | } 146 | """ 147 | ) 148 | 149 | logs_tab = QWidget() 150 | logs_layout = QVBoxLayout(logs_tab) 151 | main_window.log_display = QTextEdit() 152 | main_window.log_display.setReadOnly(True) 153 | main_window.log_display.setStyleSheet( 154 | "background-color: #1e1e1e; color: lightgrey; font-family: monospace;" 155 | ) 156 | logs_layout.addWidget(main_window.log_display) 157 | tab_widget.addTab(logs_tab, "Logs") 158 | 159 | main_window.visualize_tab = ImageStreamWindow() 160 | main_window.visualize_tab.update_active_state( 161 | main_window.config["visuals"]["show_images"] 162 | ) 163 | tab_widget.addTab(main_window.visualize_tab, "Visualize") 164 | 165 | settings_tab = QWidget() 166 | settings_layout = QGridLayout(settings_tab) 167 | 168 | bot_group = QGroupBox("Bot ") 169 | bot_layout = QFormLayout() 170 | main_window.log_level_dropdown = QComboBox() 171 | main_window.log_level_dropdown.addItems( 172 | ["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"] 173 | ) 174 | main_window.log_level_dropdown.setCurrentText( 175 | main_window.config["bot"]["log_level"] 176 | ) 177 | bot_layout.addRow("Log Level:", main_window.log_level_dropdown) 178 | 179 | main_window.adb_ip_input = QLineEdit() 180 | main_window.adb_ip_input.setText(main_window.config["adb"]["ip"]) 181 | main_window.device_serial_input = QLineEdit() 182 | main_window.device_serial_input.setText( 183 | main_window.config["adb"]["device_serial"] 184 | ) 185 | bot_layout.addRow("ADB IP Address:", main_window.adb_ip_input) 186 | bot_layout.addRow("Device Serial:", main_window.device_serial_input) 187 | 188 | bot_group.setLayout(bot_layout) 189 | 190 | visuals_group = QGroupBox("Visuals Settings") 191 | visuals_layout = QFormLayout() 192 | main_window.save_labels_checkbox = QCheckBox( 193 | "Save labels to /debug folder" 194 | ) 195 | main_window.save_labels_checkbox.setChecked( 196 | main_window.config["visuals"]["save_labels"] 197 | ) 198 | main_window.save_images_checkbox = QCheckBox( 199 | "Save labeled images to /debug folder" 200 | ) 201 | main_window.save_images_checkbox.setChecked( 202 | main_window.config["visuals"]["save_images"] 203 | ) 204 | main_window.show_images_checkbox = QCheckBox("Enable visualizer") 205 | main_window.show_images_checkbox.setChecked( 206 | main_window.config["visuals"]["show_images"] 207 | ) 208 | visuals_layout.addRow(main_window.save_labels_checkbox) 209 | visuals_layout.addRow(main_window.save_images_checkbox) 210 | visuals_layout.addRow(main_window.show_images_checkbox) 211 | visuals_group.setLayout(visuals_layout) 212 | 213 | save_config_group = QGroupBox() 214 | save_config_layout = QHBoxLayout() 215 | save_config_button = QPushButton("Save config to file") 216 | save_config_button.clicked.connect( 217 | lambda: save_config(main_window.update_config()) 218 | ) 219 | save_config_layout.addWidget(save_config_button) 220 | save_config_group.setLayout(save_config_layout) 221 | save_config_group.setMaximumHeight(50) 222 | 223 | ingame_group = QGroupBox("Ingame Settings") 224 | ingame_layout = QFormLayout() 225 | 226 | main_window.play_action_delay_input = QDoubleSpinBox() 227 | main_window.play_action_delay_input.setRange(0.1, 5.0) 228 | main_window.play_action_delay_input.setSingleStep(0.1) 229 | main_window.play_action_delay_input.setValue( 230 | main_window.config["ingame"]["play_action"] 231 | ) 232 | ingame_layout.addRow( 233 | "Action Delay (sec):", main_window.play_action_delay_input 234 | ) 235 | 236 | main_window.load_deck_checkbox = QCheckBox("Load deck on startup") 237 | main_window.load_deck_checkbox.setChecked( 238 | main_window.config["bot"]["load_deck"] 239 | ) 240 | ingame_layout.addRow(main_window.load_deck_checkbox) 241 | 242 | main_window.auto_start_game_checkbox = QCheckBox("Auto start games") 243 | main_window.auto_start_game_checkbox.setChecked( 244 | main_window.config["bot"]["auto_start_game"] 245 | ) 246 | ingame_layout.addRow(main_window.auto_start_game_checkbox) 247 | 248 | ingame_group.setLayout(ingame_layout) 249 | 250 | settings_layout.addWidget(bot_group, 0, 0) 251 | settings_layout.addWidget(visuals_group, 1, 0) 252 | settings_layout.addWidget(save_config_group, 2, 0) 253 | settings_layout.addWidget(ingame_group, 0, 1, 3, 1) 254 | 255 | tab_widget.addTab(settings_tab, "Settings") 256 | 257 | return tab_widget 258 | -------------------------------------------------------------------------------- /clashroyalebuildabot/gui/log_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from PyQt6.QtCore import Q_ARG 4 | from PyQt6.QtCore import QMetaObject 5 | from PyQt6.QtCore import Qt 6 | 7 | 8 | class QTextEditLogger(logging.Handler): 9 | def __init__(self, text_edit): 10 | super().__init__() 11 | self.text_edit = text_edit 12 | 13 | def emit(self, record): 14 | log_entry = self.format(record) 15 | QMetaObject.invokeMethod( 16 | self.text_edit, 17 | "append", 18 | Qt.ConnectionType.QueuedConnection, 19 | Q_ARG(str, log_entry), 20 | ) 21 | -------------------------------------------------------------------------------- /clashroyalebuildabot/gui/main_window.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | from loguru import logger 4 | from PyQt6.QtCore import Qt 5 | from PyQt6.QtGui import QIcon 6 | from PyQt6.QtGui import QPixmap 7 | from PyQt6.QtWidgets import QApplication 8 | from PyQt6.QtWidgets import QMainWindow 9 | from PyQt6.QtWidgets import QVBoxLayout 10 | from PyQt6.QtWidgets import QWidget 11 | 12 | from clashroyalebuildabot import Bot 13 | from clashroyalebuildabot.bot.bot import pause_event 14 | from clashroyalebuildabot.gui.animations import start_play_button_animation 15 | from clashroyalebuildabot.gui.layout_setup import setup_tabs 16 | from clashroyalebuildabot.gui.layout_setup import setup_top_bar 17 | from clashroyalebuildabot.gui.styles import set_styles 18 | from clashroyalebuildabot.utils.logger import colorize_log 19 | from clashroyalebuildabot.utils.logger import setup_logger 20 | from error_handling import WikifiedError 21 | 22 | 23 | class MainWindow(QMainWindow): 24 | def __init__(self, config, actions): 25 | try: 26 | super().__init__() 27 | self.config = config 28 | self.actions = actions 29 | self.bot = None 30 | self.bot_thread = None 31 | self.is_running = False 32 | 33 | self.setWindowTitle(" ") 34 | self.setGeometry(100, 100, 900, 600) 35 | 36 | transparent_pixmap = QPixmap(1, 1) 37 | transparent_pixmap.fill(Qt.GlobalColor.transparent) 38 | self.setWindowIcon(QIcon(transparent_pixmap)) 39 | 40 | main_widget = QWidget(self) 41 | self.setCentralWidget(main_widget) 42 | main_layout = QVBoxLayout(main_widget) 43 | 44 | top_bar = setup_top_bar(self) 45 | tab_widget = setup_tabs(self) 46 | 47 | main_layout.addWidget(top_bar) 48 | main_layout.addWidget(tab_widget) 49 | 50 | set_styles(self) 51 | start_play_button_animation(self) 52 | except Exception as e: 53 | raise WikifiedError("004", "Error in GUI initialization.") from e 54 | 55 | def log_handler_function(self, message): 56 | formatted_message = colorize_log(message) 57 | self.log_display.append(formatted_message) 58 | QApplication.processEvents() 59 | self.log_display.verticalScrollBar().setValue( 60 | self.log_display.verticalScrollBar().maximum() 61 | ) 62 | 63 | def toggle_start_stop(self): 64 | if self.is_running: 65 | self.stop_bot() 66 | self.glow_animation.start() 67 | else: 68 | self.start_bot() 69 | self.glow_animation.stop() 70 | 71 | def toggle_pause_resume_and_display(self): 72 | if not self.bot: 73 | return 74 | if pause_event.is_set(): 75 | self.play_pause_button.setText("▶") 76 | else: 77 | self.play_pause_button.setText("⏸️") 78 | self.bot.pause_or_resume() 79 | 80 | def start_bot(self): 81 | if self.is_running: 82 | return 83 | self.update_config() 84 | self.is_running = True 85 | self.bot_thread = Thread(target=self.bot_task) 86 | self.bot_thread.daemon = True 87 | self.bot_thread.start() 88 | self.start_stop_button.setText("■") 89 | self.play_pause_button.show() 90 | self.server_id_label.setText("Status - Running") 91 | logger.info("Starting bot") 92 | 93 | def stop_bot(self): 94 | if self.bot: 95 | self.bot.stop() 96 | self.is_running = False 97 | self.start_stop_button.setText("▶") 98 | self.play_pause_button.hide() 99 | self.server_id_label.setText("Status - Stopped") 100 | logger.info("Bot stopped") 101 | 102 | def restart_bot(self): 103 | if self.is_running: 104 | self.stop_bot() 105 | self.update_config() 106 | self.start_bot() 107 | 108 | def update_config(self) -> dict: 109 | self.config["visuals"][ 110 | "save_labels" 111 | ] = self.save_labels_checkbox.isChecked() 112 | self.config["visuals"][ 113 | "save_images" 114 | ] = self.save_images_checkbox.isChecked() 115 | self.config["visuals"][ 116 | "show_images" 117 | ] = self.show_images_checkbox.isChecked() 118 | self.visualize_tab.update_active_state( 119 | self.config["visuals"]["show_images"] 120 | ) 121 | self.config["bot"]["load_deck"] = self.load_deck_checkbox.isChecked() 122 | self.config["bot"][ 123 | "auto_start_game" 124 | ] = self.auto_start_game_checkbox.isChecked() 125 | log_level_changed = ( 126 | self.config["bot"]["log_level"] 127 | != self.log_level_dropdown.currentText() 128 | ) 129 | self.config["bot"]["log_level"] = self.log_level_dropdown.currentText() 130 | if log_level_changed: 131 | setup_logger(self, self.config) 132 | self.config["ingame"]["play_action"] = round( 133 | float(self.play_action_delay_input.value()), 2 134 | ) 135 | self.config["adb"]["ip"] = self.adb_ip_input.text() 136 | self.config["adb"]["device_serial"] = self.device_serial_input.text() 137 | return self.config 138 | 139 | def bot_task(self): 140 | try: 141 | self.bot = Bot(actions=self.actions, config=self.config) 142 | self.bot.visualizer.frame_ready.connect( 143 | self.visualize_tab.update_frame 144 | ) 145 | self.bot.run() 146 | self.stop_bot() 147 | except WikifiedError: 148 | self.stop_bot() 149 | raise 150 | except Exception as e: 151 | self.stop_bot() 152 | raise WikifiedError("003", "Bot crashed.") from e 153 | 154 | def append_log(self, message): 155 | self.log_display.append(message) 156 | -------------------------------------------------------------------------------- /clashroyalebuildabot/gui/styles.py: -------------------------------------------------------------------------------- 1 | def set_styles(window): 2 | window.setStyleSheet( 3 | """ 4 | QMainWindow { 5 | background-color: #0D1117; 6 | } 7 | QLabel { 8 | color: white; 9 | padding: 2px; 10 | } 11 | QPushButton { 12 | border: none; 13 | padding: 8px; 14 | } 15 | QFrame { 16 | background-color: #1E272E; 17 | } 18 | QGroupBox { 19 | color: white; 20 | } 21 | QCheckBox { 22 | color: white; 23 | } 24 | QPushButton { 25 | color: white; 26 | } 27 | """ 28 | ) 29 | -------------------------------------------------------------------------------- /clashroyalebuildabot/gui/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import yaml 4 | 5 | from clashroyalebuildabot.constants import SRC_DIR 6 | from error_handling import WikifiedError 7 | 8 | 9 | def load_config(): 10 | try: 11 | config_path = os.path.join(SRC_DIR, "config.yaml") 12 | with open(config_path, encoding="utf-8") as file: 13 | return yaml.safe_load(file) 14 | except Exception as e: 15 | raise WikifiedError("002", "Can't parse config.") from e 16 | 17 | 18 | def save_config(config): 19 | try: 20 | config_path = os.path.join(SRC_DIR, "config.yaml") 21 | with open(config_path, "w", encoding="utf-8") as file: 22 | yaml.dump(config, file) 23 | except Exception as e: 24 | raise WikifiedError("000", "Can't save config.") from e 25 | -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/archer_queen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/archer_queen.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/archers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/archers.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/archers_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/archers_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/arrows.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/arrows.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/baby_dragon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/baby_dragon.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/balloon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/balloon.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/bandit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/bandit.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/barbarian_barrel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/barbarian_barrel.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/barbarian_hut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/barbarian_hut.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/barbarian_launcher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/barbarian_launcher.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/barbarians.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/barbarians.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/barbarians_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/barbarians_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/bats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/bats.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/bats_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/bats_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/battle_healer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/battle_healer.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/battle_ram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/battle_ram.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/blank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/blank.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/bomb_tower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/bomb_tower.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/bomber.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/bomber.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/bowler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/bowler.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/cannon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/cannon.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/cannon_cart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/cannon_cart.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/card_champion_unknown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/card_champion_unknown.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/card_legendary_unknown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/card_legendary_unknown.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/clone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/clone.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/dark_prince.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/dark_prince.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/dart_goblin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/dart_goblin.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/earthquake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/earthquake.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/electro_dragon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/electro_dragon.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/electro_giant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/electro_giant.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/electro_spirit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/electro_spirit.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/electro_wizard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/electro_wizard.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/elite_barbarians.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/elite_barbarians.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/elixir_collector.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/elixir_collector.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/elixir_golem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/elixir_golem.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/executioner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/executioner.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/fire_spirit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/fire_spirit.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/fire_spirits.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/fire_spirits.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/fireball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/fireball.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/firecracker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/firecracker.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/firecracker_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/firecracker_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/fisherman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/fisherman.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/flying_machine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/flying_machine.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/freeze.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/freeze.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/furnace.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/furnace.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/giant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/giant.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/giant_skeleton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/giant_skeleton.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/giant_snowball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/giant_snowball.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/goblin_barrel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/goblin_barrel.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/goblin_cage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/goblin_cage.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/goblin_drill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/goblin_drill.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/goblin_gang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/goblin_gang.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/goblin_giant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/goblin_giant.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/goblin_hut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/goblin_hut.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/goblins.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/goblins.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/golden_knight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/golden_knight.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/golem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/golem.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/graveyard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/graveyard.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/guards.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/guards.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/heal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/heal.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/heal_spirit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/heal_spirit.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/hog_rider.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/hog_rider.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/hunter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/hunter.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/ice_golem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/ice_golem.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/ice_spirit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/ice_spirit.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/ice_spirit_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/ice_spirit_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/ice_wizard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/ice_wizard.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/inferno_dragon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/inferno_dragon.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/inferno_tower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/inferno_tower.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/knight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/knight.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/knight_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/knight_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/lava_hound.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/lava_hound.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/lightning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/lightning.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/little_prince.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/little_prince.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/lumberjack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/lumberjack.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/magic_archer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/magic_archer.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/mega_knight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/mega_knight.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/mega_minion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/mega_minion.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/mighty_miner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/mighty_miner.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/miner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/miner.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/minion_horde.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/minion_horde.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/minions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/minions.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/minipekka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/minipekka.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/mirror.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/mirror.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/monk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/monk.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/mortar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/mortar.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/mortar_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/mortar_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/mother_witch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/mother_witch.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/musketeer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/musketeer.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/night_witch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/night_witch.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/party_hut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/party_hut.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/party_rocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/party_rocket.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/pekka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/pekka.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/phoenix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/phoenix.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/poison.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/poison.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/prince.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/prince.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/princess.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/princess.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/rage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/rage.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/raging_prince.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/raging_prince.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/ram_rider.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/ram_rider.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/rascals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/rascals.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/rocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/rocket.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/royal_delivery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/royal_delivery.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/royal_ghost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/royal_ghost.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/royal_giant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/royal_giant.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/royal_giant_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/royal_giant_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/royal_hogs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/royal_hogs.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/royal_recruits.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/royal_recruits.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/royal_recruits_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/royal_recruits_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/santa_hog_rider.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/santa_hog_rider.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/skeleton_army.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/skeleton_army.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/skeleton_barrel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/skeleton_barrel.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/skeleton_dragons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/skeleton_dragons.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/skeleton_king.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/skeleton_king.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/skeletons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/skeletons.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/skeletons_ev1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/skeletons_ev1.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/sparky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/sparky.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/spear_goblins.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/spear_goblins.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/super_archers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/super_archers.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/super_hog_rider.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/super_hog_rider.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/super_ice_golem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/super_ice_golem.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/super_lava_hound.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/super_lava_hound.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/super_magic_archer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/super_magic_archer.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/super_mini_pekka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/super_mini_pekka.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/super_witch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/super_witch.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/terry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/terry.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/tesla.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/tesla.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/the_log.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/the_log.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/three_musketeers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/three_musketeers.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/tombstone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/tombstone.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/tornado.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/tornado.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/valkyrie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/valkyrie.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/wall_breakers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/wall_breakers.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/warmth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/warmth.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/witch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/witch.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/wizard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/wizard.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/x_bow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/x_bow.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/zap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/zap.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/cards/zappies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/cards/zappies.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/icon.ico -------------------------------------------------------------------------------- /clashroyalebuildabot/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/logo.png -------------------------------------------------------------------------------- /clashroyalebuildabot/images/screen/end_of_game.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/screen/end_of_game.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/screen/in_game.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/screen/in_game.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/images/screen/lobby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/images/screen/lobby.jpg -------------------------------------------------------------------------------- /clashroyalebuildabot/models/side.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/models/side.onnx -------------------------------------------------------------------------------- /clashroyalebuildabot/models/units_M_480x352.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pbatch/ClashRoyaleBuildABot/a43674bcf7ebcf0ad9bbbff453616cadc110591b/clashroyalebuildabot/models/units_M_480x352.onnx -------------------------------------------------------------------------------- /clashroyalebuildabot/namespaces/__init__.py: -------------------------------------------------------------------------------- 1 | # Exports for data submodule 2 | from .cards import Cards 3 | from .screens import Screens 4 | from .state import State 5 | from .units import Units 6 | 7 | __all__ = ["Cards", "Units", "State", "Screens"] 8 | -------------------------------------------------------------------------------- /clashroyalebuildabot/namespaces/cards.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | from dataclasses import dataclass 3 | from typing import List 4 | 5 | from clashroyalebuildabot.namespaces.units import Unit 6 | from clashroyalebuildabot.namespaces.units import Units 7 | 8 | 9 | @dataclass(frozen=True) 10 | class Card: 11 | name: str 12 | target_anywhere: bool 13 | cost: int 14 | units: List[Unit] 15 | id_: int 16 | 17 | def __hash__(self): 18 | return hash(self.name) 19 | 20 | 21 | @dataclass(frozen=True) 22 | class _CardsNamespace: 23 | ARCHER_QUEEN: Card = Card( 24 | "archer_queen", False, 5, [Units.ARCHER_QUEEN], 26000072 25 | ) 26 | ARCHERS: Card = Card("archers", False, 3, [Units.ARCHER], 26000001) 27 | ARROWS: Card = Card("arrows", True, 3, [], 28000001) 28 | BABY_DRAGON: Card = Card( 29 | "baby_dragon", False, 4, [Units.BABY_DRAGON], 26000015 30 | ) 31 | BALLOON: Card = Card("balloon", False, 5, [Units.BALLOON], 26000006) 32 | BANDIT: Card = Card("bandit", False, 3, [Units.BANDIT], 26000046) 33 | BARBARIAN_BARREL: Card = Card( 34 | "barbarian_barrel", True, 2, [Units.BARBARIAN], 28000015 35 | ) 36 | BARBARIAN_HUT: Card = Card( 37 | "barbarian_hut", 38 | False, 39 | 7, 40 | [Units.BARBARIAN_HUT, Units.BARBARIAN], 41 | 27000005, 42 | ) 43 | BARBARIANS: Card = Card( 44 | "barbarians", False, 5, [Units.BARBARIAN], 26000008 45 | ) 46 | BATS: Card = Card("bats", False, 2, [Units.BAT], 26000049) 47 | BATTLE_HEALER: Card = Card( 48 | "battle_healer", False, 4, [Units.BATTLE_HEALER], 26000068 49 | ) 50 | BATTLE_RAM: Card = Card( 51 | "battle_ram", False, 4, [Units.BATTLE_RAM], 26000036 52 | ) 53 | BLANK: Card = Card("blank", False, 11, [], -1) 54 | BOMBER: Card = Card("bomber", False, 2, [Units.BOMBER], 26000013) 55 | BOMB_TOWER: Card = Card( 56 | "bomb_tower", False, 4, [Units.BOMB_TOWER], 27000004 57 | ) 58 | BOWLER: Card = Card("bowler", False, 5, [Units.BOWLER], 26000034) 59 | CANNON: Card = Card("cannon", False, 3, [Units.CANNON], 27000000) 60 | CANNON_CART: Card = Card( 61 | "cannon_cart", False, 5, [Units.CANNON_CART], 26000054 62 | ) 63 | CLONE: Card = Card("clone", True, 3, [], 28000013) 64 | DART_GOBLIN: Card = Card( 65 | "dart_goblin", False, 3, [Units.DART_GOBLIN], 26000027 66 | ) 67 | DARK_PRINCE: Card = Card( 68 | "dark_prince", False, 4, [Units.DARK_PRINCE], 26000040 69 | ) 70 | EARTHQUAKE: Card = Card("earthquake", True, 3, [], 28000014) 71 | ELECTRO_DRAGON: Card = Card( 72 | "electro_dragon", False, 5, [Units.ELECTRO_DRAGON], 26000063 73 | ) 74 | ELECTRO_GIANT: Card = Card( 75 | "electro_giant", False, 7, [Units.ELECTRO_GIANT], 26000085 76 | ) 77 | ELECTRO_SPIRIT: Card = Card( 78 | "electro_spirit", False, 1, [Units.ELECTRO_SPIRIT], 26000084 79 | ) 80 | ELECTRO_WIZARD: Card = Card( 81 | "electro_wizard", False, 4, [Units.ELECTRO_WIZARD], 26000042 82 | ) 83 | ELITE_BARBARIANS: Card = Card( 84 | "elite_barbarians", False, 6, [Units.ELITE_BARBARIAN], 26000043 85 | ) 86 | ELIXIR_COLLECTOR: Card = Card( 87 | "elixir_collector", False, 6, [Units.ELIXIR_COLLECTOR], 27000007 88 | ) 89 | ELIXIR_GOLEM: Card = Card( 90 | "elixir_golem", 91 | False, 92 | 3, 93 | [ 94 | Units.ELIXIR_GOLEM_LARGE, 95 | Units.ELIXIR_GOLEM_MEDIUM, 96 | Units.ELIXIR_GOLEM_SMALL, 97 | ], 98 | 26000067, 99 | ) 100 | EXECUTIONER: Card = Card( 101 | "executioner", False, 5, [Units.EXECUTIONER], 26000045 102 | ) 103 | FIRE_SPIRIT: Card = Card( 104 | "fire_spirit", False, 1, [Units.FIRE_SPIRIT], 26000031 105 | ) 106 | FIREBALL: Card = Card("fireball", True, 4, [], 28000000) 107 | FIRECRACKER: Card = Card( 108 | "firecracker", False, 3, [Units.FIRE_CRACKER], 26000064 109 | ) 110 | FISHERMAN: Card = Card("fisherman", False, 3, [Units.FISHERMAN], 26000061) 111 | FLYING_MACHINE: Card = Card( 112 | "flying_machine", False, 4, [Units.FLYING_MACHINE], 26000057 113 | ) 114 | FREEZE: Card = Card("freeze", True, 4, [], 28000005) 115 | FURNACE: Card = Card("furnace", False, 4, [Units.FURNACE], 27000010) 116 | GIANT: Card = Card("giant", False, 5, [Units.GIANT], 26000003) 117 | GIANT_SKELETON: Card = Card( 118 | "giant_skeleton", False, 6, [Units.GIANT_SKELETON], 26000020 119 | ) 120 | GIANT_SNOWBALL: Card = Card("giant_snowball", True, 2, [], 28000017) 121 | GOBLINS: Card = Card("goblins", False, 2, [Units.GOBLIN], 26000002) 122 | GOBLIN_BARREL: Card = Card( 123 | "goblin_barrel", True, 3, [Units.GOBLIN], 28000004 124 | ) 125 | GOBLIN_CAGE: Card = Card( 126 | "goblin_cage", False, 4, [Units.GOBLIN_CAGE, Units.BRAWLER], 27000012 127 | ) 128 | GOBLIN_DRILL: Card = Card( 129 | "goblin_drill", True, 4, [Units.GOBLIN_DRILL, Units.GOBLIN], 27000013 130 | ) 131 | GOBLIN_GANG: Card = Card( 132 | "goblin_gang", False, 3, [Units.GOBLIN, Units.SPEAR_GOBLIN], 26000041 133 | ) 134 | GOBLIN_GIANT: Card = Card( 135 | "goblin_giant", False, 6, [Units.SPEAR_GOBLIN], 26000060 136 | ) 137 | GOBLIN_HUT: Card = Card( 138 | "goblin_hut", 139 | False, 140 | 5, 141 | [Units.GOBLIN_HUT, Units.SPEAR_GOBLIN], 142 | 27000001, 143 | ) 144 | GOLDEN_KNIGHT: Card = Card( 145 | "golden_knight", False, 4, [Units.GOLDEN_KNIGHT], 26000074 146 | ) 147 | GOLEM: Card = Card( 148 | "golem", False, 8, [Units.GOLEM, Units.GOLEMITE], 26000009 149 | ) 150 | GRAVEYARD: Card = Card("graveyard", True, 5, [Units.SKELETON], 28000010) 151 | GUARDS: Card = Card("guards", False, 3, [Units.GUARD], 26000025) 152 | HEAL_SPIRIT: Card = Card( 153 | "heal_spirit", False, 1, [Units.HEAL_SPIRIT], 28000016 154 | ) 155 | HOG_RIDER: Card = Card("hog_rider", False, 4, [Units.HOG_RIDER], 26000021) 156 | HUNTER: Card = Card("hunter", False, 4, [Units.HUNTER], 26000044) 157 | ICE_GOLEM: Card = Card("ice_golem", False, 2, [Units.ICE_GOLEM], 26000038) 158 | ICE_SPIRIT: Card = Card( 159 | "ice_spirit", False, 1, [Units.ICE_SPIRIT], 26000030 160 | ) 161 | ICE_WIZARD: Card = Card( 162 | "ice_wizard", False, 3, [Units.ICE_WIZARD], 26000023 163 | ) 164 | INFERNO_DRAGON: Card = Card( 165 | "inferno_dragon", False, 4, [Units.INFERNO_DRAGON], 26000037 166 | ) 167 | INFERNO_TOWER: Card = Card( 168 | "inferno_tower", False, 5, [Units.INFERNO_TOWER], 27000003 169 | ) 170 | KNIGHT: Card = Card("knight", False, 3, [Units.KNIGHT], 26000000) 171 | LAVA_HOUND: Card = Card( 172 | "lava_hound", False, 7, [Units.LAVA_HOUND, Units.LAVA_PUP], 26000029 173 | ) 174 | LIGHTNING: Card = Card("lightning", True, 6, [], 28000007) 175 | LITTLE_PRINCE: Card = Card( 176 | "little_prince", 177 | False, 178 | 3, 179 | [Units.LITTLE_PRINCE, Units.ROYAL_GUARDIAN], 180 | 26000093, 181 | ) 182 | LUMBERJACK: Card = Card( 183 | "lumberjack", False, 4, [Units.LUMBERJACK], 26000035 184 | ) 185 | MAGIC_ARCHER: Card = Card( 186 | "magic_archer", False, 4, [Units.MAGIC_ARCHER], 26000062 187 | ) 188 | MEGA_KNIGHT: Card = Card( 189 | "mega_knight", False, 7, [Units.MEGA_KNIGHT], 26000055 190 | ) 191 | MEGA_MINION: Card = Card( 192 | "mega_minion", False, 3, [Units.MEGA_MINION], 26000039 193 | ) 194 | MIGHTY_MINER: Card = Card( 195 | "mighty_miner", False, 4, [Units.MIGHTY_MINER], 26000065 196 | ) 197 | MINER: Card = Card("miner", False, 3, [Units.MINER], 26000032) 198 | MINIONS: Card = Card("minions", False, 3, [Units.MINION], 26000005) 199 | MINION_HORDE: Card = Card( 200 | "minion_horde", False, 5, [Units.MINION], 26000022 201 | ) 202 | MINIPEKKA: Card = Card("minipekka", False, 4, [Units.MINIPEKKA], 26000018) 203 | MIRROR: Card = Card("mirror", True, -1, [], 28000006) 204 | MONK: Card = Card("monk", False, 5, [Units.MONK], 26000077) 205 | MORTAR: Card = Card("mortar", False, 4, [Units.MORTAR], 27000002) 206 | MOTHER_WITCH: Card = Card( 207 | "mother_witch", False, 4, [Units.MOTHER_WITCH, Units.HOG], 26000083 208 | ) 209 | MUSKETEER: Card = Card("musketeer", False, 4, [Units.MUSKETEER], 26000014) 210 | NIGHT_WITCH: Card = Card( 211 | "night_witch", False, 4, [Units.NIGHT_WITCH, Units.BAT], 26000048 212 | ) 213 | PEKKA: Card = Card("pekka", False, 7, [Units.PEKKA], 26000004) 214 | PHOENIX: Card = Card( 215 | "phoenix", 216 | False, 217 | 4, 218 | [Units.PHOENIX_LARGE, Units.PHOENIX_EGG, Units.PHOENIX_SMALL], 219 | 26000087, 220 | ) 221 | POISON: Card = Card("poison", True, 4, [], 28000009) 222 | PRINCE: Card = Card("prince", False, 5, [Units.PRINCE], 26000016) 223 | PRINCESS: Card = Card("princess", False, 3, [Units.PRINCESS], 26000026) 224 | RAGE: Card = Card("rage", True, 2, [], 28000002) 225 | RAM_RIDER: Card = Card("ram_rider", False, 5, [Units.RAM_RIDER], 26000051) 226 | RASCALS: Card = Card( 227 | "rascals", False, 5, [Units.RASCAL_BOY, Units.RASCAL_GIRL], 26000053 228 | ) 229 | ROCKET: Card = Card("rocket", True, 6, [], 28000003) 230 | ROYAL_DELIVERY: Card = Card( 231 | "royal_delivery", False, 3, [Units.ROYAL_RECRUIT], 28000018 232 | ) 233 | ROYAL_GHOST: Card = Card( 234 | "royal_ghost", False, 3, [Units.ROYAL_GHOST], 26000050 235 | ) 236 | ROYAL_GIANT: Card = Card( 237 | "royal_giant", False, 6, [Units.ROYAL_GIANT], 26000024 238 | ) 239 | ROYAL_HOGS: Card = Card( 240 | "royal_hogs", False, 5, [Units.ROYAL_HOG], 26000059 241 | ) 242 | ROYAL_RECRUITS: Card = Card( 243 | "royal_recruits", False, 7, [Units.ROYAL_RECRUIT], 26000047 244 | ) 245 | SKELETONS: Card = Card("skeletons", False, 1, [Units.SKELETON], 26000010) 246 | SKELETON_ARMY: Card = Card( 247 | "skeleton_army", False, 3, [Units.SKELETON], 26000012 248 | ) 249 | SKELETON_BARREL: Card = Card( 250 | "skeleton_barrel", False, 3, [Units.SKELETON], 26000056 251 | ) 252 | SKELETON_DRAGONS: Card = Card( 253 | "skeleton_dragons", False, 4, [Units.SKELETON_DRAGON], 26000080 254 | ) 255 | SKELETON_KING: Card = Card( 256 | "skeleton_king", 257 | False, 258 | 4, 259 | [Units.SKELETON_KING, Units.SKELETON], 260 | 26000069, 261 | ) 262 | SPARKY: Card = Card("sparky", False, 6, [Units.SPARKY], 26000033) 263 | SPEAR_GOBLINS: Card = Card( 264 | "spear_goblins", False, 2, [Units.SPEAR_GOBLIN], 26000019 265 | ) 266 | TESLA: Card = Card("tesla", False, 4, [Units.TESLA], 27000006) 267 | THE_LOG: Card = Card("the_log", True, 2, [], 28000011) 268 | THREE_MUSKETEERS: Card = Card( 269 | "three_musketeers", False, 9, [Units.MUSKETEER], 26000028 270 | ) 271 | TOMBSTONE: Card = Card("tombstone", False, 3, [Units.TOMBSTONE], 27000009) 272 | TORNADO: Card = Card("tornado", True, 3, [], 28000012) 273 | VALKYRIE: Card = Card("valkyrie", False, 4, [Units.VALKYRIE], 26000011) 274 | WALL_BREAKERS: Card = Card( 275 | "wall_breakers", False, 2, [Units.WALL_BREAKER], 26000058 276 | ) 277 | WITCH: Card = Card( 278 | "witch", False, 5, [Units.WITCH, Units.SKELETON], 26000007 279 | ) 280 | WIZARD: Card = Card("wizard", False, 5, [Units.WIZARD], 26000017) 281 | X_BOW: Card = Card("x_bow", False, 6, [Units.X_BOW], 27000008) 282 | ZAP: Card = Card("zap", True, 2, [], 28000008) 283 | ZAPPIES: Card = Card("zappies", False, 4, [Units.ZAPPY], 26000052) 284 | 285 | 286 | Cards = _CardsNamespace() 287 | NAME2CARD = dict(asdict(Cards).items()) 288 | -------------------------------------------------------------------------------- /clashroyalebuildabot/namespaces/numbers.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Tuple 3 | 4 | 5 | @dataclass(frozen=True) 6 | class NumberDetection: 7 | bbox: Tuple[int, int, int, int] 8 | number: float 9 | 10 | 11 | @dataclass(frozen=True) 12 | class Numbers: 13 | left_enemy_princess_hp: NumberDetection 14 | right_enemy_princess_hp: NumberDetection 15 | left_ally_princess_hp: NumberDetection 16 | right_ally_princess_hp: NumberDetection 17 | elixir: NumberDetection 18 | -------------------------------------------------------------------------------- /clashroyalebuildabot/namespaces/screens.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | 5 | @dataclass(frozen=True) 6 | class Screen: 7 | name: str 8 | ltrb: Optional[Tuple[float, float, float, float]] 9 | click_xy: Optional[Tuple[int, int]] 10 | 11 | 12 | # coords are scaled to 720x1280 13 | @dataclass(frozen=True) 14 | class _ScreensNamespace: 15 | UNKNOWN: Screen = Screen("unknown", None, None) 16 | IN_GAME: Screen = Screen("in_game", (148, 1254, 163, 1274), None) 17 | LOBBY: Screen = Screen( 18 | "lobby", 19 | (424, 126, 506, 181), 20 | (360, 820), 21 | ) 22 | END_OF_GAME: Screen = Screen( 23 | "end_of_game", 24 | (279, 1095, 440, 1154), 25 | (360, 1125), 26 | ) 27 | 28 | 29 | Screens = _ScreensNamespace() 30 | -------------------------------------------------------------------------------- /clashroyalebuildabot/namespaces/state.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Tuple 3 | 4 | from clashroyalebuildabot.namespaces.cards import Card 5 | from clashroyalebuildabot.namespaces.numbers import Numbers 6 | from clashroyalebuildabot.namespaces.screens import Screen 7 | from clashroyalebuildabot.namespaces.units import UnitDetection 8 | 9 | 10 | @dataclass 11 | class State: 12 | allies: List[UnitDetection] 13 | enemies: List[UnitDetection] 14 | numbers: Numbers 15 | cards: Tuple[Card, Card, Card, Card] 16 | ready: List[int] 17 | screen: Screen 18 | -------------------------------------------------------------------------------- /clashroyalebuildabot/namespaces/units.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | from dataclasses import dataclass 3 | from typing import Literal, Optional, Tuple 4 | 5 | 6 | @dataclass(frozen=True) 7 | class UnitCategory: 8 | TROOP: str = "troop" 9 | BUILDING: str = "building" 10 | 11 | 12 | @dataclass(frozen=True) 13 | class Target: 14 | AIR: str = "air" 15 | GROUND: str = "ground" 16 | BUILDINGS: str = "buildings" 17 | ALL: str = "all" 18 | 19 | 20 | @dataclass(frozen=True) 21 | class Transport: 22 | AIR: str = "air" 23 | GROUND: str = "ground" 24 | 25 | 26 | @dataclass(frozen=True) 27 | class Unit: 28 | name: str 29 | category: Literal[UnitCategory.TROOP, UnitCategory.BUILDING] 30 | target: Optional[Literal[Target.GROUND, Target.BUILDINGS, Target.ALL]] 31 | transport: Optional[Literal[Transport.AIR, Transport.GROUND]] 32 | 33 | 34 | @dataclass(frozen=True) 35 | class Position: 36 | bbox: Tuple[int, int, int, int] 37 | conf: float 38 | tile_x: int 39 | tile_y: int 40 | 41 | 42 | @dataclass(frozen=True) 43 | class UnitDetection: 44 | unit: Unit 45 | position: Position 46 | 47 | 48 | @dataclass(frozen=True) 49 | class _UnitsNamespace: 50 | ARCHER: Unit = Unit( 51 | "archer", UnitCategory.TROOP, Target.ALL, Transport.GROUND 52 | ) 53 | ARCHER_QUEEN: Unit = Unit( 54 | "archer_queen", UnitCategory.TROOP, Target.ALL, Transport.GROUND 55 | ) 56 | BABY_DRAGON: Unit = Unit( 57 | "baby_dragon", UnitCategory.TROOP, Target.ALL, Transport.AIR 58 | ) 59 | BALLOON: Unit = Unit( 60 | "balloon", UnitCategory.TROOP, Target.BUILDINGS, Transport.AIR 61 | ) 62 | BANDIT: Unit = Unit( 63 | "bandit", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 64 | ) 65 | BARBARIAN: Unit = Unit( 66 | "barbarian", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 67 | ) 68 | BARBARIAN_HUT: Unit = Unit( 69 | "barbarian_hut", UnitCategory.BUILDING, None, None 70 | ) 71 | BAT: Unit = Unit("bat", UnitCategory.TROOP, Target.ALL, Transport.AIR) 72 | BATTLE_HEALER: Unit = Unit( 73 | "battle_healer", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 74 | ) 75 | BATTLE_RAM: Unit = Unit( 76 | "battle_ram", 77 | UnitCategory.TROOP, 78 | UnitCategory.BUILDING, 79 | Transport.GROUND, 80 | ) 81 | BOMB_TOWER: Unit = Unit( 82 | "bomb_tower", UnitCategory.BUILDING, Target.GROUND, Transport.GROUND 83 | ) 84 | BOMBER: Unit = Unit( 85 | "bomber", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 86 | ) 87 | BOWLER: Unit = Unit( 88 | "bowler", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 89 | ) 90 | BRAWLER: Unit = Unit( 91 | "brawler", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 92 | ) 93 | CANNON: Unit = Unit( 94 | "cannon", UnitCategory.BUILDING, Target.GROUND, Transport.GROUND 95 | ) 96 | CANNON_CART: Unit = Unit( 97 | "cannon_cart", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 98 | ) 99 | DARK_PRINCE: Unit = Unit( 100 | "dark_prince", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 101 | ) 102 | DART_GOBLIN: Unit = Unit( 103 | "dart_goblin", UnitCategory.TROOP, Target.ALL, Transport.GROUND 104 | ) 105 | ELECTRO_DRAGON: Unit = Unit( 106 | "electro_dragon", UnitCategory.TROOP, Target.ALL, Transport.AIR 107 | ) 108 | ELECTRO_GIANT: Unit = Unit( 109 | "electro_giant", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 110 | ) 111 | ELECTRO_SPIRIT: Unit = Unit( 112 | "electro_spirit", UnitCategory.TROOP, Target.ALL, Transport.GROUND 113 | ) 114 | ELECTRO_WIZARD: Unit = Unit( 115 | "electro_wizard", UnitCategory.TROOP, Target.ALL, Transport.GROUND 116 | ) 117 | ELITE_BARBARIAN: Unit = Unit( 118 | "elite_barbarian", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 119 | ) 120 | ELIXIR_COLLECTOR: Unit = Unit( 121 | "elixir_collector", UnitCategory.BUILDING, None, None 122 | ) 123 | ELIXIR_GOLEM_LARGE: Unit = Unit( 124 | "elixir_golem_large", 125 | UnitCategory.TROOP, 126 | Target.BUILDINGS, 127 | Transport.GROUND, 128 | ) 129 | ELIXIR_GOLEM_MEDIUM: Unit = Unit( 130 | "elixir_golem_medium", 131 | UnitCategory.TROOP, 132 | Target.BUILDINGS, 133 | Transport.GROUND, 134 | ) 135 | ELIXIR_GOLEM_SMALL: Unit = Unit( 136 | "elixir_golem_small", 137 | UnitCategory.TROOP, 138 | Target.BUILDINGS, 139 | Transport.GROUND, 140 | ) 141 | EXECUTIONER: Unit = Unit( 142 | "executioner", UnitCategory.TROOP, Target.ALL, Transport.GROUND 143 | ) 144 | FIRE_SPIRIT: Unit = Unit( 145 | "fire_spirit", UnitCategory.TROOP, Target.ALL, Transport.GROUND 146 | ) 147 | FIRE_CRACKER: Unit = Unit( 148 | "firecracker", UnitCategory.TROOP, Target.ALL, Transport.GROUND 149 | ) 150 | FISHERMAN: Unit = Unit( 151 | "fisherman", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 152 | ) 153 | FLYING_MACHINE: Unit = Unit( 154 | "flying_machine", UnitCategory.TROOP, Target.ALL, Transport.AIR 155 | ) 156 | FURNACE: Unit = Unit("furnace", UnitCategory.BUILDING, None, None) 157 | GIANT: Unit = Unit( 158 | "giant", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 159 | ) 160 | GIANT_SKELETON: Unit = Unit( 161 | "giant_skeleton", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 162 | ) 163 | # This is mistake, remove the snowball next time we train the object detector 164 | GIANT_SNOWBALL: Unit = Unit( 165 | "giant_snowball", UnitCategory.TROOP, None, None 166 | ) 167 | GOBLIN: Unit = Unit( 168 | "goblin", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 169 | ) 170 | GOBLIN_CAGE: Unit = Unit("goblin_cage", UnitCategory.BUILDING, None, None) 171 | GOBLIN_DRILL: Unit = Unit( 172 | "goblin_drill", UnitCategory.BUILDING, Target.GROUND, None 173 | ) 174 | GOBLIN_HUT: Unit = Unit("goblin_hut", UnitCategory.BUILDING, None, None) 175 | GOLDEN_KNIGHT: Unit = Unit( 176 | "golden_knight", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 177 | ) 178 | GOLEM: Unit = Unit( 179 | "golem", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 180 | ) 181 | GOLEMITE: Unit = Unit( 182 | "golemite", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 183 | ) 184 | GUARD: Unit = Unit( 185 | "guard", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 186 | ) 187 | HEAL_SPIRIT: Unit = Unit( 188 | "heal_spirit", UnitCategory.TROOP, Target.ALL, Transport.GROUND 189 | ) 190 | HOG: Unit = Unit( 191 | "hog", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 192 | ) 193 | HOG_RIDER: Unit = Unit( 194 | "hog_rider", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 195 | ) 196 | HUNTER: Unit = Unit( 197 | "hunter", UnitCategory.TROOP, Target.ALL, Transport.GROUND 198 | ) 199 | ICE_GOLEM: Unit = Unit( 200 | "ice_golem", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 201 | ) 202 | ICE_SPIRIT: Unit = Unit( 203 | "ice_spirit", UnitCategory.TROOP, Target.ALL, Transport.GROUND 204 | ) 205 | ICE_WIZARD: Unit = Unit( 206 | "ice_wizard", UnitCategory.TROOP, Target.ALL, Transport.AIR 207 | ) 208 | INFERNO_DRAGON: Unit = Unit( 209 | "inferno_dragon", UnitCategory.TROOP, Target.ALL, Transport.AIR 210 | ) 211 | INFERNO_TOWER: Unit = Unit( 212 | "inferno_tower", UnitCategory.BUILDING, None, None 213 | ) 214 | KNIGHT: Unit = Unit( 215 | "knight", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 216 | ) 217 | LAVA_HOUND: Unit = Unit( 218 | "lava_hound", UnitCategory.TROOP, Target.BUILDINGS, Transport.AIR 219 | ) 220 | LAVA_PUP: Unit = Unit( 221 | "lava_pup", UnitCategory.TROOP, Target.BUILDINGS, Transport.AIR 222 | ) 223 | LITTLE_PRINCE: Unit = Unit( 224 | "little_prince", UnitCategory.TROOP, Target.ALL, Transport.GROUND 225 | ) 226 | LUMBERJACK: Unit = Unit( 227 | "lumberjack", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 228 | ) 229 | MAGIC_ARCHER: Unit = Unit( 230 | "magic_archer", UnitCategory.TROOP, Target.ALL, Transport.GROUND 231 | ) 232 | MEGA_KNIGHT: Unit = Unit( 233 | "mega_knight", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 234 | ) 235 | MEGA_MINION: Unit = Unit( 236 | "mega_minion", UnitCategory.TROOP, Target.ALL, Transport.AIR 237 | ) 238 | MIGHTY_MINER: Unit = Unit( 239 | "mighty_miner", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 240 | ) 241 | MINER: Unit = Unit( 242 | "miner", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 243 | ) 244 | MINION: Unit = Unit( 245 | "minion", UnitCategory.TROOP, Target.ALL, Transport.AIR 246 | ) 247 | MINIPEKKA: Unit = Unit( 248 | "minipekka", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 249 | ) 250 | MONK: Unit = Unit( 251 | "monk", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 252 | ) 253 | MORTAR: Unit = Unit( 254 | "mortar", UnitCategory.BUILDING, Target.GROUND, Transport.GROUND 255 | ) 256 | MOTHER_WITCH: Unit = Unit( 257 | "mother_witch", UnitCategory.TROOP, Target.ALL, Transport.GROUND 258 | ) 259 | MUSKETEER: Unit = Unit( 260 | "musketeer", UnitCategory.TROOP, Target.ALL, Transport.GROUND 261 | ) 262 | NIGHT_WITCH: Unit = Unit( 263 | "night_witch", UnitCategory.TROOP, Target.ALL, Transport.GROUND 264 | ) 265 | PEKKA: Unit = Unit( 266 | "pekka", UnitCategory.TROOP, Target.ALL, Transport.GROUND 267 | ) 268 | PHOENIX_EGG: Unit = Unit( 269 | "phoenix_egg", UnitCategory.TROOP, None, Transport.GROUND 270 | ) 271 | PHOENIX_LARGE: Unit = Unit( 272 | "phoenix_large", UnitCategory.TROOP, Target.ALL, Transport.AIR 273 | ) 274 | PHOENIX_SMALL: Unit = Unit( 275 | "phoenix_small", UnitCategory.TROOP, Target.ALL, Transport.AIR 276 | ) 277 | PRINCE: Unit = Unit( 278 | "prince", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 279 | ) 280 | PRINCESS: Unit = Unit( 281 | "princess", UnitCategory.TROOP, Target.ALL, Transport.GROUND 282 | ) 283 | RAM_RIDER: Unit = Unit( 284 | "ram_rider", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 285 | ) 286 | RASCAL_BOY: Unit = Unit( 287 | "rascal_boy", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 288 | ) 289 | RASCAL_GIRL: Unit = Unit( 290 | "rascal_girl", UnitCategory.TROOP, Target.ALL, Transport.GROUND 291 | ) 292 | ROYAL_GHOST: Unit = Unit( 293 | "royal_ghost", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 294 | ) 295 | ROYAL_GIANT: Unit = Unit( 296 | "royal_giant", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 297 | ) 298 | ROYAL_GUARDIAN: Unit = Unit( 299 | "royal_guardian", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 300 | ) 301 | ROYAL_HOG: Unit = Unit( 302 | "royal_hog", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 303 | ) 304 | ROYAL_RECRUIT: Unit = Unit( 305 | "royal_recruit", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 306 | ) 307 | SKELETON: Unit = Unit( 308 | "skeleton", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 309 | ) 310 | SKELETON_DRAGON: Unit = Unit( 311 | "skeleton_dragon", UnitCategory.TROOP, Target.ALL, Transport.AIR 312 | ) 313 | SKELETON_KING: Unit = Unit( 314 | "skeleton_king", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 315 | ) 316 | SPARKY: Unit = Unit( 317 | "sparky", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 318 | ) 319 | SPEAR_GOBLIN: Unit = Unit( 320 | "spear_goblin", UnitCategory.TROOP, Target.ALL, Transport.GROUND 321 | ) 322 | TESLA: Unit = Unit( 323 | "tesla", UnitCategory.BUILDING, Target.ALL, Transport.GROUND 324 | ) 325 | TOMBSTONE: Unit = Unit("tombstone", UnitCategory.BUILDING, None, None) 326 | VALKYRIE: Unit = Unit( 327 | "valkyrie", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 328 | ) 329 | WALL_BREAKER: Unit = Unit( 330 | "wall_breaker", UnitCategory.TROOP, Target.BUILDINGS, Transport.GROUND 331 | ) 332 | WITCH: Unit = Unit( 333 | "witch", UnitCategory.TROOP, Target.ALL, Transport.GROUND 334 | ) 335 | WIZARD: Unit = Unit( 336 | "wizard", UnitCategory.TROOP, Target.ALL, Transport.GROUND 337 | ) 338 | X_BOW: Unit = Unit( 339 | "x_bow", UnitCategory.BUILDING, Target.GROUND, Transport.GROUND 340 | ) 341 | ZAPPY: Unit = Unit( 342 | "zappy", UnitCategory.TROOP, Target.GROUND, Transport.GROUND 343 | ) 344 | 345 | 346 | Units = _UnitsNamespace() 347 | NAME2UNIT = dict(asdict(Units).items()) 348 | -------------------------------------------------------------------------------- /clashroyalebuildabot/utils/error_handling.py: -------------------------------------------------------------------------------- 1 | def get_wikified_error_message(error_code: str, reason: str) -> str: 2 | return f"Error #E{str(error_code)}: {reason}.\ 3 | See https://github.com/Pbatch/ClashRoyaleBuildABot/wiki/Troubleshooting#error-e{str(error_code)} for more information.\ 4 | Full error message below:\n\n" 5 | -------------------------------------------------------------------------------- /clashroyalebuildabot/utils/git_utils.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from time import sleep 3 | 4 | from loguru import logger 5 | 6 | from error_handling import WikifiedError 7 | 8 | 9 | def _is_branch_late() -> bool: 10 | subprocess.run( 11 | ["git", "fetch"], check=True, capture_output=True, text=True 12 | ) 13 | 14 | status = subprocess.run( 15 | ["git", "status", "-uno"], 16 | capture_output=True, 17 | text=True, 18 | check=True, 19 | ).stdout 20 | return "Your branch is up to date" not in status 21 | 22 | 23 | def _check_and_pull_updates() -> None: 24 | if not _is_branch_late(): 25 | return 26 | 27 | should_update = input( 28 | "New updates available. Do you want to update the bot? [y]/n: " 29 | ) 30 | if should_update.lower() not in ["y", "yes", ""]: 31 | return 32 | subprocess.run(["git", "pull"], check=True, capture_output=True, text=True) 33 | 34 | 35 | def check_and_pull_updates() -> None: 36 | try: 37 | _check_and_pull_updates() 38 | except subprocess.CalledProcessError as e: 39 | if "not a git repository" in e.stderr: 40 | err = "We recommend getting the project using git." 41 | err += "You won't be able to get any updates until you do." 42 | logger.warning(err) 43 | sleep(3) 44 | return 45 | logger.error(f"Error while checking / pulling updates: {e.stderr}") 46 | raise WikifiedError( 47 | "000", "Error while checking / pulling updates" 48 | ) from e 49 | -------------------------------------------------------------------------------- /clashroyalebuildabot/utils/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from loguru import logger 5 | 6 | from clashroyalebuildabot.constants import DEBUG_DIR 7 | 8 | COLORS = dict(context_info="#118aa2", time="#459028") 9 | 10 | 11 | def setup_logger(main_window, config: dict): 12 | log_level = config.get("bot", {}).get("log_level", "INFO").upper() 13 | logger.remove() 14 | logger.add(sys.stdout, level=log_level) 15 | logger.add( 16 | os.path.join(DEBUG_DIR, "bot.log"), 17 | rotation="500 MB", 18 | level=log_level, 19 | ) 20 | logger.add( 21 | main_window.log_handler_function, 22 | format="{time} {level} {module}:{function}:{line} - {message}", 23 | level=log_level, 24 | ) 25 | 26 | 27 | def colorize_log(message): 28 | log_record = message.record 29 | level = log_record["level"].name 30 | time = log_record["time"].strftime("%Y-%m-%d %H:%M:%S") 31 | module = log_record["module"] 32 | function = log_record["function"] 33 | line = log_record["line"] 34 | log_message = log_record["message"] 35 | 36 | if level == "DEBUG": 37 | color = "#147eb8" 38 | elif level == "INFO": 39 | color = "white" 40 | level = "INFO " 41 | elif level == "WARNING": 42 | color = "orange" 43 | elif level == "ERROR": 44 | color = "red" 45 | else: 46 | color = "black" 47 | 48 | return f"""[{time}] {level} | {module}:{function}:{line} - {log_message}""" 49 | -------------------------------------------------------------------------------- /clashroyalebuildabot/visualizer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | import os 3 | 4 | import numpy as np 5 | from PIL import ImageDraw 6 | from PIL import ImageFont 7 | from PyQt6.QtCore import pyqtSignal 8 | from PyQt6.QtCore import QObject 9 | 10 | from clashroyalebuildabot.constants import CARD_CONFIG 11 | from clashroyalebuildabot.constants import LABELS_DIR 12 | from clashroyalebuildabot.constants import SCREENSHOTS_DIR 13 | from clashroyalebuildabot.namespaces.numbers import NumberDetection 14 | from clashroyalebuildabot.namespaces.units import NAME2UNIT 15 | 16 | 17 | class Visualizer(QObject): 18 | _COLOUR_AND_RGBA = [ 19 | ["navy", (0, 38, 63, 127)], 20 | ["blue", (0, 120, 210, 127)], 21 | ["aqua", (115, 221, 252, 127)], 22 | ["teal", (15, 205, 202, 127)], 23 | ["olive", (52, 153, 114, 127)], 24 | ["green", (0, 204, 84, 127)], 25 | ["lime", (1, 255, 127, 127)], 26 | ["yellow", (255, 216, 70, 127)], 27 | ["orange", (255, 125, 57, 127)], 28 | ["red", (255, 47, 65, 127)], 29 | ["maroon", (135, 13, 75, 127)], 30 | ["fuchsia", (246, 0, 184, 127)], 31 | ["purple", (179, 17, 193, 127)], 32 | ["gray", (168, 168, 168, 127)], 33 | ["silver", (220, 220, 220, 127)], 34 | ] 35 | 36 | frame_ready = pyqtSignal(np.ndarray) 37 | 38 | def __init__(self, save_labels, save_images, show_images): 39 | super().__init__() 40 | self.save_labels = save_labels 41 | self.save_images = save_images 42 | self.show_images = show_images 43 | 44 | self.font = ImageFont.load_default() 45 | self.unit_names = [unit["name"] for unit in list(NAME2UNIT.values())] 46 | 47 | os.makedirs(LABELS_DIR, exist_ok=True) 48 | os.makedirs(SCREENSHOTS_DIR, exist_ok=True) 49 | 50 | @staticmethod 51 | def _write_label(image, state, basename): 52 | labels = [] 53 | for det in state.allies + state.enemies: 54 | bbox = det.position.bbox 55 | xc = (bbox[0] + bbox[2]) / (2 * image.width) 56 | yc = (bbox[1] + bbox[3]) / (2 * image.height) 57 | w = (bbox[2] - bbox[0]) / image.width 58 | h = (bbox[3] - bbox[1]) / image.height 59 | label = f"{det.unit.name} {xc} {yc} {w} {h}" 60 | labels.append(label) 61 | 62 | with open( 63 | os.path.join(LABELS_DIR, f"{basename}.txt"), "w", encoding="utf-8" 64 | ) as f: 65 | f.write("\n".join(labels)) 66 | 67 | def _draw_text(self, d, bbox, text, rgba=(0, 0, 0, 255)): 68 | text_width, text_height = d.textbbox((0, 0), text, font=self.font)[2:] 69 | text_bbox = ( 70 | bbox[0], 71 | bbox[1] - text_height, 72 | bbox[0] + text_width, 73 | bbox[1], 74 | ) 75 | d.rectangle(text_bbox, fill=rgba) 76 | d.rectangle(tuple(bbox), outline=rgba) 77 | d.text(tuple(text_bbox[:2]), text=text, fill="white") 78 | 79 | def _draw_unit_bboxes(self, d, dets, prefix): 80 | for det in dets: 81 | colour_idx = self.unit_names.index(det.unit.name) % len( 82 | self._COLOUR_AND_RGBA 83 | ) 84 | rgba = self._COLOUR_AND_RGBA[colour_idx][1] 85 | self._draw_text( 86 | d, det.position.bbox, f"{prefix}_{det.unit.name}", rgba 87 | ) 88 | 89 | def _annotate_image(self, image, state): 90 | d = ImageDraw.Draw(image, "RGBA") 91 | for det in asdict(state.numbers).values(): 92 | det = NumberDetection(**det) 93 | d.rectangle(det.bbox) 94 | self._draw_text(d, det.bbox, f"{det.number:.2f}") 95 | 96 | self._draw_unit_bboxes(d, state.allies, "ally") 97 | self._draw_unit_bboxes(d, state.enemies, "enemy") 98 | 99 | for card, position in zip(state.cards, CARD_CONFIG): 100 | d.rectangle(tuple(position)) 101 | self._draw_text(d, position, card.name) 102 | 103 | return image 104 | 105 | def run(self, image, state): 106 | n_screenshots = len(os.listdir(SCREENSHOTS_DIR)) 107 | n_labels = len(os.listdir(LABELS_DIR)) 108 | basename = max(n_labels, n_screenshots) + 1 109 | 110 | if self.save_labels: 111 | self._write_label(image, state, basename) 112 | 113 | if not self.save_images and not self.show_images: 114 | return 115 | 116 | annotated_image = self._annotate_image(image, state) 117 | 118 | if self.save_images: 119 | annotated_image.save( 120 | os.path.join(SCREENSHOTS_DIR, f"{basename}.png") 121 | ) 122 | 123 | if self.show_images: 124 | self.frame_ready.emit(np.array(annotated_image)) 125 | -------------------------------------------------------------------------------- /error_handling/__init__.py: -------------------------------------------------------------------------------- 1 | from .wikify_error import WikifiedError 2 | 3 | __all__ = ["WikifiedError"] 4 | -------------------------------------------------------------------------------- /error_handling/wikify_error.py: -------------------------------------------------------------------------------- 1 | def get_wikified_error_message(error_code: str, reason: str) -> str: 2 | err_str = f"\u26A0 Error #E{str(error_code)}: {reason}" 3 | link = "https://github.com/Pbatch/ClashRoyaleBuildABot/wiki/" 4 | link += f"Troubleshooting#error-e{str(error_code)}" 5 | err_str += f" See {link} for more information." 6 | err_str += " You might find more context above this error.\n\n" 7 | return err_str 8 | 9 | 10 | class WikifiedError(Exception): 11 | def __init__(self, error_code: str, reason: str): 12 | self.error_code = error_code 13 | self.reason = reason 14 | super().__init__(get_wikified_error_message(error_code, reason)) 15 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from error_handling import WikifiedError 2 | 3 | try: 4 | import signal 5 | import sys 6 | 7 | from loguru import logger 8 | from PyQt6.QtWidgets import QApplication 9 | 10 | from clashroyalebuildabot.actions import ArchersAction 11 | from clashroyalebuildabot.actions import BabyDragonAction 12 | from clashroyalebuildabot.actions import CannonAction 13 | from clashroyalebuildabot.actions import GoblinBarrelAction 14 | from clashroyalebuildabot.actions import KnightAction 15 | from clashroyalebuildabot.actions import MinipekkaAction 16 | from clashroyalebuildabot.actions import MusketeerAction 17 | from clashroyalebuildabot.actions import WitchAction 18 | from clashroyalebuildabot.gui.main_window import MainWindow 19 | from clashroyalebuildabot.gui.utils import load_config 20 | from clashroyalebuildabot.utils.git_utils import check_and_pull_updates 21 | from clashroyalebuildabot.utils.logger import setup_logger 22 | except Exception as e: 23 | raise WikifiedError("001", "Missing imports.") from e 24 | 25 | 26 | def main(): 27 | check_and_pull_updates() 28 | actions = [ 29 | ArchersAction, 30 | GoblinBarrelAction, 31 | BabyDragonAction, 32 | CannonAction, 33 | KnightAction, 34 | MinipekkaAction, 35 | MusketeerAction, 36 | WitchAction, 37 | ] 38 | try: 39 | config = load_config() 40 | 41 | app = QApplication([]) 42 | window = MainWindow(config, actions) 43 | setup_logger(window, config) 44 | 45 | window.show() 46 | sys.exit(app.exec()) 47 | except WikifiedError: 48 | raise 49 | except Exception as e: 50 | logger.error(f"An error occurred in main loop: {e}") 51 | sys.exit(1) 52 | 53 | 54 | if __name__ == "__main__": 55 | signal.signal(signal.SIGINT, signal.SIG_DFL) 56 | main() 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "ClashRoyaleBuildABot" 7 | version = "0.1.0" 8 | authors = [ 9 | {name = "pbatch"}, 10 | ] 11 | description = "A platform for creating bots to play Clash Royale" 12 | readme = "README.md" 13 | requires-python = ">= 3.9" 14 | dependencies = [ 15 | "flatbuffers>=2.0", 16 | "numpy>=1.23.5,<2", 17 | "Pillow>=10.1.0", 18 | "protobuf>=3.20.3", 19 | "keyboard>=0.13.5", 20 | "scipy>=1.13.1", 21 | "rich>=13.7.1", 22 | "loguru>=0.7.2", 23 | "PyYAML>=6.0.1", 24 | "pybind11>=2.12", 25 | "requests>=2.25.1", 26 | "PyQt6>=6.7.1", 27 | "av", 28 | "tqdm", 29 | "opencv-python==4.10.0.84", 30 | "pre-commit==3.5.0", 31 | "black==24.4.0", 32 | "flake8==7.0.0", 33 | "isort==5.13.2", 34 | "pylint==3.1.0", 35 | 36 | ] 37 | [project.optional-dependencies] 38 | cpu = [ 39 | "onnxruntime>=1.18.0" 40 | ] 41 | gpu = [ 42 | "onnxruntime-gpu>=1.18.0", 43 | ] 44 | 45 | [tool.black] 46 | line-length = 79 47 | include = '\.pyi?$' 48 | exclude = ''' 49 | /( 50 | \.git 51 | | \.hg 52 | | \.mypy_cache 53 | | \.tox 54 | | \.venv 55 | | _build 56 | | buck-out 57 | )/ 58 | ''' 59 | 60 | [tool.isort] 61 | profile = "black" 62 | line_length = 79 63 | force_single_line = true 64 | force_sort_within_sections = true 65 | lexicographical = true 66 | single_line_exclusions = ["typing"] 67 | order_by_type = false 68 | group_by_package = true 69 | skip_glob = ["venv/*"] 70 | --------------------------------------------------------------------------------