Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- docker exec app pip install --upgrade pipenv
# Create pipenv virtual environment and install dev packages for testing
# Command origins from here: https://stackoverflow.com/a/28037991/10882657
- docker exec -i app bash -c "cd /root/template && pipenv install --dev"
- docker exec -i app bash -c "cd /root/template && pipenv install --dev --python 3.7"
# Run tests
- docker exec -i app bash -c "cd /root/template && pipenv run pytest test/"
# Shut down and remove container
Expand All @@ -43,7 +43,7 @@ jobs:
- docker run -it -d --name app python:3.8-rc-slim
- docker cp . app:/root/template
- docker exec app pip install --upgrade pipenv
- docker exec -i app bash -c "cd /root/template && pipenv --python python install --dev"
- docker exec -i app bash -c "cd /root/template && pipenv --python python install --dev --python 3.8"
- docker exec -i app bash -c "cd /root/template && pipenv run pytest test/"
- docker rm -f app

Expand Down
43 changes: 25 additions & 18 deletions examples/terran/ramp_wall.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,36 @@ async def on_step(self, iteration):
# Uncomment the following if you want to build 3 supplydepots in the wall instead of a barracks in the middle + 2 depots in the corner
# depot_placement_positions = self.main_base_ramp.corner_depots | {self.main_base_ramp.depot_in_middle}

barracks_placement_position = None
barracks_placement_position = self.main_base_ramp.barracks_correct_placement
# If you prefer to have the barracks in the middle without room for addons, use the following instead
# barracks_placement_position = self.main_base_ramp.barracks_in_middle

depots = self.units(SUPPLYDEPOT) | self.units(SUPPLYDEPOTLOWERED)

# Draw ramp points
# def terrain_to_z_height(h):
# return round(16 * h / 255, 2)

# for ramp in self.game_info.map_ramps:
# for p in ramp.points:
# h = self.get_terrain_height(p)
# h2 = terrain_to_z_height(h)
# pos = Point3((p.x, p.y, h2))
# p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z))
# p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.5))
# print(f"drawing {p0} to {p1}")
# self._client.debug_box_out(p0, p1, color=Point3((255, 0, 0)))
#
# await self._client.send_debug()

# Filter locations close to finished supply depots
def terrain_to_z_height(h):
return round(16 * h / 255, 2)

for ramp in self.game_info.map_ramps:
for p in ramp.points:
h = self.get_terrain_height(p)
h2 = terrain_to_z_height(h)
pos = Point3((p.x, p.y, h2))
p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z))
p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.5))
# print(f"Drawing {p0} to {p1}")
color=Point3((255, 0, 0))
if p in ramp.upper:
color = Point3((0, 255, 0))
if p in ramp.upper2_for_ramp_wall:
color = Point3((0, 255, 255))
if p in ramp.lower:
color = Point3((0, 0, 255))
self._client.debug_box_out(p0, p1, color=color)

await self._client.send_debug()

# # Filter locations close to finished supply depots
if depots:
depot_placement_positions = {d for d in depot_placement_positions if depots.closest_distance_to(d) > 1}

Expand All @@ -82,7 +88,7 @@ async def on_step(self, iteration):
await self.do(w.build(SUPPLYDEPOT, target_depot_location))

# Build barracks
if depots.ready.exists and self.can_afford(BARRACKS) and not self.already_pending(BARRACKS):
if depots.ready and self.can_afford(BARRACKS) and not self.already_pending(BARRACKS):
if self.units(BARRACKS).amount + self.already_pending(BARRACKS) > 0:
return
ws = self.workers.gathering
Expand All @@ -108,6 +114,7 @@ def main():
"DarknessSanctuaryLE",
"ParaSiteLE", # Has 5 upper points at the main ramp
"AcolyteLE", # Has 4 upper points at the ramp to the in-base natural and 2 upper points at the small ramp
"HonorgroundsLE", # Has 4 or 9 upper points at the large main base ramp
]
)
sc2.run_game(
Expand Down
140 changes: 75 additions & 65 deletions sc2/bot_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,24 @@ def known_enemy_structures(self) -> Units:

@property
def main_base_ramp(self) -> "Ramp":
""" Returns the Ramp instance of the closest main-ramp to start location. Look in game_info.py for more information """
""" Returns the Ramp instance of the closest main-ramp to start location.
Look in game_info.py for more information """
if hasattr(self, "cached_main_base_ramp"):
return self.cached_main_base_ramp
""" The reason for len(ramp.upper) in {2, 5} is:
ParaSite map has 5 upper points, and most other maps have 2 upper points at the main ramp. The map Acolyte has 4 upper points at the wrong ramp (which is closest to the start position) """
self.cached_main_base_ramp = min(
(ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {2, 5}),
key=lambda r: self.start_location.distance_to(r.top_center),
)
# The reason for len(ramp.upper) in {2, 5} is:
# ParaSite map has 5 upper points, and most other maps have 2 upper points at the main ramp.
# The map Acolyte has 4 upper points at the wrong ramp (which is closest to the start position).
try:
self.cached_main_base_ramp = min(
(ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {2, 5}),
key=lambda r: self.start_location.distance_to(r.top_center),
)
except ValueError:
# Hardcoded hotfix for Honorgrounds LE map, as that map has a large main base ramp with inbase natural
self.cached_main_base_ramp = min(
(ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {4, 9}),
key=lambda r: self.start_location.distance_to(r.top_center),
)
return self.cached_main_base_ramp

@property_cache_forever
Expand All @@ -148,64 +157,65 @@ def expansion_locations(self) -> Dict[Point2, Units]:
# any resource in a group is closer than 6 to any resource of another group

# Distance we group resources by
RESOURCE_SPREAD_THRESHOLD = 8.5
minerals = self.state.mineral_field
geysers = self.state.vespene_geyser
all_resources = minerals | geysers
# Presort resources to get faster clustering
all_resources.sort(key=lambda resource: (resource.position.x, resource.position.y))
# Create a group for every resource
resource_groups = [[resource] for resource in all_resources]
# Loop the merging process as long as we change something
found_something = True
while found_something:
found_something = False
# Check every combination of two groups
for group_a, group_b in itertools.combinations(resource_groups, 2):
# Check if any pair of resource of these groups is closer than threshold together
if any(
resource_a.distance_to(resource_b) <= RESOURCE_SPREAD_THRESHOLD
for resource_a, resource_b in itertools.product(group_a, group_b)
):
# Remove the single groups and add the merged group
resource_groups.remove(group_a)
resource_groups.remove(group_b)
resource_groups.append(group_a + group_b)
found_something = True
break
# Distance offsets we apply to center of each resource group to find expansion position
offset_range = 7
offsets = [
(x, y)
for x in range(-offset_range, offset_range + 1)
for y in range(-offset_range, offset_range + 1)
if 4 <= math.hypot(x, y) <= 7
]
# Dict we want to return
centers = {}
# For every resource group:
for resources in resource_groups:
# Possible expansion points
amount = len(resources)
# Calculate center, round and add 0.5 because expansion location will have (x.5, y.5)
# coordinates because bases have size 5.
center_x = round(sum(resource.position.x for resource in resources) / amount) + 0.5
center_y = round(sum(resource.position.y for resource in resources) / amount) + 0.5
possible_points = (Point2((offset[0] + center_x, offset[1] + center_y)) for offset in offsets)
# Filter out points that are too near
possible_points = (
point
for point in possible_points
# Check if point can be built on
if self._game_info.placement_grid[point.rounded] != 0
# Check if all resources have enough space to point
and all(point.distance_to(resource) >= (7 if resource in geysers else 6) for resource in resources)
)
# Choose best fitting point
# TODO can we improve this by calculating the distance only one time?
result = min(possible_points, key=lambda point: sum(point.distance_to(resource) for resource in resources))
centers[result] = resources
return centers
from .helpers.devtools import time_this

with time_this("expo locations"):
RESOURCE_SPREAD_THRESHOLD = 8.5
minerals = self.state.mineral_field
geysers = self.state.vespene_geyser
all_resources = minerals | geysers
# Presort resources to get faster clustering
# all_resources.sort(key=lambda resource: resource.position.x ** 2 + resource.position.y ** 2)
all_resources.sort(key=lambda resource: resource.position.x ** 2 + resource.position.y ** 2)
# Create a group for every resource
resource_groups = [[resource] for resource in all_resources]
# Loop the merging process as long as we change something
found_something = True
while found_something:
found_something = False
# Check every combination of two groups
for group_a, group_b in itertools.combinations(resource_groups, 2):
# Check if any pair of resource of these groups is closer than threshold together
if any(
resource_a.distance_to(resource_b) <= RESOURCE_SPREAD_THRESHOLD
for resource_a, resource_b in itertools.product(group_a, group_b)
):
# Remove the single groups and add the merged group
resource_groups.remove(group_a)
resource_groups.remove(group_b)
resource_groups.append(group_a + group_b)
found_something = True
break
# Distance offsets we apply to center of each resource group to find expansion position
offset_range = 7
offsets = [(x, y) for x, y in itertools.product(range(-offset_range, offset_range + 1), repeat=2)]
# Dict we want to return
centers = {}
# For every resource group:
for resources in resource_groups:
# Possible expansion points
amount = len(resources)
# Calculate center, round and add 0.5 because expansion location will have (x.5, y.5)
# coordinates because bases have size 5.
center_x = round(sum(resource.position.x for resource in resources) / amount) + 0.5
center_y = round(sum(resource.position.y for resource in resources) / amount) + 0.5
possible_points = (Point2((offset[0] + center_x, offset[1] + center_y)) for offset in offsets)
# Filter out points that are too near
possible_points = (
point
for point in possible_points
# Check if point can be built on
if self._game_info.placement_grid[point.rounded] == 1
# Check if all resources have enough space to point
and all(point.distance_to(resource) > (7 if resource in geysers else 6) for resource in resources)
)
# Choose best fitting point
# TODO can we improve this by calculating the distance only one time?
result = min(
possible_points, key=lambda point: sum(point.distance_to(resource) for resource in resources)
)
centers[result] = resources
return centers

def _correct_zerg_supply(self):
""" The client incorrectly rounds zerg supply down instead of up (see
Expand Down
24 changes: 18 additions & 6 deletions sc2/game_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ def bottom_center(self) -> Point2:
return pos

@property_immutable_cache
def barracks_in_middle(self) -> Point2:
def barracks_in_middle(self) -> Optional[Point2]:
""" Barracks position in the middle of the 2 depots """
if len(self.upper) not in {2, 5}:
return None
if len(self.upper2_for_ramp_wall) == 2:
points = self.upper2_for_ramp_wall
p1 = points.pop().offset((self.x_offset, self.y_offset))
Expand All @@ -105,27 +107,35 @@ def barracks_in_middle(self) -> Point2:
raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.")

@property_immutable_cache
def depot_in_middle(self) -> Point2:
def depot_in_middle(self) -> Optional[Point2]:
""" Depot in the middle of the 3 depots """
if len(self.upper2_for_ramp_wall) == 2:
points = self.upper2_for_ramp_wall
p1 = points.pop().offset((self.x_offset, self.y_offset)) # still an error with pixelmap?
p1 = points.pop().offset((self.x_offset, self.y_offset))
p2 = points.pop().offset((self.x_offset, self.y_offset))
# Offset from top point to depot center is (1.5, 0.5)
intersects = p1.circle_intersection(p2, 2.5 ** 0.5)
try:
intersects = p1.circle_intersection(p2, 2.5 ** 0.5)
except AssertionError:
# Returns None when no placement was found, this is the case on the map Honorgrounds LE with an exceptionally large main base ramp
return None
anyLowerPoint = next(iter(self.lower))
return max(intersects, key=lambda p: p.distance_to_point2(anyLowerPoint))
raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.")

@property_mutable_cache
def corner_depots(self) -> Set[Point2]:
""" Finds the 2 depot positions on the outside """
if not self.upper2_for_ramp_wall:
return set()
if len(self.upper2_for_ramp_wall) == 2:
points = self.upper2_for_ramp_wall
p1 = points.pop().offset((self.x_offset, self.y_offset)) # still an error with pixelmap?
p1 = points.pop().offset((self.x_offset, self.y_offset))
p2 = points.pop().offset((self.x_offset, self.y_offset))
center = p1.towards(p2, p1.distance_to_point2(p2) / 2)
depotPosition = self.depot_in_middle
if depotPosition is None:
return set()
# Offset from middle depot to corner depots is (2, 1)
intersects = center.circle_intersection(depotPosition, 5 ** 0.5)
return intersects
Expand All @@ -140,8 +150,10 @@ def barracks_can_fit_addon(self) -> bool:
raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.")

@property_immutable_cache
def barracks_correct_placement(self) -> Point2:
def barracks_correct_placement(self) -> Optional[Point2]:
""" Corrected placement so that an addon can fit """
if self.barracks_in_middle is None:
return None
if len(self.upper2_for_ramp_wall) == 2:
if self.barracks_can_fit_addon:
return self.barracks_in_middle
Expand Down
7 changes: 1 addition & 6 deletions test/generate_pickle_files_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@

class ExporterBot(sc2.BotAI):
async def on_step(self, iteration):
if iteration == 0:
await self.on_first_iteration()

actions = []
await self.do_actions(actions)

Expand Down Expand Up @@ -76,7 +73,7 @@ async def on_start_async(self):
def main():

maps = [
"16-BitLE.SC2Map",
"16-BitLE",
"AbiogenesisLE",
"AbyssalReefLE",
"AcidPlantLE",
Expand Down Expand Up @@ -108,7 +105,6 @@ def main():
"HonorgroundsLE",
"InterloperLE",
"KairosJunctionLE",
"KairosJunctionLE",
"KingsCoveLE",
"LostandFoundLE",
"MechDepotLE",
Expand All @@ -119,7 +115,6 @@ def main():
"OldSunshine",
"PaladinoTerminalLE",
"ParaSiteLE",
"ParaSiteLE",
"PortAleksanderLE",
"PrimusQ9",
"ProximaStationLE",
Expand Down
Binary file added test/pickle_data/16-Bit LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Abiogenesis LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Abyssal Reef LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Acid Plant LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Acolyte LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Ascension to Aiur LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Automaton LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Backwater LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Battle on the Boardwalk LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Bel'Shir Vestige LE (Void).pkl
Binary file not shown.
Binary file added test/pickle_data/Blackpink LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Blood Boil LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Blueshift LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Cactus Valley LE (Void).pkl
Binary file not shown.
Binary file added test/pickle_data/Catalyst LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Cerulean Fall LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Cyber Forest LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Darkness Sanctuary LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Defender's Landing LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Dreamcatcher LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Eastwatch LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Fracture LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Frost LE (Void).pkl
Binary file not shown.
Binary file added test/pickle_data/Honorgrounds LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Interloper LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Kairos Junction LE.pkl
Binary file not shown.
Binary file added test/pickle_data/King's Cove LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Lost and Found LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Mech Depot LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Neon Violet Square LE.pkl
Binary file not shown.
Binary file added test/pickle_data/New Repugnancy LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Newkirk Precinct TE (Void).pkl
Binary file not shown.
Binary file added test/pickle_data/Odyssey LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Old Sunshine.pkl
Binary file not shown.
Binary file added test/pickle_data/Paladino Terminal LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Para Site LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/Port Aleksander LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Proxima Station LE.pkl
Binary file not shown.
Binary file added test/pickle_data/Redshift LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/STC - Primus Q-9.pkl
Binary file not shown.
Binary file modified test/pickle_data/Sanglune.pkl
Binary file not shown.
Binary file added test/pickle_data/Sequencer LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/The Timeless Wild [PLX].pkl
Binary file not shown.
Binary file modified test/pickle_data/Urzagol [PLX].pkl
Binary file not shown.
Binary file added test/pickle_data/Year Zero LE.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC10] Artana.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC10] Digital Frontier.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC10] Treachery.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC11] Crystal Cavern.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC11] Reminiscence.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC12] Acropolis.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC12] Bandwidth.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC12] Ephemeron.pkl
Binary file not shown.
Binary file modified test/pickle_data/[TLMC12] Triton.pkl
Binary file not shown.
20 changes: 14 additions & 6 deletions test/test_pickled_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def test_bot_ai(self, bot: BotAI):
bot._game_info.map_ramps, bot._game_info.vision_blockers = bot._game_info._find_ramps_and_vision_blockers()
assert bot.main_base_ramp # Test if any ramp was found
# TODO: Cache all expansion positions for a map and check if it is the same
assert len(bot.expansion_locations) >= 12
assert len(bot.expansion_locations) >= 10
# On N player maps, it is expected that there are N*X bases because of symmetry, at least for 1vs1 maps
assert (
len(bot.expansion_locations) % (len(bot.enemy_start_locations) + 1) == 0
Expand Down Expand Up @@ -169,15 +169,23 @@ def test_game_info(self, bot: BotAI):

# Test if main base ramp works
ramp: Ramp = bot.main_base_ramp
assert ramp.barracks_correct_placement
assert ramp.barracks_in_middle
assert ramp.depot_in_middle
assert len(ramp.corner_depots) == 2
# On the map HonorgroundsLE, the main base is large and it would take a bit of effort to fix, so it returns None or empty set
if len(ramp.upper) in {2, 5}:
assert ramp.barracks_correct_placement
assert ramp.barracks_in_middle
assert ramp.depot_in_middle
assert len(ramp.corner_depots) == 2
assert ramp.upper2_for_ramp_wall
else:
# On maps it is unable to find valid wall positions (Honorgrounds LE) it should return None
assert ramp.barracks_correct_placement is None
assert ramp.barracks_in_middle is None
assert ramp.depot_in_middle is None
assert ramp.corner_depots == set()
assert ramp.top_center
assert ramp.bottom_center
assert ramp.size
assert ramp.points
assert ramp.upper2_for_ramp_wall
assert ramp.upper
assert ramp.lower

Expand Down