Skip to content

Commit 9e6e015

Browse files
committed
Add arcade map bot example
1 parent de2a492 commit 9e6e015

File tree

1 file changed

+121
-0
lines changed

1 file changed

+121
-0
lines changed

examples/arcade_bot.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import sc2
2+
from sc2 import Race
3+
from sc2.player import Bot
4+
5+
from sc2.units import Units
6+
from sc2.unit import Unit
7+
from sc2.position import Point2, Point3
8+
9+
from sc2.ids.unit_typeid import UnitTypeId
10+
from sc2.ids.upgrade_id import UpgradeId
11+
from sc2.ids.buff_id import BuffId
12+
from sc2.ids.ability_id import AbilityId
13+
14+
from typing import List, Dict, Set, Tuple, Any, Optional, Union # mypy type checking
15+
16+
"""
17+
To play an arcade map, you need to download the map first.
18+
19+
Open the StarCraft2 Map Editor through the Battle.net launcher, in the top left go to
20+
File -> Open -> (Tab) Blizzard -> Log in -> with "Source: Map/Mod Name" search for your desired map, in this example "Marine Split Challenge-LOTV" map created by printf
21+
Hit "Ok" and confirm the download. Now that the map is opened, go to "File -> Save as" to store it on your hard drive.
22+
Now load the arcade map by entering your map name below in
23+
sc2.maps.get("YOURMAPNAME") without the .SC2Map extension
24+
25+
26+
Map info:
27+
You start with 30 marines, level N has 15+N speed banelings on creep
28+
29+
Type in game "sling" to activate zergling+baneling combo
30+
Type in game "stim" to activate stimpack
31+
32+
33+
Improvements that could be made:
34+
- Make marines constantly run if they have a ling/bane very close to them
35+
- Split marines before engaging
36+
"""
37+
38+
class MarineSplitChallenge(sc2.BotAI):
39+
async def on_step(self, iteration):
40+
if iteration == 0:
41+
await self.on_first_iteration()
42+
43+
actions = []
44+
45+
# do marine micro vs zerglings
46+
for unit in self.units(UnitTypeId.MARINE):
47+
48+
if self.known_enemy_units:
49+
50+
# attack (or move towards) zerglings / banelings
51+
if unit.weapon_cooldown <= self._client.game_step / 2:
52+
enemies_in_range = self.known_enemy_units.filter(lambda u: unit.target_in_range(u))
53+
54+
# attack lowest hp enemy if any enemy is in range
55+
if enemies_in_range:
56+
# Use stimpack
57+
if self.already_pending_upgrade(UpgradeId.STIMPACK) == 1 and not unit.has_buff(BuffId.STIMPACK) and unit.health > 10:
58+
actions.append(unit(AbilityId.EFFECT_STIM))
59+
60+
61+
# attack baneling first
62+
filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.BANELING)
63+
64+
if not filtered_enemies_in_range:
65+
filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.ZERGLING)
66+
# attack lowest hp unit
67+
lowest_hp_enemy_in_range = min(filtered_enemies_in_range, key=lambda u: u.health)
68+
actions.append(unit.attack(lowest_hp_enemy_in_range))
69+
70+
# no enemy is in attack-range, so give attack command to closest instead
71+
else:
72+
closest_enemy = self.known_enemy_units.closest_to(unit)
73+
actions.append(unit.attack(closest_enemy))
74+
75+
76+
# move away from zergling / banelings
77+
else:
78+
stutter_step_positions = self.position_around_unit(unit, distance=4)
79+
80+
# filter in pathing grid
81+
stutter_step_positions = {p for p in stutter_step_positions if self.in_pathing_grid(p)}
82+
83+
# find position furthest away from enemies and closest to unit
84+
enemies_in_range = self.known_enemy_units.filter(lambda u: unit.target_in_range(u, -0.5))
85+
86+
if stutter_step_positions and enemies_in_range:
87+
retreat_position = max(stutter_step_positions, key=lambda x: x.distance_to(enemies_in_range.center) - x.distance_to(unit))
88+
actions.append(unit.move(retreat_position))
89+
90+
else:
91+
print("No retreat positions detected for unit {} at {}.".format(unit, unit.position.rounded))
92+
93+
await self.do_actions(actions)
94+
95+
96+
97+
async def on_first_iteration(self):
98+
await self.chat_send("Edit this message for automatic chat commands.")
99+
self._client.game_step = 4 # do actions every X frames instead of every 8th
100+
101+
102+
103+
def position_around_unit(self, pos: Union[Unit, Point2, Point3], distance: int=1, step_size: int=1, exclude_out_of_bounds: bool=True):
104+
pos = pos.position.to2.rounded
105+
positions = {pos.offset(Point2((x, y)))
106+
for x in range(-distance, distance+1, step_size)
107+
for y in range(-distance, distance+1, step_size)
108+
if (x, y) != (0, 0)}
109+
# filter positions outside map size
110+
if exclude_out_of_bounds:
111+
positions = {p for p in positions if 0 <= p[0] < self._game_info.pathing_grid.width and 0 <= p[1] < self._game_info.pathing_grid.height}
112+
return positions
113+
114+
115+
def main():
116+
sc2.run_game(sc2.maps.get("Marine Split Challenge"), [
117+
Bot(Race.Terran, MarineSplitChallenge()),
118+
], realtime=False, save_replay_as="Example.SC2Replay")
119+
120+
if __name__ == '__main__':
121+
main()

0 commit comments

Comments
 (0)