Skip to content

Commit 800ed98

Browse files
committed
Add to examples.
Add a custom bot implementation example modeled after StandardBot.
1 parent 2cdd721 commit 800ed98

3 files changed

Lines changed: 234 additions & 2 deletions

File tree

example/custom_action.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""
2+
custom_action.py
3+
standard_action.py reimplemented as an import from a clashroyalebuildabot install
4+
"""
5+
6+
from clashroyalebuildabot.bot import Action
7+
8+
9+
class CustomAction(Action):
10+
score = None
11+
12+
@staticmethod
13+
def _distance(x1, y1, x2, y2):
14+
return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
15+
16+
def _calculate_spell_score(self, units, radius, min_to_hit):
17+
"""
18+
Calculate the score for a spell card (either fireball or arrows)
19+
20+
The score is defined as [A, B, C]
21+
A is 1 if we'll hit `min_to_hit` or more units, 0 otherwise
22+
B is the number of units we hit
23+
C is the negative distance to the furthest unit
24+
"""
25+
score = [0, 0, 0]
26+
for k, v in units.items():
27+
if k[:4] == 'ally':
28+
continue
29+
for unit in v['positions']:
30+
tile_x, tile_y = unit['tile_xy']
31+
# Assume the unit will move down a few spaces
32+
tile_y -= 2
33+
34+
# Add 1 to the score if the spell will hit the unit
35+
distance = self._distance(tile_x, tile_y, self.tile_x, self.tile_y)
36+
if distance <= radius - 1:
37+
score[1] += 1
38+
score[2] = min(score[2], -distance)
39+
40+
# Set score[0] to 1 if we think we'll hit enough units
41+
if score[1] >= min_to_hit:
42+
score[0] = 1
43+
44+
return score
45+
46+
def _calculate_knight_score(self, state):
47+
"""
48+
Only play the knight if a ground troop is on our side of the battlefield
49+
Play the knight in the center, vertically aligned with the troop
50+
"""
51+
score = [0] if state['numbers']['elixir']['number'] != 10 else [0.5]
52+
for k, v in state['units'].items():
53+
if k[:4] == 'ally':
54+
continue
55+
for unit in v['positions']:
56+
tile_x, tile_y = unit['tile_xy']
57+
if self.tile_y < tile_y <= 14 and v['transport'] == 'ground':
58+
if tile_x > 8 and self.tile_x == 9 or tile_x <= 8 and self.tile_x == 8:
59+
score = [1, self.tile_y - tile_y]
60+
return score
61+
62+
def _calculate_minions_score(self, state):
63+
"""
64+
Only play minions on top of enemy units
65+
"""
66+
score = [0] if state['numbers']['elixir']['number'] != 10 else [0.5]
67+
for k, v in state['units'].items():
68+
if k[:4] == 'ally':
69+
continue
70+
for unit in v['positions']:
71+
tile_x, tile_y = unit['tile_xy']
72+
distance = self._distance(tile_x, tile_y, self.tile_x, self.tile_y)
73+
if distance < 1:
74+
score = [1, -distance]
75+
return score
76+
77+
def _calculate_fireball_score(self, state):
78+
"""
79+
Only play fireball if at least 3 units will be hit
80+
Try to hit as many units as possible
81+
"""
82+
return self._calculate_spell_score(state['units'], radius=2.5, min_to_hit=3)
83+
84+
def _calculate_arrows_score(self, state):
85+
"""
86+
Only play arrows if at least 5 units will be hit
87+
Try to hit as many units as possible
88+
"""
89+
return self._calculate_spell_score(state['units'], radius=4, min_to_hit=5)
90+
91+
def _calculate_archers_score(self, state):
92+
"""
93+
Only play the archers if there is a troop on our side of the battlefield
94+
Play the archers in the center, vertically aligned with the troop
95+
"""
96+
score = [0] if state['numbers']['elixir']['number'] != 10 else [0.5]
97+
for k, v in state['units'].items():
98+
if k[:4] == 'ally':
99+
continue
100+
for unit in v['positions']:
101+
tile_x, tile_y = unit['tile_xy']
102+
if self.tile_y < tile_y <= 14:
103+
if tile_x > 8 and self.tile_x == 10 or tile_x <= 8 and self.tile_x == 7:
104+
score = [1, self.tile_y - tile_y]
105+
return score
106+
107+
def _calculate_giant_score(self, state):
108+
"""
109+
Only place the giant when at 10 elixir
110+
Place it as high up as possible
111+
Try to target the lowest hp tower
112+
"""
113+
score = [0]
114+
left_hp, right_hp = [state['numbers'][f'{direction}_enemy_princess_hp']['number']
115+
for direction in ['left', 'right']]
116+
if state['numbers']['elixir']['number'] == 10:
117+
if self.tile_x == 3:
118+
score = [1, self.tile_y, left_hp != -1, left_hp <= right_hp]
119+
elif self.tile_x == 14:
120+
score = [1, self.tile_y, right_hp != -1, right_hp <= left_hp]
121+
122+
return score
123+
124+
def _calculate_minipekka_score(self, state):
125+
"""
126+
Place minipekka on the bridge as high up as possible
127+
Try to target the lowest hp tower
128+
"""
129+
left_hp, right_hp = [state['numbers'][f'{direction}_enemy_princess_hp']['number']
130+
for direction in ['left', 'right']]
131+
score = [0]
132+
if self.tile_x == 3:
133+
score = [1, self.tile_y, left_hp != -1, left_hp <= right_hp]
134+
elif self.tile_x == 14:
135+
score = [1, self.tile_y, right_hp != -1, right_hp <= left_hp]
136+
return score
137+
138+
def _calculate_musketeer_score(self, state):
139+
"""
140+
Place musketeer at 5-6 tiles away from enemies
141+
That should be just within her range
142+
"""
143+
score = [0]
144+
for k, v in state['units'].items():
145+
if k[:4] == 'ally':
146+
continue
147+
for unit in v['positions']:
148+
tile_x, tile_y = unit['tile_xy']
149+
distance = self._distance(tile_x, tile_y, self.tile_x, self.tile_y)
150+
if 5 < distance < 6:
151+
score = [1]
152+
elif distance < 5:
153+
score = [0]
154+
return score
155+
156+
def calculate_score(self, state):
157+
name_to_score = {'knight': self._calculate_knight_score,
158+
'minions': self._calculate_minions_score,
159+
'fireball': self._calculate_fireball_score,
160+
'giant': self._calculate_giant_score,
161+
'minipekka': self._calculate_minipekka_score,
162+
'musketeer': self._calculate_musketeer_score,
163+
'arrows': self._calculate_arrows_score,
164+
'archers': self._calculate_archers_score
165+
}
166+
score_function = name_to_score[self.name]
167+
score = score_function(state)
168+
self.score = score
169+
return score

example/custom_bot.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
custom_bot.py
3+
standard_bot.py reimplemented as an import from a clashroyalebuildabot install
4+
"""
5+
6+
import random
7+
import time
8+
9+
from clashroyalebuildabot.bot import Bot
10+
from custom_action import CustomAction
11+
from clashroyalebuildabot.data.constants import DISPLAY_WIDTH, SCREENSHOT_WIDTH, DISPLAY_HEIGHT, SCREENSHOT_HEIGHT
12+
13+
14+
class CustomBot(Bot):
15+
def __init__(self, card_names, debug=False):
16+
preset_deck = {'minions', 'archers', 'arrows', 'giant', 'minipekka', 'fireball', 'knight', 'musketeer'}
17+
if set(card_names) != preset_deck:
18+
raise ValueError(f'You must use the preset deck with cards {preset_deck} for StandardBot')
19+
super().__init__(card_names, CustomAction, debug=debug)
20+
21+
def _preprocess(self):
22+
"""
23+
Perform preprocessing on the state
24+
25+
Estimate the tile of each unit to be the bottom of their bounding box
26+
"""
27+
for k, v in self.state['units'].items():
28+
for unit in v['positions']:
29+
bbox = unit['bounding_box']
30+
bbox[0] *= DISPLAY_WIDTH / SCREENSHOT_WIDTH
31+
bbox[1] *= DISPLAY_HEIGHT / SCREENSHOT_HEIGHT
32+
bbox[2] *= DISPLAY_WIDTH / SCREENSHOT_WIDTH
33+
bbox[3] *= DISPLAY_HEIGHT / SCREENSHOT_HEIGHT
34+
bbox_bottom = [((bbox[0] + bbox[2]) / 2), bbox[3]]
35+
unit['tile_xy'] = self._get_nearest_tile(*bbox_bottom)
36+
37+
def run(self):
38+
while True:
39+
# Set the state of the game
40+
self.set_state()
41+
# Obtain a list of playable actions
42+
actions = self.get_actions()
43+
if actions:
44+
# Shuffle the actions (because action scores might be the same)
45+
random.shuffle(actions)
46+
# Preprocessing
47+
self._preprocess()
48+
# Get the best action
49+
action = max(actions, key=lambda x: x.calculate_score(self.state))
50+
# Skip the action if it doesn't score high enough
51+
if action.score[0] == 0:
52+
continue
53+
# Play the best action
54+
self.play_action(action)
55+
# Log the result
56+
print(f'Playing {action} with score {action.score} and sleeping for 1 second')
57+
time.sleep(1.0)

example/main.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
from clashroyalebuildabot.bot import StandardBot
1+
"""
2+
A CustomBot implementation through import
3+
"""
4+
from custom_bot import CustomBot # see custom_bot.py
25

36

47
def main():
8+
# Set required bot variables
59
card_names = ['minions', 'archers', 'arrows', 'giant',
610
'minipekka', 'fireball', 'knight', 'musketeer']
7-
bot = StandardBot(card_names, debug=True)
11+
# Define an instance of CustomBot
12+
bot = CustomBot(card_names, debug=True)
13+
# and run!
814
bot.run()
915

1016

0 commit comments

Comments
 (0)