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
45 changes: 23 additions & 22 deletions sc2/bot_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from collections import Counter
from typing import Any, Dict, List, Optional, Set, Tuple, Union # mypy type checking

from s2clientprotocol import common_pb2 as common_pb

from .cache import property_cache_forever, property_cache_once_per_frame
from .data import ActionResult, Alert, Race, Result, Target, race_gas, race_townhalls, race_worker
from .game_data import AbilityData, GameData
Expand Down Expand Up @@ -126,15 +124,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 @@ -149,13 +156,9 @@ def expansion_locations(self) -> Dict[Point2, Units]:

# 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]
resource_groups = [[resource] for resource in self.state.resources]
# Loop the merging process as long as we change something
found_something = True
while found_something:
Expand All @@ -177,9 +180,8 @@ def expansion_locations(self) -> Dict[Point2, Units]:
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
for x, y in itertools.product(range(-offset_range, offset_range + 1), repeat=2)
if math.hypot(x, y) <= 8
]
# Dict we want to return
centers = {}
Expand All @@ -189,20 +191,19 @@ def expansion_locations(self) -> Dict[Point2, Units]:
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
center_x = int(sum(resource.position.x for resource in resources) / amount) + 0.5
center_y = int(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
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)
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
Expand Down
20 changes: 20 additions & 0 deletions sc2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,26 @@ async def get_game_data(self) -> GameData:
)
return GameData(result.data)

async def dump_data(self, ability_id=True, unit_type_id=True, upgrade_id=True, buff_id=True, effect_id=True):
"""
Dump the game data files
choose what data to dump in the keywords
this function writes to a text file
call it one time in on_step with:
await self._client.dump_data()
"""
result = await self._execute(
data=sc_pb.RequestData(
ability_id=ability_id,
unit_type_id=unit_type_id,
upgrade_id=upgrade_id,
buff_id=buff_id,
effect_id=effect_id,
)
)
with open("data_dump.txt", "a") as file:
file.write(str(result.data))

async def get_game_info(self) -> GameInfo:
result = await self._execute(game_info=sc_pb.RequestGameInfo())
return GameInfo(result.game_info)
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
Loading