├── .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 |
9 |
10 | |
11 |
12 |
13 | |
14 |
15 |
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 | [](https://discord.gg/K4UfbsfcMa)
20 | [](https://www.python.org/downloads/release/python-3100/)
21 | [](#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 |
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 |
--------------------------------------------------------------------------------