Skip to content

Commit 32784d5

Browse files
authored
Merge pull request #1 from tweakimp/develop
Develop
2 parents a5dbfb2 + 1dc888e commit 32784d5

File tree

9 files changed

+207
-202
lines changed

9 files changed

+207
-202
lines changed

sc2/bot_ai.py

Lines changed: 68 additions & 37 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]:
@@ -752,7 +783,7 @@ def _prepare_first_step(self):
752783
"""First step extra preparations. Must not be called before _prepare_step."""
753784
if self.townhalls:
754785
self._game_info.player_start_location = self.townhalls.first.position
755-
self._game_info.map_ramps = self._game_info._find_ramps()
786+
self._game_info.map_ramps, self._game_info.vision_blockers = self._game_info._find_ramps_and_vision_blockers()
756787

757788
def _prepare_step(self, state, proto_game_info):
758789
# Set attributes from new state before on_step."""

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: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from collections import deque
22
from typing import Any, Deque, Dict, FrozenSet, Generator, List, Optional, Sequence, Set, Tuple, Union
33

4+
import numpy as np
5+
46
from .cache import property_immutable_cache, property_mutable_cache
57
from .pixel_map import PixelMap
68
from .player import Player
@@ -57,7 +59,7 @@ def upper2_for_ramp_wall(self) -> Set[Point2]:
5759
return set() # HACK: makes this work for now
5860
# FIXME: please do
5961

60-
upper2 = sorted(list(self.upper), key=lambda x: x._distance_squared(self.bottom_center), reverse=True)
62+
upper2 = sorted(list(self.upper), key=lambda x: x.distance_to_point2(self.bottom_center), reverse=True)
6163
while len(upper2) > 2:
6264
upper2.pop()
6365
return set(upper2)
@@ -99,7 +101,7 @@ def barracks_in_middle(self) -> Point2:
99101
# Offset from top point to barracks center is (2, 1)
100102
intersects = p1.circle_intersection(p2, 5 ** 0.5)
101103
anyLowerPoint = next(iter(self.lower))
102-
return max(intersects, key=lambda p: p._distance_squared(anyLowerPoint))
104+
return max(intersects, key=lambda p: p.distance_to_point2(anyLowerPoint))
103105
raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.")
104106

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

118120
@property_mutable_cache
@@ -122,7 +124,7 @@ def corner_depots(self) -> Set[Point2]:
122124
points = self.upper2_for_ramp_wall
123125
p1 = points.pop().offset((self.x_offset, self.y_offset)) # still an error with pixelmap?
124126
p2 = points.pop().offset((self.x_offset, self.y_offset))
125-
center = p1.towards(p2, p1.distance_to(p2) / 2)
127+
center = p1.towards(p2, p1.distance_to_point2(p2) / 2)
126128
depotPosition = self.depot_in_middle
127129
# Offset from middle depot to corner depots is (2, 1)
128130
intersects = center.circle_intersection(depotPosition, 5 ** 0.5)
@@ -168,22 +170,38 @@ def __init__(self, proto):
168170
self.playable_area = Rect.from_proto(self._proto.start_raw.playable_area)
169171
self.map_center = self.playable_area.center
170172
self.map_ramps: List[Ramp] = None # Filled later by BotAI._prepare_first_step
173+
self.vision_blockers: Set[Point2] = None # Filled later by BotAI._prepare_first_step
171174
self.player_races: Dict[int, "Race"] = {
172175
p.player_id: p.race_actual or p.race_requested for p in self._proto.player_info
173176
}
174177
self.start_locations: List[Point2] = [Point2.from_proto(sl) for sl in self._proto.start_raw.start_locations]
175178
self.player_start_location: Point2 = None # Filled later by BotAI._prepare_first_step
176179

177-
def _find_ramps(self) -> List[Ramp]:
178-
"""Calculate (self.pathing_grid - self.placement_grid) (for sets) and then find ramps by comparing heights."""
180+
def _find_ramps_and_vision_blockers(self) -> Tuple[List[Ramp], Set[Point2]]:
181+
""" Calculate points that are pathable but not placeable.
182+
Then devide them into ramp points if not all points around the points are equal height
183+
and into vision blockers if they are. """
184+
185+
def equal_height_around(tile):
186+
# mask to slice array 1 around tile
187+
sliced = self.terrain_height.data_numpy[tile[1] - 1 : tile[1] + 2, tile[0] - 1 : tile[0] + 2]
188+
return len(np.unique(sliced)) == 1
189+
179190
map_area = self.playable_area
180-
rampPoints = (
181-
Point2((x, y))
182-
for x in range(map_area.x, map_area.x + map_area.width)
183-
for y in range(map_area.y, map_area.y + map_area.height)
184-
if self.placement_grid[(x, y)] == 0 and self.pathing_grid[(x, y)] == 1
185-
)
186-
return [Ramp(group, self) for group in self._find_groups(rampPoints)]
191+
# all points in the playable area that are pathable but not placable
192+
points = [
193+
Point2((b, a))
194+
for (a, b), value in np.ndenumerate(self.pathing_grid.data_numpy)
195+
if value == 1
196+
and map_area.x <= a < map_area.x + map_area.width
197+
and map_area.y <= b < map_area.y + map_area.height
198+
and self.placement_grid[(b, a)] == 0
199+
]
200+
# devide points into ramp points and vision blockers
201+
rampPoints = [point for point in points if not equal_height_around(point)]
202+
visionBlockers = set(point for point in points if equal_height_around(point))
203+
ramps = [Ramp(group, self) for group in self._find_groups(rampPoints)]
204+
return ramps, visionBlockers
187205

188206
def _find_groups(self, points: Set[Point2], minimum_points_per_group: int = 8):
189207
"""
@@ -224,7 +242,7 @@ def paint(pt: Point2) -> None:
224242
if picture[py][px] != NOT_COLORED_YET:
225243
continue
226244
point: Point2 = Point2((px, py))
227-
remaining.remove(point)
245+
remaining.discard(point)
228246
paint(point)
229247
queue.append(point)
230248
currentGroup.add(point)

0 commit comments

Comments
 (0)