Skip to content

Commit 63082ef

Browse files
authored
Merge pull request #274 from tweakimp/develop
Update library
2 parents fd8c08a + 3ceca2f commit 63082ef

File tree

9 files changed

+172
-175
lines changed

9 files changed

+172
-175
lines changed

sc2/bot_ai.py

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def main_base_ramp(self) -> "Ramp":
133133
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) """
134134
self.cached_main_base_ramp = min(
135135
(ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {2, 5}),
136-
key=lambda r: self.start_location._distance_squared(r.top_center),
136+
key=lambda r: self.start_location.distance_to(r.top_center),
137137
)
138138
return self.cached_main_base_ramp
139139

@@ -148,7 +148,7 @@ def expansion_locations(self) -> Dict[Point2, Units]:
148148
# any resource in a group is closer than 6 to any resource of another group
149149

150150
# Distance we group resources by
151-
RESOURCE_SPREAD_THRESHOLD = 36
151+
RESOURCE_SPREAD_THRESHOLD = 8.5
152152
minerals = self.state.mineral_field
153153
geysers = self.state.vespene_geyser
154154
all_resources = minerals | geysers
@@ -164,7 +164,7 @@ def expansion_locations(self) -> Dict[Point2, Units]:
164164
for group_a, group_b in itertools.combinations(resource_groups, 2):
165165
# Check if any pair of resource of these groups is closer than threshold together
166166
if any(
167-
resource_a.position._distance_squared(resource_b.position) <= RESOURCE_SPREAD_THRESHOLD
167+
resource_a.distance_to(resource_b) <= RESOURCE_SPREAD_THRESHOLD
168168
for resource_a, resource_b in itertools.product(group_a, group_b)
169169
):
170170
# Remove the single groups and add the merged group
@@ -179,7 +179,7 @@ def expansion_locations(self) -> Dict[Point2, Units]:
179179
(x, y)
180180
for x in range(-offset_range, offset_range + 1)
181181
for y in range(-offset_range, offset_range + 1)
182-
if 49 >= x ** 2 + y ** 2 >= 16
182+
if 4 <= math.hypot(x, y) <= 7
183183
]
184184
# Dict we want to return
185185
centers = {}
@@ -199,17 +199,11 @@ def expansion_locations(self) -> Dict[Point2, Units]:
199199
# Check if point can be built on
200200
if self._game_info.placement_grid[point.rounded] != 0
201201
# Check if all resources have enough space to point
202-
and all(
203-
point._distance_squared(resource.position) >= (49 if resource in geysers else 36)
204-
for resource in resources
205-
)
202+
and all(point.distance_to(resource) >= (7 if resource in geysers else 6) for resource in resources)
206203
)
207204
# Choose best fitting point
208205
# TODO can we improve this by calculating the distance only one time?
209-
result = min(
210-
possible_points,
211-
key=lambda point: sum(point._distance_squared(resource.position) for resource in resources),
212-
)
206+
result = min(possible_points, key=lambda point: sum(point.distance_to(resource) for resource in resources))
213207
centers[result] = resources
214208
return centers
215209

@@ -271,7 +265,7 @@ async def get_next_expansion(self) -> Optional[Point2]:
271265
for el in self.expansion_locations:
272266

273267
def is_near_to_expansion(t):
274-
return t.position._distance_squared(el) < self.EXPANSION_GAP_THRESHOLD ** 2
268+
return t.distance_to(el) < self.EXPANSION_GAP_THRESHOLD
275269

276270
if any(map(is_near_to_expansion, self.townhalls)):
277271
# already taken
@@ -288,16 +282,27 @@ def is_near_to_expansion(t):
288282

289283
return closest
290284

291-
async def distribute_workers(self):
285+
async def distribute_workers(self, resource_ratio: float = 2):
292286
"""
293287
Distributes workers across all the bases taken.
288+
Keyword `resource_ratio` takes a float. If the current minerals to gas
289+
ratio is bigger than `resource_ratio`, this function prefer filling geysers
290+
first, if it is lower, it will prefer sending workers to minerals first.
291+
This is only for workers that need to be moved anyways, it will NOT will
292+
geysers on its own.
293+
294+
NOTE: This function is far from optimal, if you really want to have
295+
refined worker control, you should write your own distribution function.
296+
For example long distance mining control and moving workers if a base was killed
297+
are not being handled.
298+
294299
WARNING: This is quite slow when there are lots of workers or multiple bases.
295300
"""
296-
if not self.state.mineral_field or not self.workers or not self.owned_expansions.ready:
301+
if not self.state.mineral_field or not self.workers or not self.townhalls.ready:
297302
return
298303
actions = []
299304
worker_pool = [worker for worker in self.workers.idle]
300-
bases = self.owned_expansions.ready
305+
bases = self.townhalls.ready
301306
geysers = self.geysers.ready
302307

303308
# list of places that need more workers
@@ -308,53 +313,79 @@ async def distribute_workers(self):
308313
# perfect amount of workers, skip mining place
309314
if not difference:
310315
continue
311-
if mining_place.has_vespene:
316+
if mining_place.is_vespene_geyser:
312317
# get all workers that target the gas extraction site
313318
# or are on their way back from it
314319
local_workers = self.workers.filter(
315320
lambda unit: unit.order_target == mining_place.tag
316321
or (unit.is_carrying_vespene and unit.order_target == bases.closest_to(mining_place).tag)
317322
)
318323
else:
319-
# get minerals around expansion
320-
local_minerals = self.expansion_locations[mining_place.position].filter(
321-
lambda resource: resource.has_minerals
322-
)
324+
# get tags of minerals around expansion
325+
local_minerals_tags = {
326+
mineral.tag for mineral in self.state.mineral_field if mineral.distance_to(mining_place) <= 8
327+
}
323328
# get all target tags a worker can have
324329
# tags of the minerals he could mine at that base
325330
# get workers that work at that gather site
326331
local_workers = self.workers.filter(
327-
lambda unit: unit.order_target in local_minerals.tags
332+
lambda unit: unit.order_target in local_minerals_tags
328333
or (unit.is_carrying_minerals and unit.order_target == mining_place.tag)
329334
)
330335
# too many workers
331336
if difference > 0:
332-
worker_pool.append(local_workers[:difference])
337+
for worker in local_workers[:difference]:
338+
worker_pool.append(worker)
333339
# too few workers
334340
# add mining place to deficit bases for every missing worker
335341
else:
336342
deficit_mining_places += [mining_place for _ in range(-difference)]
337343

344+
# prepare all minerals near a base if we have too many workers
345+
# and need to send them to the closest patch
346+
if len(worker_pool) > len(deficit_mining_places):
347+
all_minerals_near_base = [
348+
mineral
349+
for mineral in self.state.mineral_field
350+
if any(mineral.distance_to(base) <= 8 for base in self.townhalls.ready)
351+
]
338352
# distribute every worker in the pool
339353
for worker in worker_pool:
340354
# as long as have workers and mining places
341355
if deficit_mining_places:
342-
# remove current place from the list for next loop
343-
current_place = deficit_mining_places.pop(0)
356+
# choose only mineral fields first if current mineral to gas ratio is less than target ratio
357+
if self.vespene and self.minerals / self.vespene < resource_ratio:
358+
possible_mining_places = [place for place in deficit_mining_places if not place.vespene_contents]
359+
# else prefer gas
360+
else:
361+
possible_mining_places = [place for place in deficit_mining_places if place.vespene_contents]
362+
# if preferred type is not available any more, get all other places
363+
if not possible_mining_places:
364+
possible_mining_places = deficit_mining_places
365+
# find closest mining place
366+
current_place = min(deficit_mining_places, key=lambda place: place.distance_to(worker))
367+
# remove it from the list
368+
deficit_mining_places.remove(current_place)
344369
# if current place is a gas extraction site, go there
345-
if current_place.has_vespene:
370+
if current_place.vespene_contents:
346371
actions.append(worker.gather(current_place))
347372
# if current place is a gas extraction site,
348373
# go to the mineral field that is near and has the most minerals left
349374
else:
350-
local_minerals = self.expansion_locations[current_place.position].filter(
351-
lambda resource: resource.has_minerals
352-
)
375+
local_minerals = [
376+
mineral for mineral in self.state.mineral_field if mineral.distance_to(current_place) <= 8
377+
]
353378
target_mineral = max(local_minerals, key=lambda mineral: mineral.mineral_contents)
354379
actions.append(worker.gather(target_mineral))
355380
# more workers to distribute than free mining spots
356-
# else:
357-
# pass
381+
# send to closest if worker is doing nothing
382+
elif worker.is_idle and all_minerals_near_base:
383+
target_mineral = min(all_minerals_near_base, key=lambda mineral: mineral.distance_to(worker))
384+
actions.append(worker.gather(target_mineral))
385+
else:
386+
# there are no deficit mining places and worker is not idle
387+
# so dont move him
388+
pass
358389

359390
await self.do_actions(actions)
360391

@@ -366,7 +397,7 @@ def owned_expansions(self) -> Dict[Point2, Unit]:
366397
for el in self.expansion_locations:
367398

368399
def is_near_to_expansion(t):
369-
return t.position._distance_squared(el) < self.EXPANSION_GAP_THRESHOLD ** 2
400+
return t.distance_to(el) < self.EXPANSION_GAP_THRESHOLD
370401

371402
th = next((x for x in self.townhalls if is_near_to_expansion(x)), None)
372403
if th:
@@ -425,21 +456,21 @@ async def can_cast(
425456
ability_target == 1
426457
or ability_target == Target.PointOrNone.value
427458
and isinstance(target, (Point2, Point3))
428-
and unit.position._distance_squared(target.position) <= cast_range ** 2
459+
and unit.distance_to(target) <= cast_range
429460
): # cant replace 1 with "Target.None.value" because ".None" doesnt seem to be a valid enum name
430461
return True
431462
# Check if able to use ability on a unit
432463
elif (
433464
ability_target in {Target.Unit.value, Target.PointOrUnit.value}
434465
and isinstance(target, Unit)
435-
and unit.position._distance_squared(target.position) <= cast_range ** 2
466+
and unit.distance_to(target) <= cast_range
436467
):
437468
return True
438469
# Check if able to use ability on a position
439470
elif (
440471
ability_target in {Target.Point.value, Target.PointOrUnit.value}
441472
and isinstance(target, (Point2, Point3))
442-
and unit.position._distance_squared(target) <= cast_range ** 2
473+
and unit.distance_to(target) <= cast_range
443474
):
444475
return True
445476
return False
@@ -514,7 +545,7 @@ async def find_placement(
514545
if random_alternative:
515546
return random.choice(possible)
516547
else:
517-
return min(possible, key=lambda p: p._distance_squared(near))
548+
return min(possible, key=lambda p: p.distance_to_point2(near))
518549
return None
519550

520551
def already_pending_upgrade(self, upgrade_type: UpgradeId) -> Union[int, float]:

sc2/cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def property_cache_once_per_frame(f):
2020
then clears it if it is accessed in a different game loop.
2121
Only works on properties of the bot object, because it requires
2222
access to self.state.game_loop """
23-
23+
2424
@wraps(f)
2525
def inner(self):
2626
property_cache = "_cache_" + f.__name__

sc2/game_info.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def upper2_for_ramp_wall(self) -> Set[Point2]:
5757
return set() # HACK: makes this work for now
5858
# FIXME: please do
5959

60-
upper2 = sorted(list(self.upper), key=lambda x: x._distance_squared(self.bottom_center), reverse=True)
60+
upper2 = sorted(list(self.upper), key=lambda x: x.distance_to_point2(self.bottom_center), reverse=True)
6161
while len(upper2) > 2:
6262
upper2.pop()
6363
return set(upper2)
@@ -99,7 +99,7 @@ def barracks_in_middle(self) -> Point2:
9999
# Offset from top point to barracks center is (2, 1)
100100
intersects = p1.circle_intersection(p2, 5 ** 0.5)
101101
anyLowerPoint = next(iter(self.lower))
102-
return max(intersects, key=lambda p: p._distance_squared(anyLowerPoint))
102+
return max(intersects, key=lambda p: p.distance_to_point2(anyLowerPoint))
103103
raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.")
104104

105105
@property_immutable_cache
@@ -112,7 +112,7 @@ def depot_in_middle(self) -> Point2:
112112
# Offset from top point to depot center is (1.5, 0.5)
113113
intersects = p1.circle_intersection(p2, 2.5 ** 0.5)
114114
anyLowerPoint = next(iter(self.lower))
115-
return max(intersects, key=lambda p: p._distance_squared(anyLowerPoint))
115+
return max(intersects, key=lambda p: p.distance_to_point2(anyLowerPoint))
116116
raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.")
117117

118118
@property_mutable_cache
@@ -122,7 +122,7 @@ def corner_depots(self) -> Set[Point2]:
122122
points = self.upper2_for_ramp_wall
123123
p1 = points.pop().offset((self.x_offset, self.y_offset)) # still an error with pixelmap?
124124
p2 = points.pop().offset((self.x_offset, self.y_offset))
125-
center = p1.towards(p2, p1.distance_to(p2) / 2)
125+
center = p1.towards(p2, p1.distance_to_point2(p2) / 2)
126126
depotPosition = self.depot_in_middle
127127
# Offset from middle depot to corner depots is (2, 1)
128128
intersects = center.circle_intersection(depotPosition, 5 ** 0.5)
@@ -224,7 +224,7 @@ def paint(pt: Point2) -> None:
224224
if picture[py][px] != NOT_COLORED_YET:
225225
continue
226226
point: Point2 = Point2((px, py))
227-
remaining.remove(point)
227+
remaining.discard(point)
228228
paint(point)
229229
queue.append(point)
230230
currentGroup.add(point)

sc2/game_state.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .position import Point2, Point3
99
from .power_source import PsionicMatrix
1010
from .score import ScoreDetails
11+
from .unit import Unit
1112
from .units import Units
1213

1314

@@ -124,53 +125,53 @@ def __init__(self, response_observation):
124125
# https://github.com/Blizzard/s2client-proto/blob/33f0ecf615aa06ca845ffe4739ef3133f37265a9/s2clientprotocol/score.proto#L31
125126
self.score: ScoreDetails = ScoreDetails(self.observation.score)
126127
self.abilities = self.observation.abilities # abilities of selected units
127-
# Fix for enemy units detected by my sensor tower, as blips have less unit information than normal visible units
128-
blipUnits, minerals, geysers, destructables, enemy, own, watchtowers = ([] for _ in range(7))
128+
129+
self._blipUnits = []
130+
self.own_units: Units = Units([])
131+
self.enemy_units: Units = Units([])
132+
self.mineral_field: Units = Units([])
133+
self.vespene_geyser: Units = Units([])
134+
self.resources: Units = Units([])
135+
self.destructables: Units = Units([])
136+
self.watchtowers: Units = Units([])
137+
self.units: Units = Units([])
129138

130139
for unit in self.observation_raw.units:
131140
if unit.is_blip:
132-
blipUnits.append(unit)
141+
self._blipUnits.append(unit)
133142
else:
143+
unit_obj = Unit(unit)
144+
self.units.append(unit_obj)
134145
alliance = unit.alliance
135146
# Alliance.Neutral.value = 3
136147
if alliance == 3:
137148
unit_type = unit.unit_type
138149
# XELNAGATOWER = 149
139150
if unit_type == 149:
140-
watchtowers.append(unit)
141-
# all destructable rocks except the one below the main base ramps
142-
elif unit.radius > 1.5:
143-
destructables.append(unit)
151+
self.watchtowers.append(unit_obj)
144152
# mineral field enums
145153
elif unit_type in mineral_ids:
146-
minerals.append(unit)
154+
self.mineral_field.append(unit_obj)
155+
self.resources.append(unit_obj)
147156
# geyser enums
148157
elif unit_type in geyser_ids:
149-
geysers.append(unit)
158+
self.vespene_geyser.append(unit_obj)
159+
self.resources.append(unit_obj)
160+
# all destructable rocks
161+
else:
162+
self.destructables.append(unit_obj)
150163
# Alliance.Self.value = 1
151164
elif alliance == 1:
152-
own.append(unit)
165+
self.own_units.append(unit_obj)
153166
# Alliance.Enemy.value = 4
154167
elif alliance == 4:
155-
enemy.append(unit)
156-
157-
resources = minerals + geysers
158-
visible_units = resources + destructables + enemy + own + watchtowers
159-
160-
self.own_units: Units = Units.from_proto(own)
161-
self.enemy_units: Units = Units.from_proto(enemy)
162-
self.mineral_field: Units = Units.from_proto(minerals)
163-
self.vespene_geyser: Units = Units.from_proto(geysers)
164-
self.resources: Units = Units.from_proto(resources)
165-
self.destructables: Units = Units.from_proto(destructables)
166-
self.watchtowers: Units = Units.from_proto(watchtowers)
167-
self.units: Units = Units.from_proto(visible_units)
168+
self.enemy_units.append(unit_obj)
168169
self.upgrades: Set[UpgradeId] = {UpgradeId(upgrade) for upgrade in self.observation_raw.player.upgrade_ids}
169170

170171
# Set of unit tags that died this step
171172
self.dead_units: Set[int] = {dead_unit_tag for dead_unit_tag in self.observation_raw.event.dead_units}
172-
173-
self.blips: Set[Blip] = {Blip(unit) for unit in blipUnits}
173+
# Set of enemy units detected by own sensor tower, as blips have less unit information than normal visible units
174+
self.blips: Set[Blip] = {Blip(unit) for unit in self._blipUnits}
174175
# self.visibility[point]: 0=Hidden, 1=Fogged, 2=Visible
175176
self.visibility: PixelMap = PixelMap(self.observation_raw.map_state.visibility, mirrored=True)
176177
# self.creep[point]: 0=No creep, 1=creep

0 commit comments

Comments
 (0)