├── CreepyBot ├── CreepyBot.py ├── LICENSE.txt ├── README.txt ├── __init__.py ├── example_bot.py └── run.py ├── DragonBot ├── burny_basic_ai.py └── dragon_bot.py ├── LadderBots.json └── README.md /CreepyBot/CreepyBot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bot name: CreepyBot 3 | Bot author: BuRny (or BurnySc2) 4 | Bot version: v1.0 5 | Bot date (YYYY-MM-DD): 2018-06-17 6 | 7 | This bot was made by BuRny for the "KOTN: Probots" tournament 8 | https://eschamp.challonge.com/Probot1 9 | 10 | Homepage: https://github.com/BurnySc2 11 | """ 12 | 13 | 14 | # pylint: disable=E0602 15 | """ylint: disable=E0001 16 | ylint: disable=C, E0602, W0612, W0702, W0621, R0912, R0915, W0603 17 | add a "p" 18 | """ 19 | 20 | import random, json, time 21 | from collections import OrderedDict 22 | 23 | # download maps from https://github.com/Blizzard/s2client-proto#map-packs 24 | 25 | # import os # load text file 26 | import math # distance calculation 27 | # import re # parsing build orders from text file 28 | from sc2.unit import Unit 29 | from sc2.units import Units 30 | from sc2.data import race_gas, race_worker, race_townhalls, ActionResult, Attribute, Race 31 | 32 | import sc2 # pip install sc2 33 | from sc2 import Race, Difficulty 34 | from sc2.constants import * # for autocomplete 35 | from sc2.ids.unit_typeid import * 36 | from sc2.ids.ability_id import * 37 | from sc2.position import Point2, Point3 38 | 39 | from sc2.player import Bot, Computer, Human 40 | 41 | # from zerg.zerg_rush import ZergRushBot 42 | # from protoss.cannon_rush import CannonRushBot 43 | 44 | class ManageThreats(object): 45 | def __init__(self, client, game_data): 46 | # usage: 47 | # self.defenseGroup = ManageThreats(self._client, self._game_data) 48 | 49 | # class data: 50 | self._client = client 51 | self._game_data = game_data 52 | self.threats = {} 53 | self.assignedUnitsTags = set() 54 | self.unassignedUnitsTags = set() 55 | 56 | # customizable parameters upon instance creation 57 | self.retreatLocations = None # retreat to the nearest location if hp percentage reached below "self.retreatWhenHp" 58 | self.retreatWhenHp = 0 # make a unit micro and retreat when this HP percentage is reached 59 | 60 | self.attackLocations = None # attack any of these locations if there are no threats 61 | 62 | self.treatThreatsAsAllies = False # if True, will mark threats as allies and tries to protect them instead 63 | # self.defendRange = 5 # if a unit is in range within 5 of any of the threats, attack them 64 | 65 | self.clumpUpEnabled = False 66 | self.clumpDistance = 7 # not yet tested - sums up the distance to the center of the unit-ball, if too far away and not engaged with enemy: will make them clump up before engaging again 67 | 68 | self.maxAssignedPerUnit = 10 # the maximum number of units that can be assigned per enemy unit / threat 69 | 70 | self.leader = None # will be automatically assigned if "self.attackLocations" is not None 71 | 72 | availableModes = ["closest", "distributeEqually"] # todo: focus fire 73 | self.mode = "closest" 74 | 75 | def addThreat(self, enemies): 76 | if isinstance(enemies, Units): 77 | for unit in enemies: 78 | self.addThreat(unit) 79 | elif isinstance(enemies, Unit): 80 | self.addThreat(enemies.tag) 81 | elif isinstance(enemies, int): 82 | if enemies not in self.threats: 83 | self.threats[enemies] = set() 84 | 85 | def clearThreats(self, threats=None): 86 | # accepts None, integer or iterable (with tags) as argument 87 | if threats is None: 88 | threats = self.threats 89 | elif isinstance(threats, int): 90 | threats = set([threats]) 91 | 92 | # check for dead threats: 93 | for threat in threats: 94 | if threat in self.threats: 95 | unitsThatNowHaveNoTarget = self.threats.pop(threat) # remove and return the set 96 | self.assignedUnitsTags -= unitsThatNowHaveNoTarget 97 | self.unassignedUnitsTags |= unitsThatNowHaveNoTarget # append the tags to unassignedUnits 98 | 99 | def addDefense(self, myUnits): 100 | if isinstance(myUnits, Units): 101 | for unit in myUnits: 102 | self.addDefense(unit) 103 | elif isinstance(myUnits, Unit): 104 | self.addDefense(myUnits.tag) 105 | elif isinstance(myUnits, int): 106 | if myUnits not in self.assignedUnitsTags: 107 | self.unassignedUnitsTags.add(myUnits) 108 | 109 | def removeDefense(self, myUnits): 110 | if isinstance(myUnits, Units): 111 | for unit in myUnits: 112 | self.removeDefense(unit) 113 | elif isinstance(myUnits, Unit): 114 | self.removeDefense(myUnits.tag) 115 | elif isinstance(myUnits, int): 116 | self.assignedUnitsTags.discard(myUnits) 117 | self.unassignedUnitsTags.discard(myUnits) 118 | for key in self.threats.keys(): 119 | self.threats[key].discard(myUnits) 120 | 121 | def setRetreatLocations(self, locations, removePreviousLocations=False): 122 | if self.retreatLocations is None or removePreviousLocations: 123 | self.retreatLocations = [] 124 | if isinstance(locations, list): 125 | # we assume this is a list of points or units 126 | for location in locations: 127 | self.retreatLocations.append(location.position.to2) 128 | else: 129 | self.retreatLocations.append(location.position.to2) 130 | 131 | def unassignUnit(self, myUnit): 132 | for key, value in self.threats.items(): 133 | # if myUnit.tag in value: 134 | value.discard(myUnit.tag) 135 | # break 136 | self.unassignedUnitsTags.add(myUnit.tag) 137 | self.assignedUnitsTags.discard(myUnit.tag) 138 | 139 | def getThreatTags(self): 140 | """Returns a set of unit tags that are considered as threats 141 | 142 | Returns: 143 | set -- set of enemy unit tags 144 | """ 145 | return set(self.threats.keys()) 146 | 147 | def getMyUnitTags(self): 148 | """Returns a set of tags that are in this group 149 | 150 | Returns: 151 | set -- set of my unit tags 152 | """ 153 | return self.assignedUnitsTags | self.unassignedUnitsTags 154 | 155 | def centerOfUnits(self, units): 156 | if isinstance(units, list): 157 | units = Units(units, self._game_data) 158 | assert isinstance(units, Units) 159 | assert units.exists 160 | if len(units) == 1: 161 | return units[0].position.to2 162 | coordX = sum([unit.position.x for unit in units]) / len(units) 163 | coordY = sum([unit.position.y for unit in units]) / len(units) 164 | return Point2((coordX, coordY)) 165 | 166 | async def update(self, myUnitsFromState, enemyUnitsFromState, enemyStartLocations, iteration): 167 | # example usage: attackgroup1.update(self.units, self.known_enemy_units, self.enemy_start_locations, iteration) 168 | assignedUnits = myUnitsFromState.filter(lambda x:x.tag in self.assignedUnitsTags) 169 | unassignedUnits = myUnitsFromState.filter(lambda x:x.tag in self.unassignedUnitsTags) 170 | if not self.treatThreatsAsAllies: 171 | threats = enemyUnitsFromState.filter(lambda x:x.tag in self.threats) 172 | else: 173 | threats = myUnitsFromState.filter(lambda x:x.tag in self.threats) 174 | aliveThreatTags = {x.tag for x in threats} 175 | deadThreatTags = {k for k in self.threats.keys() if k not in aliveThreatTags} 176 | 177 | # check for dead threats: 178 | self.clearThreats(threats=deadThreatTags) 179 | 180 | # check for dead units: 181 | self.assignedUnitsTags = {x.tag for x in assignedUnits} 182 | self.unassignedUnitsTags = {x.tag for x in unassignedUnits} 183 | # update dead assigned units inside the dicts 184 | for key in self.threats.keys(): 185 | values = self.threats[key] 186 | self.threats[key] = {x for x in values if x in self.assignedUnitsTags} 187 | 188 | # if self.treatThreatsAsAllies: 189 | # print("supportgroup threat tags:", self.getThreatTags()) 190 | # print("supportgroup existing threats:", threats) 191 | # for k,v in self.threats.items(): 192 | # print(k,v) 193 | # print("supportgroup units unassigned:", unassignedUnits) 194 | # print("supportgroup units assigned:", assignedUnits) 195 | 196 | canAttackAir = [QUEEN, CORRUPTOR] 197 | canAttackGround = [ROACH, BROODLORD, QUEEN, ZERGLING] 198 | 199 | recentlyAssigned = set() 200 | # assign unassigned units a threat # TODO: attackmove on the position or attack the unit? 201 | for unassignedUnit in unassignedUnits.filter(lambda x:x.health / x.health_max > self.retreatWhenHp): 202 | # if self.retreatLocations is not None and unassignedUnit.health / unassignedUnit.health_max < self.retreatWhenHp: 203 | # continue 204 | # if len(unassignedUnit.orders) == 1 and unassignedUnit.orders[0].ability.id in [AbilityId.ATTACK]: 205 | # continue 206 | if not threats.exists: 207 | if self.attackLocations is not None and unassignedUnit.is_idle: 208 | await self.do(unassignedUnit.move(random.choice(self.attackLocations))) 209 | else: 210 | # filters threats if current looped unit can attack air (and enemy is flying) or can attack ground (and enemy is ground unit) 211 | # also checks if current unit is in threats at all and if the maxAssigned is not overstepped 212 | filteredThreats = threats.filter(lambda x: x.tag in self.threats and len(self.threats[x.tag]) < self.maxAssignedPerUnit and ((x.is_flying and unassignedUnit.type_id in canAttackAir) or (not x.is_flying and unassignedUnit.type_id in canAttackGround))) 213 | 214 | chosenTarget = None 215 | if not filteredThreats.exists and threats.exists: 216 | chosenTarget = threats.random # for units like viper which cant attack, they will just amove there 217 | elif self.mode == "closest": 218 | # TODO: only attack units that this unit can actually attack, like dont assign air if it cant shoot up 219 | if filteredThreats.exists: 220 | # only assign targets if there are any threats left 221 | chosenTarget = filteredThreats.closest_to(unassignedUnit) 222 | elif self.mode == "distributeEqually": 223 | threatTagWithLeastAssigned = min([[x, len(y)] for x, y in self.threats.items()], key=lambda q: q[1]) 224 | # if self.treatThreatsAsAllies: 225 | # print("supportgroup least assigned", threatTagWithLeastAssigned) 226 | # if self.treatThreatsAsAllies: 227 | # print("supportgroup filtered threats", filteredThreats) 228 | if filteredThreats.exists: 229 | # only assign target if there are any threats remaining that have no assigned allied units 230 | chosenTarget = filteredThreats.find_by_tag(threatTagWithLeastAssigned[0]) 231 | # if self.treatThreatsAsAllies: 232 | # print("supportgroup chosen target", chosenTarget) 233 | else: 234 | chosenTarget = random.choice(threats) 235 | 236 | if chosenTarget is not None: 237 | # add unit to assigned target 238 | self.unassignedUnitsTags.discard(unassignedUnit.tag) 239 | self.assignedUnitsTags.add(unassignedUnit.tag) 240 | self.threats[chosenTarget.tag].add(unassignedUnit.tag) 241 | recentlyAssigned.add(unassignedUnit.tag) 242 | # threats.remove(chosenTarget) 243 | unassignedUnits.remove(unassignedUnit) 244 | assignedUnits.append(unassignedUnit) 245 | if unassignedUnit.distance_to(chosenTarget) > 3: 246 | # amove towards target when we want to help allied units 247 | await self.do(unassignedUnit.attack(chosenTarget.position)) 248 | break # iterating over changing list 249 | 250 | # if self.treatThreatsAsAllies and len(recentlyAssigned) > 0: 251 | # print("supportgroup recently assigned", recentlyAssigned) 252 | 253 | clumpedUnits = False 254 | if assignedUnits.exists and self.clumpUpEnabled: 255 | amountUnitsInDanger = [threats.closer_than(10, x).exists for x in assignedUnits].count(True) 256 | # print("wanting to clump up") 257 | if amountUnitsInDanger < assignedUnits.amount / 5: # if only 10% are in danger, then its worth the risk to clump up again 258 | # make all units clump up more until trying to push / attack again 259 | center = self.centerOfUnits(assignedUnits) 260 | distanceSum = 0 261 | for u in assignedUnits: 262 | distanceSum += u.distance_to(center) 263 | distanceSum /= assignedUnits.amount 264 | 265 | if distanceSum > self.clumpDistance: 266 | clumpedUnits = True 267 | for unit in assignedUnits: 268 | await self.do(unit.attack(center)) 269 | 270 | if not clumpedUnits: 271 | for unit in assignedUnits: 272 | if unit.tag in recentlyAssigned: 273 | continue 274 | # # move close to leader if he exists and if unit is far from leader 275 | # if self.attackLocations is not None \ 276 | # and leader is not None \ 277 | # and unit.tag != leader.tag \ 278 | # and (unit.is_idle or len(unit.orders) == 1 and unit.orders[0].ability.id in [AbilityId.MOVE]) \ 279 | # and unit.distance_to(leader) > self.clumpDistance: 280 | # await self.do(unit.attack(leader.position)) 281 | 282 | # if unit is idle or move commanding, move directly to target, if close to target, amove 283 | if unit.is_idle or len(unit.orders) == 1 and unit.orders[0].ability.id in [AbilityId.MOVE]: 284 | assignedTargetTag = next((k for k,v in self.threats.items() if unit.tag in v), None) 285 | if assignedTargetTag is not None: 286 | assignedTarget = threats.find_by_tag(assignedTargetTag) 287 | if assignedTarget is None: 288 | self.unassignUnit(unit) 289 | elif assignedTarget.distance_to(unit) <= 13 or threats.filter(lambda x: x.distance_to(unit) < 13).exists: 290 | await self.do(unit.attack(assignedTarget.position)) 291 | elif assignedTarget.distance_to(unit) > 13 and unit.is_idle and unit.tag != assignedTarget.tag: 292 | await self.do(unit.attack(unit.position.to2.towards(assignedTarget.position.to2, 20))) # move follow command 293 | else: 294 | self.unassignUnit(unit) 295 | # # if unit.is_idle: 296 | # # self.unassignUnit(unit) 297 | # elif len(unit.orders) == 1 and unit.orders[0].ability.id in [AbilityId.MOVE]: 298 | # # make it amove again 299 | # for key, value in self.threats.items(): 300 | # if unit.tag in value: 301 | # assignedTargetTag = key 302 | # assignedTarget = threats.find_by_tag(assignedTargetTag) 303 | # if assignedTarget is None: 304 | # continue 305 | # # self.unassignUnit(unit) 306 | # elif assignedTarget.distance_to(unit) <= 13: 307 | # await self.do(unit.attack(assignedTarget.position)) 308 | # break 309 | # # elif assignedTarget.distance_to(unit) > 13: 310 | # # await self.do(unit.move(assignedTarget)) 311 | 312 | # move to retreatLocation when there are no threats or when a unit is low hp 313 | if self.retreatLocations is not None and not threats.exists and iteration % 20 == 0: 314 | for unit in unassignedUnits.idle: 315 | closestRetreatLocation = unit.position.to2.closest(self.retreatLocations) 316 | if unit.distance_to(closestRetreatLocation) > 10: 317 | await self.do(unit.move(closestRetreatLocation)) 318 | 319 | # move when low hp 320 | elif self.retreatLocations is not None and self.retreatWhenHp != 0: 321 | for unit in (assignedUnits | unassignedUnits).filter(lambda x:x.health / x.health_max < self.retreatWhenHp): 322 | closestRetreatLocation = unit.position.to2.closest(self.retreatLocations) 323 | if unit.distance_to(closestRetreatLocation) > 6: 324 | await self.do(unit.move(closestRetreatLocation)) 325 | 326 | async def do(self, action): 327 | r = await self._client.actions(action, game_data=self._game_data) 328 | return r 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | class CreepyBot(sc2.BotAI): 352 | def __init__(self): 353 | self.reservedWorkers = [] 354 | self.queensAssignedHatcheries = {} # contains a list of queen: hatchery assignments (for injects) 355 | self.workerProductionEnabled = [] 356 | self.armyProductionEnabled = [] 357 | self.queenProductionEnabled = [] 358 | self.haltRedistributeWorkers = False 359 | 360 | 361 | # following are default TRUE: 362 | self.enableCreepSpread = True 363 | self.enableInjects = True 364 | 365 | self.getLingSpeed = False 366 | 367 | self.enableMakingRoaches = True 368 | self.getRoachBurrowAndBurrow = True # researches burrow and roach burrow move 369 | self.waitForRoachBurrowBeforeAttacking = True # if this is True, set the above to True also 370 | 371 | self.enableLateGame = True 372 | self.waitForBroodlordsBeforeAttacking = False # if this is True, set the above to True also 373 | self.allowedToResupplyAttackGroups = False 374 | 375 | # the bot will try to make corruptor in this ratio now 376 | self.corruptorRatioFactor = 3 377 | self.broodlordRatioFactor = 3 378 | self.viperRatioFactor = 1 379 | 380 | 381 | # required for overlord production 382 | self.larvaPerHatch = 1 / 11 # 1 larva every 11 secs 383 | self.larvaPerInject = 3 / 40 # 1 larva every 40 secs 384 | 385 | 386 | self.nextExpansionInfo = { 387 | # "workerTag": workerId, 388 | # "location": nextExpansionLocation, 389 | } 390 | 391 | self.opponentInfo = { 392 | "spawnLocation": None, # for 4player maps 393 | "expansions": [], # stores a list of Point2 objects of expansions 394 | "expansionsTags": set(), # stores the expansions above as tags so we dont count them double 395 | "furthestAwayExpansion": None, # stores the expansion furthest away - important for spine crawler and pool placement 396 | "race": None, 397 | "armyTagsScouted": [], # list of dicts with entries: {"tag": 123, "scoutTime": 15.6, "supply": 2} 398 | "armySupplyScouted": 0, 399 | "armySupplyScoutedClose": 0, 400 | "armySupplyVisible": 0, 401 | "scoutingUnitsNeeded": 0, 402 | } 403 | self.myDefendGroup = None 404 | self.myAttackGroup = None 405 | self.mySupportGroup = None 406 | 407 | self.droneLimit = 80 # how many drones to have at late game 408 | 409 | self.defendRangeToTownhalls = 30 # how close the enemy has to be before defenses are alerted 410 | 411 | self.priotizeFirstNQueens = 4 412 | self.totalQueenLimit = 6 # dont have to change it, instead change the two lines below to set the queen limit before/after GREATERSPIRE tech 413 | self.totalEarlyGameQueenLimit = 8 414 | self.totalLateGameQueenLimit = 25 415 | self.injectQueenLimit = 4 # how many queens will be injecting until we have a greater spire? 416 | self.stopMakingNewTumorsWhenAtCoverage = 0.3 # stops queens from putting down new tumors and save up transfuse energy 417 | self.creepTargetDistance = 15 # was 10 418 | self.creepTargetCountsAsReachedDistance = 10 # was 25 419 | 420 | 421 | self.creepSpreadInterval = 10 422 | self.injectInterval = 100 423 | self.workerTransferInterval = 10 424 | self.buildStuffInverval = 2 # was 4 425 | self.microInterval = 1 # was 3 426 | 427 | self.prepareStart2Ran = False 428 | 429 | async def _prepare_start2(self): 430 | # print("distance to closest mineral field:", self.state.mineral_field.closest_to(self.townhalls.random).distance_to(self.townhalls.random)) 431 | # is about 6.0 432 | 433 | self.prepareStart2Ran = True 434 | # find start locations so creep tumors dont block it 435 | if self.enableCreepSpread: 436 | await self.findExactExpansionLocations() 437 | 438 | # split workers to closest mineral field 439 | if self.townhalls.exists: 440 | mfs = self.state.mineral_field.closer_than(10, self.townhalls.random) 441 | for drone in self.units(DRONE): 442 | await self.do(drone.gather(mfs.closest_to(drone))) 443 | 444 | # set amount of spawn locations 445 | self.opponentInfo["scoutingUnitsNeeded"] = len(self.enemy_start_locations) 446 | 447 | if self.townhalls.exists: 448 | self.opponentInfo["furthestAwayExpansion"] = self.townhalls.random.position.to2.closest(self.enemy_start_locations) 449 | 450 | # a = self.state.units.filter(lambda x: x.name == "DestructibleRockEx16x6") 451 | # print(a) 452 | # print(a.random) 453 | # print(vars(a.random)) 454 | # print(a.random._type_data) 455 | # print(vars(a.random._type_data)) 456 | # print(a.random._game_data) 457 | # print(vars(a.random._game_data)) 458 | 459 | def getTimeInSeconds(self): 460 | # returns real time if game is played on "faster" 461 | return self.state.game_loop * 0.725 * (1/16) 462 | 463 | def getUnitInfo(self, unit, field="food_required"): 464 | # get various unit data, see list below 465 | # usage: getUnitInfo(ROACH, "mineral_cost") 466 | assert isinstance(unit, (Unit, UnitTypeId)) 467 | if isinstance(unit, Unit): 468 | # unit = unit.type_id 469 | unit = unit._type_data._proto 470 | else: 471 | unit = self._game_data.units[unit.value]._proto 472 | # unit = self._game_data.units[unit.value] 473 | # print(vars(unit)) # uncomment to get the list below 474 | if hasattr(unit, field): 475 | return getattr(unit, field) 476 | else: 477 | return None 478 | """ 479 | name: "Drone" 480 | available: true 481 | cargo_size: 1 482 | attributes: Light 483 | attributes: Biological 484 | movement_speed: 2.8125 485 | armor: 0.0 486 | weapons { 487 | type: Ground 488 | damage: 5.0 489 | attacks: 1 490 | range: 0.10009765625 491 | speed: 1.5 492 | } 493 | mineral_cost: 50 494 | vespene_cost: 0 495 | food_required: 1.0 496 | ability_id: 1342 497 | race: Zerg 498 | build_time: 272.0 499 | sight_range: 8.0 500 | """ 501 | 502 | def convertWeaponInfo(self, info): 503 | types = {1: "ground", 2: "air", 3:"any"} 504 | if info is None: 505 | return None 506 | returnDict = { 507 | "type": types[info.type], 508 | "damage": info.damage, 509 | "attacks": info.attacks, 510 | "range": info.range, 511 | "speed": info.speed, 512 | "dps": info.damage * info.attacks / info.speed 513 | } 514 | if hasattr(info, "damage_bonus"): 515 | bonus = info.damage_bonus 516 | try: 517 | # TODO: try to get rid of try / except and change attribute to "light" or "armored" etc 518 | returnDict["bonusAttribute"] = bonus[0].attribute 519 | returnDict["bonusDamage"] = bonus[0].bonus 520 | except: pass 521 | return returnDict 522 | 523 | def getSpecificUnitInfo(self, unit, query="dps"): 524 | # usage: print(self.getSpecificUnitInfo(self.units(DRONE).random)[0]["dps"]) 525 | if query == "dps": 526 | unitInfo = self.getUnitInfo(unit, "weapons") 527 | if unitInfo is None: 528 | return None, None 529 | weaponInfos = [] 530 | for weapon in unitInfo: 531 | weaponInfos.append(self.convertWeaponInfo(weapon)) 532 | if len(weaponInfos) == 0: 533 | return [{"dps": 0}] 534 | return weaponInfos 535 | 536 | def centerOfUnits(self, units): 537 | if isinstance(units, list): 538 | units = Units(units, self._game_data) 539 | assert isinstance(units, Units) 540 | assert units.exists 541 | if len(units) == 1: 542 | return units[0].position.to2 543 | coordX = sum([unit.position.x for unit in units]) / len(units) 544 | coordY = sum([unit.position.y for unit in units]) / len(units) 545 | return Point2((coordX, coordY)) 546 | 547 | # def findUnitGroup(self, unit, unitsCloseTo, maxDistanceToOtherUnits=30, minSupply=0, excludeUnits=None): 548 | # # group clustering https://mubaris.com/2017/10/01/kmeans-clustering-in-python/ 549 | # # actually this function is not really related to that algorithm 550 | 551 | # # this function takes two required arguments 552 | # # unit - a unit spotted, e.g. enemy unit closest to a friendly building 553 | # # unitsCloseTo - a group of units, e.g. self.units(ROACH) | self.units(HYDRA) 554 | # # or self.state.units.enemy.not_structure 555 | 556 | # assert isinstance(unitsCloseTo, Units) 557 | # if unitsCloseTo.amount == 0: 558 | # return [] 559 | 560 | # unitGroup = unitsCloseTo.closer_than(maxDistanceToOtherUnits, unit.position) 561 | # if excludeUnits != None: 562 | # assert isinstance(excludeUnits, Units) 563 | # unitGroup - excludeUnits 564 | # if minSupply > 0: 565 | # supply = sum([self.getUnitInfo(x, "food_required") for x in unitGroup]) 566 | # if minSupply > supply: 567 | # return self.units(QUEEN).not_ready # empty units list, idk how to create an empty one :( 568 | # return unitGroup 569 | 570 | # def unitsFromList(self, lst): 571 | # assert isinstance(lst, list) 572 | # if len(lst) == 0: 573 | # # return self.units(QUEEN).not_ready 574 | # return Units([], self._game_data) 575 | # # elif len(lst) == 1: 576 | # # return lst[0] 577 | # else: 578 | # # returnUnits = self.units(QUEEN).not_ready 579 | # returnUnits = Units([], self._game_data) 580 | # for entry in lst: 581 | # returnUnits = returnUnits | entry 582 | # return returnUnits 583 | 584 | async def findExactExpansionLocations(self): 585 | # execute this on start, finds all expansions where creep tumors should not be build near 586 | self.exactExpansionLocations = [] 587 | for loc in self.expansion_locations.keys(): 588 | self.exactExpansionLocations.append(await self.find_placement(HATCHERY, loc, minDistanceToResources=5.5, placement_step=1)) # TODO: change mindistancetoresource so that a hatch still has room to be built 589 | 590 | async def assignWorkerRallyPoint(self): 591 | if hasattr(self, "hatcheryRallyPointsSet"): 592 | for hatch in self.townhalls: 593 | if hatch.tag not in self.hatcheryRallyPointsSet: 594 | # abilities = await self.get_available_abilities(hatch) 595 | # if RALLY_HATCHERY_WORKERS in abilities: 596 | # rally workers to nearest mineral field 597 | mf = self.state.mineral_field.closest_to(hatch.position.to2.offset(Point2((0, -3)))) 598 | err = await self.do(hatch(RALLY_WORKERS, mf)) 599 | if not err: 600 | mfs = self.state.mineral_field.closer_than(10, hatch.position.to2) 601 | if mfs.exists: 602 | loc = self.centerOfUnits(mfs) 603 | err = await self.do(hatch(RALLY_UNITS, loc)) 604 | if not err: 605 | self.hatcheryRallyPointsSet[hatch.tag] = loc 606 | else: 607 | self.hatcheryRallyPointsSet = {} 608 | 609 | def assignQueen(self, maxAmountInjectQueens=5): 610 | # # list of all alive queens and bases, will be used for injecting 611 | if not hasattr(self, "queensAssignedHatcheries"): 612 | self.queensAssignedHatcheries = {} 613 | 614 | if maxAmountInjectQueens == 0: 615 | self.queensAssignedHatcheries = {} 616 | 617 | # if queen is done, move it to the closest hatch/lair/hive that doesnt have a queen assigned 618 | queensNoInjectPartner = self.units(QUEEN).filter(lambda q: q.tag not in self.queensAssignedHatcheries.keys()) 619 | basesNoInjectPartner = self.townhalls.filter(lambda h: h.tag not in self.queensAssignedHatcheries.values() and h.build_progress > 0.8) 620 | 621 | for queen in queensNoInjectPartner: 622 | if basesNoInjectPartner.amount == 0: 623 | break 624 | closestBase = basesNoInjectPartner.closest_to(queen) 625 | self.queensAssignedHatcheries[queen.tag] = closestBase.tag 626 | basesNoInjectPartner = basesNoInjectPartner - [closestBase] 627 | break # else one hatch gets assigned twice 628 | 629 | 630 | async def doQueenInjects(self, iteration): 631 | # list of all alive queens and bases, will be used for injecting 632 | aliveQueenTags = [queen.tag for queen in self.units(QUEEN)] # list of numbers (tags / unit IDs) 633 | aliveBasesTags = [base.tag for base in self.townhalls] 634 | 635 | # make queens inject if they have 25 or more energy 636 | toRemoveTags = [] 637 | 638 | if hasattr(self, "queensAssignedHatcheries"): 639 | for queenTag, hatchTag in self.queensAssignedHatcheries.items(): 640 | # queen is no longer alive 641 | if queenTag not in aliveQueenTags: 642 | toRemoveTags.append(queenTag) 643 | continue 644 | # hatchery / lair / hive is no longer alive 645 | if hatchTag not in aliveBasesTags: 646 | toRemoveTags.append(queenTag) 647 | continue 648 | # queen and base are alive, try to inject if queen has 25+ energy 649 | queen = self.units(QUEEN).find_by_tag(queenTag) 650 | hatch = self.townhalls.find_by_tag(hatchTag) 651 | if hatch.is_ready: 652 | if queen.energy >= 25 and queen.is_idle and not hatch.has_buff(QUEENSPAWNLARVATIMER): 653 | await self.do(queen(EFFECT_INJECTLARVA, hatch)) 654 | else: 655 | if iteration % self.injectInterval == 0 and queen.is_idle and queen.position.distance_to(hatch.position) > 10: 656 | await self.do(queen(AbilityId.MOVE, hatch.position.to2)) 657 | 658 | # clear queen tags (in case queen died or hatch got destroyed) from the dictionary outside the iteration loop 659 | for tag in toRemoveTags: 660 | self.queensAssignedHatcheries.pop(tag) 661 | 662 | async def findCreepPlantLocation(self, targetPositions, castingUnit, minRange=None, maxRange=None, stepSize=1, onlyAttemptPositionsAroundUnit=False, locationAmount=32, dontPlaceTumorsOnExpansions=True): 663 | """function that figures out which positions are valid for a queen or tumor to put a new tumor 664 | 665 | Arguments: 666 | targetPositions {set of Point2} -- For me this parameter is a set of Point2 objects where creep should go towards 667 | castingUnit {Unit} -- The casting unit (queen or tumor) 668 | 669 | Keyword Arguments: 670 | minRange {int} -- Minimum range from the casting unit's location (default: {None}) 671 | maxRange {int} -- Maximum range from the casting unit's location (default: {None}) 672 | onlyAttemptPositionsAroundUnit {bool} -- if True, it will only attempt positions around the unit (ideal for tumor), if False, it will attempt a lot of positions closest from hatcheries (ideal for queens) (default: {False}) 673 | locationAmount {int} -- a factor for the amount of positions that will be attempted (default: {50}) 674 | dontPlaceTumorsOnExpansions {bool} -- if True it will sort out locations that would block expanding there (default: {True}) 675 | 676 | Returns: 677 | list of Point2 -- a list of valid positions to put a tumor on 678 | """ 679 | 680 | assert isinstance(castingUnit, Unit) 681 | positions = [] 682 | ability = self._game_data.abilities[ZERGBUILD_CREEPTUMOR.value] 683 | if minRange is None: minRange = 0 684 | if maxRange is None: maxRange = 500 685 | 686 | # get positions around the casting unit 687 | positions = self.getPositionsAroundUnit(castingUnit, minRange=minRange, maxRange=maxRange, stepSize=stepSize, locationAmount=locationAmount) 688 | 689 | # stop when map is full with creep 690 | if len(self.positionsWithoutCreep) == 0: 691 | return None 692 | 693 | # filter positions that would block expansions 694 | if dontPlaceTumorsOnExpansions and hasattr(self, "exactExpansionLocations"): 695 | positions = [x for x in positions if self.getHighestDistance(x.closest(self.exactExpansionLocations), x) > 3] 696 | # TODO: need to check if this doesnt have to be 6 actually 697 | # this number cant also be too big or else creep tumors wont be placed near mineral fields where they can actually be placed 698 | 699 | # check if any of the positions are valid 700 | validPlacements = await self._client.query_building_placement(ability, positions) 701 | 702 | # filter valid results 703 | validPlacements = [p for index, p in enumerate(positions) if validPlacements[index] == ActionResult.Success] 704 | 705 | allTumors = self.units(CREEPTUMOR) | self.units(CREEPTUMORBURROWED) | self.units(CREEPTUMORQUEEN) 706 | # usedTumors = allTumors.filter(lambda x:x.tag in self.usedCreepTumors) 707 | unusedTumors = allTumors.filter(lambda x:x.tag not in self.usedCreepTumors) 708 | if castingUnit is not None and castingUnit in allTumors: 709 | unusedTumors = unusedTumors.filter(lambda x:x.tag != castingUnit.tag) 710 | 711 | # filter placements that are close to other unused tumors 712 | if len(unusedTumors) > 0: 713 | validPlacements = [x for x in validPlacements if x.distance_to(unusedTumors.closest_to(x)) >= 10] 714 | 715 | validPlacements.sort(key=lambda x: x.distance_to(x.closest(self.positionsWithoutCreep)), reverse=False) 716 | 717 | if len(validPlacements) > 0: 718 | return validPlacements 719 | return None 720 | 721 | def getManhattanDistance(self, unit1, unit2): 722 | assert isinstance(unit1, (Unit, Point2, Point3)) 723 | assert isinstance(unit2, (Unit, Point2, Point3)) 724 | if isinstance(unit1, Unit): 725 | unit1 = unit1.position.to2 726 | if isinstance(unit2, Unit): 727 | unit2 = unit2.position.to2 728 | return abs(unit1.x - unit2.x) + abs(unit1.y - unit2.y) 729 | 730 | def getHighestDistance(self, unit1, unit2): 731 | # returns just the highest distance difference, return max(abs(x2-x1), abs(y2-y1)) 732 | # required for creep tumor placement 733 | assert isinstance(unit1, (Unit, Point2, Point3)) 734 | assert isinstance(unit2, (Unit, Point2, Point3)) 735 | if isinstance(unit1, Unit): 736 | unit1 = unit1.position.to2 737 | if isinstance(unit2, Unit): 738 | unit2 = unit2.position.to2 739 | return max(abs(unit1.x - unit2.x), abs(unit1.y - unit2.y)) 740 | 741 | def getPositionsAroundUnit(self, unit, minRange=0, maxRange=500, stepSize=1, locationAmount=32): 742 | # e.g. locationAmount=4 would only consider 4 points: north, west, east, south 743 | assert isinstance(unit, (Unit, Point2, Point3)) 744 | if isinstance(unit, Unit): 745 | loc = unit.position.to2 746 | else: 747 | loc = unit 748 | positions = [Point2(( \ 749 | loc.x + distance * math.cos(math.pi * 2 * alpha / locationAmount), \ 750 | loc.y + distance * math.sin(math.pi * 2 * alpha / locationAmount))) \ 751 | for alpha in range(locationAmount) # alpha is the angle here, locationAmount is the variable on how accurate the attempts look like a circle (= how many points on a circle) 752 | for distance in range(minRange, maxRange+1)] # distance depending on minrange and maxrange 753 | return positions 754 | 755 | async def updateCreepCoverage(self, stepSize=None): 756 | if stepSize is None: 757 | stepSize = self.creepTargetDistance 758 | ability = self._game_data.abilities[ZERGBUILD_CREEPTUMOR.value] 759 | 760 | positions = [Point2((x, y)) \ 761 | for x in range(self._game_info.playable_area[0]+stepSize, self._game_info.playable_area[0] + self._game_info.playable_area[2]-stepSize, stepSize) \ 762 | for y in range(self._game_info.playable_area[1]+stepSize, self._game_info.playable_area[1] + self._game_info.playable_area[3]-stepSize, stepSize)] 763 | 764 | validPlacements = await self._client.query_building_placement(ability, positions) 765 | successResults = [ 766 | ActionResult.Success, # tumor can be placed there, so there must be creep 767 | ActionResult.CantBuildLocationInvalid, # location is used up by another building or doodad, 768 | ActionResult.CantBuildTooFarFromCreepSource, # - just outside of range of creep 769 | # ActionResult.CantSeeBuildLocation - no vision here 770 | ] 771 | # self.positionsWithCreep = [p for index, p in enumerate(positions) if validPlacements[index] in successResults] 772 | self.positionsWithCreep = [p for valid, p in zip(validPlacements, positions) if valid in successResults] 773 | self.positionsWithoutCreep = [p for index, p in enumerate(positions) if validPlacements[index] not in successResults] 774 | self.positionsWithoutCreep = [p for valid, p in zip(validPlacements, positions) if valid not in successResults] 775 | return self.positionsWithCreep, self.positionsWithoutCreep 776 | 777 | 778 | async def doCreepSpread(self): 779 | # only use queens that are not assigned to do larva injects 780 | allTumors = self.units(CREEPTUMOR) | self.units(CREEPTUMORBURROWED) | self.units(CREEPTUMORQUEEN) 781 | 782 | if not hasattr(self, "usedCreepTumors"): 783 | self.usedCreepTumors = set() 784 | 785 | # gather all queens that are not assigned for injecting and have 25+ energy 786 | if hasattr(self, "queensAssignedHatcheries"): 787 | unassignedQueens = self.units(QUEEN).filter(lambda q: (q.tag not in self.queensAssignedHatcheries and q.energy >= 25 or q.energy >= 50) and (q.is_idle or len(q.orders) == 1 and q.orders[0].ability.id in [AbilityId.MOVE])) 788 | else: 789 | unassignedQueens = self.units(QUEEN).filter(lambda q: q.energy >= 25 and (q.is_idle or len(q.orders) == 1 and q.orders[0].ability.id in [AbilityId.MOVE])) 790 | 791 | # update creep coverage data and points where creep still needs to go 792 | if not hasattr(self, "positionsWithCreep") or self.iteration % self.creepSpreadInterval * 10 == 0: 793 | posWithCreep, posWithoutCreep = await self.updateCreepCoverage() 794 | totalPositions = len(posWithCreep) + len(posWithoutCreep) 795 | self.creepCoverage = len(posWithCreep) / totalPositions 796 | # print(self.getTimeInSeconds(), "creep coverage:", creepCoverage) 797 | 798 | # filter out points that have already tumors / bases near them 799 | if hasattr(self, "positionsWithoutCreep"): 800 | self.positionsWithoutCreep = [x for x in self.positionsWithoutCreep if (allTumors | self.townhalls).closer_than(self.creepTargetCountsAsReachedDistance, x).amount < 1 or (allTumors | self.townhalls).closer_than(self.creepTargetCountsAsReachedDistance + 10, x).amount < 5] # have to set this to some values or creep tumors will clump up in corners trying to get to a point they cant reach 801 | 802 | # make all available queens spread creep until creep coverage is reached 50% 803 | if hasattr(self, "creepCoverage") and (self.creepCoverage < self.stopMakingNewTumorsWhenAtCoverage or allTumors.amount - len(self.usedCreepTumors) < 25): 804 | for queen in unassignedQueens: 805 | # locations = await self.findCreepPlantLocation(self.positionsWithoutCreep, castingUnit=queen, minRange=3, maxRange=30, stepSize=2, locationAmount=16) 806 | if self.townhalls.ready.exists: 807 | locations = await self.findCreepPlantLocation(self.positionsWithoutCreep, castingUnit=queen, minRange=3, maxRange=30, stepSize=2, locationAmount=16) 808 | # locations = await self.findCreepPlantLocation(self.positionsWithoutCreep, castingUnit=self.townhalls.ready.random, minRange=3, maxRange=30, stepSize=2, locationAmount=16) 809 | if locations is not None: 810 | for loc in locations: 811 | err = await self.do(queen(BUILD_CREEPTUMOR_QUEEN, loc)) 812 | if not err: 813 | break 814 | 815 | 816 | unusedTumors = allTumors.filter(lambda x: x.tag not in self.usedCreepTumors) 817 | tumorsMadeTumorPositions = set() 818 | for tumor in unusedTumors: 819 | tumorsCloseToTumor = [x for x in tumorsMadeTumorPositions if tumor.distance_to(Point2(x)) < 8] 820 | if len(tumorsCloseToTumor) > 0: 821 | continue 822 | abilities = await self.get_available_abilities(tumor) 823 | if AbilityId.BUILD_CREEPTUMOR_TUMOR in abilities: 824 | locations = await self.findCreepPlantLocation(self.positionsWithoutCreep, castingUnit=tumor, minRange=10, maxRange=10) # min range could be 9 and maxrange could be 11, but set both to 10 and performance is a little better 825 | if locations is not None: 826 | for loc in locations: 827 | err = await self.do(tumor(BUILD_CREEPTUMOR_TUMOR, loc)) 828 | if not err: 829 | tumorsMadeTumorPositions.add((tumor.position.x, tumor.position.y)) 830 | self.usedCreepTumors.add(tumor.tag) 831 | break 832 | 833 | async def getPathDistance(self, pos1, pos2): 834 | """gets the pathing distance between pos1 and pos2 835 | 836 | Arguments: 837 | pos1 {unit, Point2} -- position 1 838 | pos2 {Point2} -- position 2 839 | 840 | Returns: 841 | int -- distance (i guess the units of the distance is equivalent to the attackrange of in game units) 842 | """ 843 | return await self._client.query_pathing(pos1, pos2) 844 | 845 | async def getClosestByPath(self, units1, unit2): 846 | """ Returns the unit from units1 that is closest by path to unit2 """ 847 | # DOESNT SEEM TO WORK RELIABLY 848 | assert isinstance(unit2, (Unit, Point2)) 849 | if isinstance(units1, (Units, list)): 850 | closest = None 851 | closestDist = 999999999999 852 | for u in units1: 853 | d = await self.getPathDistance(unit2.position.to2, u.position.to2) 854 | print("path distance result:", unit2.position.to2, u.position.to2, d) 855 | if d is not None and d < closestDist: 856 | closest = u 857 | closestDist = d 858 | print("closest by path workingg") 859 | return closest 860 | return None 861 | 862 | ################################ 863 | ######### IMPORTANT DEFAULT FUNCTIONS 864 | ################################ 865 | 866 | async def find_placement(self, building, near, max_distance=20, random_alternative=False, placement_step=3, min_distance=0, minDistanceToResources=3): 867 | """Finds a placement location for building.""" 868 | 869 | assert isinstance(building, (AbilityId, UnitTypeId)) 870 | # assert self.can_afford(building) 871 | assert isinstance(near, Point2) 872 | 873 | if isinstance(building, UnitTypeId): 874 | building = self._game_data.units[building.value].creation_ability 875 | else: # AbilityId 876 | building = self._game_data.abilities[building.value] 877 | 878 | if await self.can_place(building, near): 879 | return near 880 | 881 | for distance in range(min_distance, max_distance, placement_step): 882 | possible_positions = [Point2(p).offset(near).to2 for p in ( 883 | [(dx, -distance) for dx in range(-distance, distance+1, placement_step)] + 884 | [(dx, distance) for dx in range(-distance, distance+1, placement_step)] + 885 | [(-distance, dy) for dy in range(-distance, distance+1, placement_step)] + 886 | [( distance, dy) for dy in range(-distance, distance+1, placement_step)] 887 | )] 888 | if (self.townhalls | self.state.mineral_field | self.state.vespene_geyser).exists and minDistanceToResources > 0: 889 | possible_positions = [x for x in possible_positions if (self.state.mineral_field | self.state.vespene_geyser).closest_to(x).distance_to(x) >= minDistanceToResources] # filter out results that are too close to resources 890 | 891 | res = await self._client.query_building_placement(building, possible_positions) 892 | possible = [p for r, p in zip(res, possible_positions) if r == ActionResult.Success] 893 | if not possible: 894 | continue 895 | 896 | if random_alternative: 897 | return random.choice(possible) 898 | else: 899 | return min(possible, key=lambda p: p.distance_to(near)) 900 | return None 901 | 902 | async def distribute_workers(self, performanceHeavy=False, onlySaturateGas=False): 903 | expansion_locations = self.expansion_locations 904 | owned_expansions = self.owned_expansions 905 | 906 | 907 | mineralTags = [x.tag for x in self.state.units.mineral_field] 908 | # gasTags = [x.tag for x in self.state.units.vespene_geyser] 909 | geyserTags = [x.tag for x in self.geysers] 910 | 911 | workerPool = self.units & [] 912 | workerPoolTags = set() 913 | 914 | # find all geysers that have surplus or deficit 915 | deficitGeysers = {} 916 | surplusGeysers = {} 917 | for g in self.geysers.filter(lambda x:x.vespene_contents > 0): 918 | # only loop over geysers that have still gas in them 919 | deficit = g.ideal_harvesters - g.assigned_harvesters 920 | if deficit > 0: 921 | deficitGeysers[g.tag] = {"unit": g, "deficit": deficit} 922 | elif deficit < 0: 923 | surplusWorkers = self.workers.closer_than(10, g).filter(lambda w:w not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in geyserTags) 924 | # workerPool.extend(surplusWorkers) 925 | for i in range(-deficit): 926 | if surplusWorkers.amount > 0: 927 | w = surplusWorkers.pop() 928 | workerPool.append(w) 929 | workerPoolTags.add(w.tag) 930 | surplusGeysers[g.tag] = {"unit": g, "deficit": deficit} 931 | 932 | if not onlySaturateGas: 933 | # find all townhalls that have surplus or deficit 934 | deficitTownhalls = {} 935 | surplusTownhalls = {} 936 | for th in self.townhalls: 937 | deficit = th.ideal_harvesters - th.assigned_harvesters 938 | if deficit > 0: 939 | deficitTownhalls[th.tag] = {"unit": th, "deficit": deficit} 940 | elif deficit < 0: 941 | surplusWorkers = self.workers.closer_than(10, th).filter(lambda w:w.tag not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in mineralTags) 942 | # workerPool.extend(surplusWorkers) 943 | for i in range(-deficit): 944 | if surplusWorkers.amount > 0: 945 | w = surplusWorkers.pop() 946 | workerPool.append(w) 947 | workerPoolTags.add(w.tag) 948 | surplusTownhalls[th.tag] = {"unit": th, "deficit": deficit} 949 | 950 | if all([len(deficitGeysers) == 0, len(surplusGeysers) == 0, len(surplusTownhalls) == 0 or deficitTownhalls == 0]): 951 | # cancel early if there is nothing to balance 952 | return 953 | 954 | # check if deficit in gas less or equal than what we have in surplus, else grab some more workers from surplus bases 955 | deficitGasCount = sum(gasInfo["deficit"] for gasTag, gasInfo in deficitGeysers.items() if gasInfo["deficit"] > 0) 956 | surplusCount = sum(-gasInfo["deficit"] for gasTag, gasInfo in surplusGeysers.items() if gasInfo["deficit"] < 0) 957 | surplusCount += sum(-thInfo["deficit"] for thTag, thInfo in surplusTownhalls.items() if thInfo["deficit"] < 0) 958 | 959 | if deficitGasCount - surplusCount > 0: 960 | # grab workers near the gas who are mining minerals 961 | for gTag, gInfo in deficitGeysers.items(): 962 | if workerPool.amount >= deficitGasCount: 963 | break 964 | workersNearGas = self.workers.closer_than(10, gInfo["unit"]).filter(lambda w:w.tag not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in mineralTags) 965 | while workersNearGas.amount > 0 and workerPool.amount < deficitGasCount: 966 | w = workersNearGas.pop() 967 | workerPool.append(w) 968 | workerPoolTags.add(w.tag) 969 | 970 | # now we should have enough workers in the pool to saturate all gases, and if there are workers left over, make them mine at townhalls that have mineral workers deficit 971 | for gTag, gInfo in deficitGeysers.items(): 972 | if performanceHeavy: 973 | # sort furthest away to closest (as the pop() function will take the last element) 974 | workerPool.sort(key=lambda x:x.distance_to(gInfo["unit"]), reverse=True) 975 | for i in range(gInfo["deficit"]): 976 | if workerPool.amount > 0: 977 | w = workerPool.pop() 978 | if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: 979 | await self.do(w.gather(gInfo["unit"], queue=True)) 980 | else: 981 | await self.do(w.gather(gInfo["unit"])) 982 | 983 | if not onlySaturateGas: 984 | # if we now have left over workers, make them mine at bases with deficit in mineral workers 985 | for thTag, thInfo in deficitTownhalls.items(): 986 | if performanceHeavy: 987 | # sort furthest away to closest (as the pop() function will take the last element) 988 | workerPool.sort(key=lambda x:x.distance_to(thInfo["unit"]), reverse=True) 989 | for i in range(thInfo["deficit"]): 990 | if workerPool.amount > 0: 991 | w = workerPool.pop() 992 | mf = self.state.mineral_field.closer_than(10, thInfo["unit"]).closest_to(w) 993 | if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: 994 | await self.do(w.gather(mf, queue=True)) 995 | else: 996 | await self.do(w.gather(mf)) 997 | 998 | # TODO: check if a drone is mining from a destroyed base (= if nearest townhalf from the GATHER target is >10 away) -> make it mine at another mineral patch 999 | 1000 | def select_build_worker(self, pos, force=False, excludeTags=[]): 1001 | workers = self.workers.closer_than(50, pos) or self.workers 1002 | for worker in workers.prefer_close_to(pos).prefer_idle: 1003 | if not worker.orders or len(worker.orders) == 1 and worker.orders[0].ability.id in [AbilityId.MOVE, AbilityId.HARVEST_GATHER, AbilityId.HARVEST_RETURN] and worker.tag not in excludeTags: 1004 | return worker 1005 | return workers.random if force else None 1006 | 1007 | def already_pending(self, unit_type): 1008 | ability = self._game_data.units[unit_type.value].creation_ability 1009 | unitAttributes = self._game_data.units[unit_type.value].attributes 1010 | 1011 | # # the following checks for construction of buildings, i think 8 in unitAttributes stands for "structure" tag 1012 | # # i commented the following out because i think that is not what is meant with "already pending", but rather having a worker queued up to place a building, or having units in production queue 1013 | # if self.units(unit_type).not_ready.exists and 8 in unitAttributes: 1014 | # return len(self.units(unit_type).not_ready) 1015 | # the following checks for units being made from eggs and trained units in general 1016 | if 8 not in unitAttributes and any(o.ability == ability for w in (self.units.not_structure) for o in w.orders): 1017 | return sum([o.ability == ability for w in (self.units - self.workers) for o in w.orders]) 1018 | # following checks for unit production in a building queue, like queen, also checks if hatch is morphing to LAIR 1019 | elif any(o.ability.id == ability.id for w in (self.units.structure) for o in w.orders): 1020 | return sum([o.ability.id == ability.id for w in (self.units.structure) for o in w.orders]) 1021 | elif any(o.ability == ability for w in self.workers for o in w.orders): 1022 | return sum([o.ability == ability for w in self.workers for o in w.orders]) 1023 | elif any(egg.orders[0].ability == ability for egg in self.units(EGG)): 1024 | return sum([egg.orders[0].ability == ability for egg in self.units(EGG)]) 1025 | return 0 1026 | 1027 | async def on_step(self, iteration): 1028 | """ this was my first goal when starting to write this bot 1029 | ✓ create overlords based on: how much larva is produced in 18 seconds (overlord build time) - based on number of hatcheries and inject queens 1030 | ✓ if we have 2 or more hatcheries building (pending), start making spawning pool 1031 | ✓ once pool is building / pending, get the first gas 1032 | ✓ when at 100 gas, get ling speed 1033 | ✓ if we have 50 or more drones building (pending), start roach warren and take more gas 1034 | ✓ once at 4:30 time, get lair 1035 | ✓ at 70 or more drones, start making roaches 1036 | ✓ make as many extractors to have a proper 4:1 or 3:1 income ratio (minerals:gas) 1037 | ✓ once at 120+ supply, make 2 evo chambers and get range + armor upgrade 1038 | ✓ if lair complete, get roach speed 1039 | ✓ once at 180+ supply, attack with roaches 1040 | ✓ when pool is done, make queens (one per hatch) and assign queens to hatcheries (inject) 1041 | """ 1042 | 1043 | self.workerProductionEnabled = [True] 1044 | self.queenProductionEnabled = [True] 1045 | self.armyProductionEnabled = [True] 1046 | self.iteration = iteration 1047 | if not self.prepareStart2Ran: 1048 | await self._prepare_start2() 1049 | self.currentDroneCountIncludingPending = self.units(DRONE).amount + self.already_pending(DRONE) + self.units(EXTRACTOR).ready.filter(lambda x:x.vespene_contents > 0).amount 1050 | if self.units(GREATERSPIRE).exists: 1051 | self.totalQueenLimit = self.totalLateGameQueenLimit 1052 | else: 1053 | self.totalQueenLimit = self.totalEarlyGameQueenLimit 1054 | 1055 | ################################ 1056 | ######### MACRO (ECONOMY) 1057 | ################################ 1058 | 1059 | if iteration % self.buildStuffInverval == 0: 1060 | # calc larva production 1061 | larvaPerSecond = self.townhalls.ready.amount * self.larvaPerHatch + self.units(QUEEN).ready.amount * self.larvaPerInject 1062 | larvaPer18Seconds = larvaPerSecond * 18 # overlord build time is 18 secs 1063 | 1064 | # create overlords if low on supply 1065 | # print(self.supply_left + self.already_pending(OVERLORD) * 8, larvaPer18Seconds) 1066 | if self.supply_left + self.already_pending(OVERLORD) * 8 < 2 * larvaPer18Seconds and self.supply_cap + self.already_pending(OVERLORD) * 8 < 200 and self.supply_used >= 13 or self.supply_cap < 14: 1067 | self.workerProductionEnabled.append(False) 1068 | self.armyProductionEnabled.append(False) 1069 | if self.can_afford(OVERLORD) and self.units(LARVA).exists: 1070 | await self.do(self.units(LARVA).random.train(OVERLORD)) 1071 | 1072 | # take one extractor if we have 2+ hatcheries 1073 | if self.townhalls.amount >= 2 and self.units(EXTRACTOR).amount + self.already_pending(EXTRACTOR) < 1 and (self.getLingSpeed or (self.currentDroneCountIncludingPending > 25 and self.units(SPAWNINGPOOL).exists)): 1074 | vgs = self.state.vespene_geyser.closer_than(10, self.townhalls.ready.random) 1075 | for vg in vgs: 1076 | worker = self.select_build_worker(vg.position) 1077 | if worker is None: break 1078 | if self.can_afford(EXTRACTOR) and await self.can_place(EXTRACTOR, vg.position): 1079 | err = await self.do(worker.build(EXTRACTOR, vg)) 1080 | if not err: break 1081 | 1082 | # townhall furthest away from enemy base - that is where i will make all the tech buildings 1083 | townhallLocationFurthestFromOpponent = None 1084 | if self.townhalls.ready.exists and self.known_enemy_structures.exists: 1085 | townhallLocationFurthestFromOpponent = max([x.position.to2 for x in self.townhalls.ready], key=lambda x: x.closest(self.known_enemy_structures).distance_to(x)) 1086 | if townhallLocationFurthestFromOpponent is None and self.townhalls.ready.exists: 1087 | townhallLocationFurthestFromOpponent = self.townhalls.ready.random.position.to2 1088 | 1089 | # create spawning pool if we have 2+ hatches 1090 | if self.currentDroneCountIncludingPending > 17 and not self.units(SPAWNINGPOOL).exists and self.already_pending(SPAWNINGPOOL) < 1 and self.townhalls.amount >= 2: 1091 | self.workerProductionEnabled.append(False) 1092 | if self.can_afford(SPAWNINGPOOL): 1093 | pos = await self.find_placement(SPAWNINGPOOL, townhallLocationFurthestFromOpponent, min_distance=6) 1094 | # pos = await self.find_placement(SPAWNINGPOOL, self.townhalls.ready.random.position.to2, min_distance=6) 1095 | if pos is not None: 1096 | drone = self.workers.closest_to(pos) 1097 | if self.can_afford(SPAWNINGPOOL): 1098 | err = await self.do(drone.build(SPAWNINGPOOL, pos)) 1099 | 1100 | # if pool is done, research ling speed if we have the money 1101 | if self.getLingSpeed: # parameter, see at __init__ function 1102 | if self.units(SPAWNINGPOOL).ready.exists: 1103 | pool = self.units(SPAWNINGPOOL).ready.first 1104 | abilities = await self.get_available_abilities(pool) 1105 | if AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST in abilities and self.can_afford(AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST): 1106 | error = await self.do(pool(AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST)) 1107 | 1108 | # if pool is done, make queens 1109 | # this is at the end of the script 1110 | 1111 | # only assign queens once the first creep tumors are placed (those are more important than inject at the start afaik) 1112 | if ((self.units(CREEPTUMOR) | self.units(CREEPTUMORBURROWED) | self.units(CREEPTUMORQUEEN)).amount >= 2 or not self.enableCreepSpread) and self.enableInjects: 1113 | if self.units(GREATERSPIRE).exists: 1114 | # unassign queens when we have enough hatcheries in late game when we only build expensive units that dont require much larva 1115 | self.assignQueen(0) 1116 | else: 1117 | self.assignQueen(self.injectQueenLimit) 1118 | # perform injects (if they have enough energy) 1119 | await self.doQueenInjects(iteration) 1120 | 1121 | # make roach warren when at 43+ drones 1122 | if (self.currentDroneCountIncludingPending >= 40 or self.opponentInfo["armySupplyScouted"] > 5) and self.units(ROACHWARREN).amount + self.already_pending(ROACHWARREN) < 1 and self.townhalls.exists and self.enableMakingRoaches: 1123 | self.workerProductionEnabled.append(False) # priotize 1124 | if self.can_afford(ROACHWARREN): 1125 | pos = await self.find_placement(ROACHWARREN, townhallLocationFurthestFromOpponent, min_distance=6) 1126 | # pos = await self.find_placement(ROACHWARREN, self.townhalls.ready.random.position.to2, min_distance=6) 1127 | if pos is not None: 1128 | drone = self.workers.closest_to(pos) 1129 | if self.can_afford(ROACHWARREN): 1130 | err = await self.do(drone.build(ROACHWARREN, pos)) 1131 | 1132 | # make 2 evo chambers when at 35+ drones 1133 | if self.currentDroneCountIncludingPending >= 37 and self.units(EVOLUTIONCHAMBER).amount + self.already_pending(EVOLUTIONCHAMBER) < 2 and self.townhalls.amount > 2: 1134 | # self.workerProductionEnabled.append(False) # priotize? 1135 | if self.can_afford(EVOLUTIONCHAMBER): 1136 | pos = await self.find_placement(EVOLUTIONCHAMBER, townhallLocationFurthestFromOpponent, min_distance=6) 1137 | # pos = await self.find_placement(EVOLUTIONCHAMBER, self.townhalls.ready.random.position.to2, min_distance=6) 1138 | if pos is not None: 1139 | drone = self.workers.closest_to(pos) 1140 | if self.can_afford(EVOLUTIONCHAMBER): 1141 | err = await self.do(drone.build(EVOLUTIONCHAMBER, pos)) 1142 | 1143 | # take more extractors if we have 66+ drones 1144 | if self.currentDroneCountIncludingPending >= 66 and self.units(EXTRACTOR).filter(lambda x:x.vespene_contents > 0).amount + self.already_pending(EXTRACTOR) < 9 and self.already_pending(EXTRACTOR) < 4 and self.townhalls.amount > 2: 1145 | self.workerProductionEnabled.append(False) 1146 | vgs = self.state.vespene_geyser.closer_than(10, self.townhalls.filter(lambda x: x.build_progress > 0.6).random) 1147 | for vg in vgs: 1148 | worker = self.select_build_worker(vg.position) 1149 | if worker is None: break 1150 | if self.can_afford(EXTRACTOR) and await self.can_place(EXTRACTOR, vg.position): 1151 | err = await self.do(worker.build(EXTRACTOR, vg)) 1152 | if not err: break 1153 | 1154 | # after 4:30 start lair 1155 | if self.getTimeInSeconds() > 4.5 * 60 and self.currentDroneCountIncludingPending > 50: 1156 | if self.already_pending(LAIR) < 1 and self.units(HATCHERY).ready.idle.exists and self.units(SPAWNINGPOOL).ready.exists and not (self.units(LAIR) | self.units(HIVE)).exists and self.vespene >= 100: 1157 | self.queenProductionEnabled.append(False) 1158 | self.armyProductionEnabled.append(False) 1159 | self.workerProductionEnabled.append(False) 1160 | if self.can_afford(LAIR): 1161 | err = await self.do(self.units(HATCHERY).ready.idle.random(UPGRADETOLAIR_LAIR)) 1162 | 1163 | # make infestation pit when above 160 supply 1164 | if (self.supply_used > 130 or not self.enableMakingRoaches) and self.units(LAIR).ready.amount > 0 and self.units(INFESTATIONPIT).amount + self.already_pending(INFESTATIONPIT) < 1 and self.townhalls.amount > 3: 1165 | self.armyProductionEnabled.append(False) 1166 | self.workerProductionEnabled.append(False) 1167 | pos = await self.find_placement(INFESTATIONPIT, townhallLocationFurthestFromOpponent, min_distance=6) 1168 | # pos = await self.find_placement(INFESTATIONPIT, self.townhalls.ready.random.position.to2, min_distance=6) 1169 | if pos is not None: 1170 | drone = self.workers.closest_to(pos) 1171 | if self.can_afford(INFESTATIONPIT): 1172 | err = await self.do(drone.build(INFESTATIONPIT, pos)) 1173 | 1174 | # morph to hive 1175 | if self.supply_used > 150 and self.units(LAIR).ready.idle.exists and self.units(INFESTATIONPIT).ready.exists and not self.already_pending(HIVE) and not self.units(HIVE).exists and self.townhalls.amount > 3: 1176 | self.armyProductionEnabled.append(False) 1177 | self.workerProductionEnabled.append(False) 1178 | if self.can_afford(HIVE): 1179 | err = await self.do(self.units(LAIR).ready.idle.first(UPGRADETOHIVE_HIVE)) 1180 | 1181 | # if lategame enabled: get spire (and then greater spire) 1182 | if self.supply_used > 80 and (self.already_pending(HIVE) or self.units(HIVE).exists) and self.units(GREATERSPIRE).amount + self.already_pending(GREATERSPIRE) < 1 and self.enableLateGame: 1183 | if not self.units(GREATERSPIRE).exists and self.units(SPIRE).ready.idle.exists and self.already_pending(GREATERSPIRE) < 1: 1184 | self.armyProductionEnabled.append(False) 1185 | self.workerProductionEnabled.append(False) 1186 | # morph spire to greater spire 1187 | if self.can_afford(GREATERSPIRE): 1188 | err = await self.do(self.units(SPIRE).ready.idle.random(UPGRADETOGREATERSPIRE_GREATERSPIRE)) 1189 | 1190 | # build spire if we dont have spire or greater spire 1191 | elif not self.units(GREATERSPIRE).exists and not self.units(SPIRE).exists and self.already_pending(SPIRE) < 1: 1192 | self.armyProductionEnabled.append(False) 1193 | self.workerProductionEnabled.append(False) 1194 | if self.can_afford(SPIRE): 1195 | pos = await self.find_placement(SPIRE, townhallLocationFurthestFromOpponent, min_distance=6, minDistanceToResources=2) 1196 | # pos = await self.find_placement(SPIRE, self.townhalls.ready.random.position.to2, min_distance=6, minDistanceToResources=2) 1197 | if pos is not None: 1198 | drone = self.workers.closest_to(pos) 1199 | if self.can_afford(SPIRE): 1200 | err = await self.do(drone.build(SPIRE, pos)) 1201 | 1202 | ################################ 1203 | ######### MACRO (UPGRADES) 1204 | ################################ 1205 | 1206 | # when lair is done, get roach speed 1207 | if self.units(ROACHWARREN).ready.amount > 0 and (self.units(LAIR).ready.amount > 0 or self.units(HIVE).amount > 0) and self.enableMakingRoaches: 1208 | if self.units(ROACHWARREN).ready.idle.amount > 0: 1209 | rw = self.units(ROACHWARREN).ready.idle.random 1210 | abilities = await self.get_available_abilities(rw) 1211 | if AbilityId.RESEARCH_GLIALREGENERATION in abilities and self.can_afford(AbilityId.RESEARCH_GLIALREGENERATION): 1212 | error = await self.do(rw(AbilityId.RESEARCH_GLIALREGENERATION)) 1213 | 1214 | # research roach burrow movement 1215 | elif self.getRoachBurrowAndBurrow and AbilityId.RESEARCH_TUNNELINGCLAWS in abilities and self.can_afford(AbilityId.RESEARCH_TUNNELINGCLAWS): 1216 | error = await self.do(rw(AbilityId.RESEARCH_TUNNELINGCLAWS)) 1217 | 1218 | # research burrow from hatch, getting all abilities from hatch is still buggy 1219 | rw = self.units(ROACHWARREN).ready.random 1220 | abilities = await self.get_available_abilities(rw) 1221 | if self.getRoachBurrowAndBurrow and AbilityId.RESEARCH_TUNNELINGCLAWS not in abilities and self.can_afford(AbilityId.RESEARCH_TUNNELINGCLAWS) and self.can_afford(AbilityId.RESEARCH_BURROW): 1222 | if self.iteration % self.creepSpreadInterval == 0: 1223 | drone = self.units(DRONE).random 1224 | if drone is not None: 1225 | droneAbilities = await self.get_available_abilities(drone) 1226 | if AbilityId.BURROWDOWN_DRONE not in droneAbilities and self.can_afford(AbilityId.RESEARCH_BURROW): 1227 | if self.units(HATCHERY).ready.idle.exists: 1228 | hatch = self.units(HATCHERY).ready.idle.random 1229 | error = await self.do(hatch(AbilityId.RESEARCH_BURROW)) 1230 | 1231 | # research overlord speed from hatchery if we have a big bank 1232 | if self.minerals > 1000 and self.vespene > 1000 and self.can_afford(AbilityId.RESEARCH_PNEUMATIZEDCARAPACE) and self.units(HATCHERY).idle.exists: 1233 | hatch = self.units(HATCHERY).idle.random 1234 | if self.can_afford(AbilityId.RESEARCH_PNEUMATIZEDCARAPACE): 1235 | await self.do(hatch(RESEARCH_PNEUMATIZEDCARAPACE)) 1236 | 1237 | # get roach upgrades 1-1 2-2 3-3 1238 | if self.units(EVOLUTIONCHAMBER).ready.idle.exists: 1239 | for evo in self.units(EVOLUTIONCHAMBER).ready.idle: 1240 | abilities = await self.get_available_abilities(evo) 1241 | targetAbilities = [AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3] 1242 | if self.units(GREATERSPIRE).exists: 1243 | targetAbilities.extend([AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1, 1244 | AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2, 1245 | AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3]) 1246 | for ability in targetAbilities: 1247 | if ability in abilities: 1248 | if self.can_afford(ability): 1249 | err = await self.do(evo(ability)) 1250 | if not err: 1251 | break 1252 | 1253 | # if lategame enabled: get air upgrades if we have idle greater spire 1254 | if self.enableLateGame and self.units(GREATERSPIRE).ready.idle.exists: # and self.can_afford(RESEARCH_ZERGFLYERATTACKLEVEL3): 1255 | gs = self.units(GREATERSPIRE).ready.idle.random 1256 | abilities = await self.get_available_abilities(gs) 1257 | targetAbilities = [AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1, 1258 | AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2, 1259 | AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3, 1260 | AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1, 1261 | AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2, 1262 | AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3] 1263 | for ability in targetAbilities: 1264 | if self.can_afford(ability) and ability in abilities: 1265 | err = await self.do(gs(ability)) 1266 | if not err: 1267 | break 1268 | 1269 | ################################ 1270 | ######### MACRO (EXPANDING) 1271 | ################################ 1272 | 1273 | # boolean, expand when game time > 10 mins and we have less than 30 mineral fields 1274 | # or if we have DRONES > (TOWNHALLS - 1) * 16 1275 | inNeedOfExpansion = self.getTimeInSeconds() > 10*60 and sum([self.state.mineral_field.closer_than(10, x).amount for x in self.townhalls]) < 32 or self.currentDroneCountIncludingPending + 16 > (self.townhalls.amount) * 16 + self.units(EXTRACTOR).ready.filter(lambda x:x.vespene_contents > 0).amount * 3 1276 | 1277 | if self.townhalls.amount > 1: 1278 | self.haltRedistributeWorkers = False 1279 | # send worker to the first expansion to build it asap 1280 | if self.townhalls.amount < 2 and (self.currentDroneCountIncludingPending > 17 or (self.can_afford(HATCHERY) and self.units(DRONE).amount > 5)) and self.nextExpansionInfo == {} and self.already_pending(HATCHERY) < 1: 1281 | location = await self.get_next_expansion() 1282 | w = self.select_build_worker(location, excludeTags=self.reservedWorkers) 1283 | loc = await self.find_placement(HATCHERY, near=location, random_alternative=False, placement_step=1, minDistanceToResources=5, max_distance=20) 1284 | if loc is not None: 1285 | location = loc 1286 | self.reservedWorkers.append(w.tag) 1287 | self.nextExpansionInfo = { 1288 | "workerTag": w.tag, 1289 | "location": location 1290 | } 1291 | print(self.getTimeInSeconds(), "moving worker to expand", w.tag) 1292 | self.haltRedistributeWorkers = True 1293 | self.workerProductionEnabled.append(False) 1294 | await self.do(w.move(location)) 1295 | 1296 | elif self.townhalls.amount < 2 and self.nextExpansionInfo != {} and self.already_pending(HATCHERY) < 1: 1297 | self.haltRedistributeWorkers = True 1298 | self.workerProductionEnabled.append(False) 1299 | if self.can_afford(HATCHERY): 1300 | w = self.workers.find_by_tag(self.nextExpansionInfo["workerTag"]) 1301 | if w is not None: 1302 | location = self.nextExpansionInfo["location"] 1303 | print(self.getTimeInSeconds(), "building first expansion with worker", w.tag, self.nextExpansionInfo["workerTag"]) 1304 | err = await self.do(w.build(HATCHERY, location)) 1305 | if not err: 1306 | self.nextExpansionInfo = {} 1307 | 1308 | # expand if money available, only take one expansion at a time (queued by worker) 1309 | # dont OVERexpand, only expand if we have less than X mineral fields near our base 1310 | elif (self.currentDroneCountIncludingPending > self.townhalls.amount * 16 or (self.can_afford(HATCHERY) and self.townhalls.amount < 3 or self.minerals > 2000)) and self.townhalls.amount > 1 and inNeedOfExpansion and self.already_pending(HATCHERY) < 1: 1311 | self.workerProductionEnabled.append(False) 1312 | if self.can_afford(HATCHERY): 1313 | location = await self.get_next_expansion() 1314 | location = await self.find_placement(HATCHERY, near=location, random_alternative=False, placement_step=1, minDistanceToResources=5) 1315 | if location is not None: 1316 | w = self.select_build_worker(location, excludeTags=self.reservedWorkers) 1317 | if w is not None: 1318 | err = await self.build(HATCHERY, location, max_distance=20, unit=w, random_alternative=False, placement_step=1) 1319 | 1320 | # set rally point of hatcheries to nearby mineral field 1321 | if iteration % self.injectInterval == 0: 1322 | await self.assignWorkerRallyPoint() 1323 | 1324 | ################################ 1325 | ######### MACRO (worker distribution) 1326 | ################################ 1327 | 1328 | if not self.haltRedistributeWorkers: 1329 | # make idle workers mine at the nearest hatchery 1330 | for worker in self.workers.idle: 1331 | closestHatchery = self.townhalls.ready.closest_to(worker.position) 1332 | await self.do(worker.gather(self.state.mineral_field.closest_to(closestHatchery))) 1333 | # every few frames, redistribute workers equally 1334 | if iteration % self.workerTransferInterval * 5 == 0: 1335 | # redistribute workers (alternatively: set rally points) 1336 | await self.distribute_workers() 1337 | elif iteration % self.workerTransferInterval == 0: 1338 | # redistribute workers (alternatively: set rally points) 1339 | await self.distribute_workers(onlySaturateGas=True) 1340 | 1341 | ################################ 1342 | ######### CREEPSPREAD 1343 | ################################ 1344 | # manage creep spread 1345 | 1346 | if self.enableCreepSpread and iteration % self.creepSpreadInterval == 0 and (self.getTimeInSeconds() > 3 * 60 or self.opponentInfo["armySupplyScouted"] < 5 or (self.units(CREEPTUMOR) | self.units(CREEPTUMORBURROWED) | self.units(CREEPTUMORQUEEN)).amount < 2): 1347 | await self.doCreepSpread() 1348 | 1349 | ################################ 1350 | ######### MACRO (ARMY) 1351 | ################################ 1352 | 1353 | if iteration % self.buildStuffInverval == 0: 1354 | # make scouting units 1355 | if not hasattr(self, "scoutingUnits"): 1356 | self.scoutingUnits = set() 1357 | if self.units(ZERGLING).amount + self.already_pending(ZERGLING) + len(self.scoutingUnits) < self.opponentInfo["scoutingUnitsNeeded"] and self.supply_left > 1 and self.units(SPAWNINGPOOL).ready.exists and self.units(LARVA).exists and self.supply_used < 198: 1358 | # self.workerProductionEnabled.append(False) # this doesnt seem to be very necessary 1359 | if self.can_afford(ZERGLING): 1360 | await self.do(self.units(LARVA).random.train(ZERGLING)) 1361 | # add lings to scouting units 1362 | for ling in self.units(ZERGLING).filter(lambda x:x.health >= 20): 1363 | if len(self.scoutingUnits) < self.opponentInfo["scoutingUnitsNeeded"] and ling.tag not in self.scoutingUnits: 1364 | print("added ling to scouting units group") 1365 | self.scoutingUnits.add(ling.tag) 1366 | # # clear dead lings from scouting units list - i dont want that hear because i have it again a few lines below 1367 | 1368 | # make 1 roach for every 2 enemy unit supply 1369 | makeDefensiveRoaches = self.opponentInfo["armySupplyScoutedClose"] // 1 > self.units.not_structure.filter(lambda x:x.type_id not in [DRONE, LARVA, OVERLORD, ZERGLING]).amount + self.already_pending(ROACH) + self.already_pending(QUEEN) + self.units(SPINECRAWLER).not_ready.amount and self.getTimeInSeconds() < 6*60 and self.vespene > 25 1370 | if makeDefensiveRoaches: 1371 | # stop worker production for a while to build a defense since an attack is likely to come 1372 | self.workerProductionEnabled.append(False) 1373 | 1374 | # if roach warren is ready, start making roaches 1375 | if self.units(ROACHWARREN).ready.exists and \ 1376 | all(self.armyProductionEnabled) and \ 1377 | (not self.units(GREATERSPIRE).exists or self.minerals > self.vespene * 2 > 1000) and \ 1378 | ((self.minerals > 400 and self.vespene > 400 or not self.units(EVOLUTIONCHAMBER).idle.exists and self.currentDroneCountIncludingPending >= self.droneLimit) or makeDefensiveRoaches) and self.supply_used < 197: 1379 | if self.supply_left > 1 and self.can_afford(ROACH): 1380 | roachesAffordable = min((196 - self.supply_used)//2, self.supply_left // 2, self.minerals // 75, self.vespene // 25, self.units(LARVA).amount) 1381 | for count, larva in enumerate(self.units(LARVA)): 1382 | if count >= roachesAffordable: 1383 | break 1384 | if self.can_afford(ROACH): 1385 | await self.do(larva.train(ROACH)) 1386 | 1387 | # if we have lair, make overseer 1388 | if all(self.armyProductionEnabled) and (self.units(LAIR) | self.units(HIVE)).exists and (self.units(OVERSEER) | self.units(OVERLORDCOCOON)).amount < 3 and hasattr(self, "overlordsSendlocation") and (self.minerals > 500 and self.vespene > 500 or self.supply_used > 150): 1389 | if self.units(OVERLORD).exists and self.can_afford(OVERSEER): 1390 | assignedOverlordTags = set().union(*list(self.overlordsSendlocation.values())) 1391 | availableOverlords = self.units(OVERLORD).filter(lambda x:x.tag not in assignedOverlordTags) 1392 | if availableOverlords.exists and self.can_afford(OVERSEER): 1393 | ov = availableOverlords.random 1394 | await self.do(ov(MORPH_OVERSEER)) 1395 | 1396 | # if greater spire is ready, make units in a ratio 1397 | # bl - corruptor - viper 1398 | # 2 - 3 - 1 1399 | corruptorsTotal = self.units(CORRUPTOR).amount + self.already_pending(CORRUPTOR) 1400 | broodlordsTotal = self.units(BROODLORD).amount + self.units(BROODLORDCOCOON).amount 1401 | vipersTotal = self.units(VIPER).amount + self.already_pending(VIPER) 1402 | makeCorruptors = self.units(CORRUPTOR).amount < 1 \ 1403 | or corruptorsTotal / self.corruptorRatioFactor < broodlordsTotal / self.broodlordRatioFactor + 1 \ 1404 | and corruptorsTotal / self.corruptorRatioFactor < vipersTotal / self.viperRatioFactor + 1 # or corruptorsTotal < 15 1405 | makeBroodlords = corruptorsTotal / self.corruptorRatioFactor > broodlordsTotal / self.broodlordRatioFactor and self.units(CORRUPTOR).amount > 1 1406 | makeVipers = corruptorsTotal / self.corruptorRatioFactor > vipersTotal / self.viperRatioFactor 1407 | 1408 | if all(self.armyProductionEnabled) and self.units(GREATERSPIRE).exists and self.can_afford(CORRUPTOR) and self.supply_left > 1 and makeCorruptors: # leave a bit supply for viper and BL morphs 1409 | corruptorAffordable = min(self.supply_left // 2, self.minerals // 150, self.vespene // 100, self.units(LARVA).amount) 1410 | for count, larva in enumerate(self.units(LARVA)): 1411 | if count >= corruptorAffordable: 1412 | break 1413 | if self.can_afford(CORRUPTOR): 1414 | await self.do(larva.train(CORRUPTOR)) 1415 | 1416 | elif all(self.armyProductionEnabled) and self.units(GREATERSPIRE).exists and self.units(CORRUPTOR).idle.exists and self.supply_left > 1 and makeBroodlords: 1417 | if self.can_afford(BROODLORD): 1418 | lowestHpIdleCorruptor = min(self.units(CORRUPTOR), key=lambda x:x.health) 1419 | await self.do(lowestHpIdleCorruptor(MORPHTOBROODLORD_BROODLORD)) 1420 | 1421 | elif all(self.armyProductionEnabled) and self.units(GREATERSPIRE).exists and self.can_afford(VIPER) and self.supply_left > 5 and makeVipers: # leave a bit supply for viper and BL morphs 1422 | if self.can_afford(VIPER) and self.units(LARVA).exists: 1423 | await self.do(self.units(LARVA).random.train(VIPER)) 1424 | 1425 | ################################ 1426 | ######### INTEL / INFO / SCOUTING 1427 | ################################ 1428 | 1429 | if iteration % self.workerTransferInterval == 0: 1430 | # overlord scouting, send overlord to each expansion that isnt taken by opponent 1431 | if not hasattr(self, "overlordsSendlocation"): 1432 | self.overlordsSendlocation = OrderedDict({base.position.to2: set() for base in self.exactExpansionLocations}) 1433 | self.overlordsSendlocation = OrderedDict(sorted(self.overlordsSendlocation.items(), key=lambda x: x[0].closest(self.enemy_start_locations).distance_to(x[0]))) 1434 | 1435 | # else: 1436 | # # clear dead overlords and send new ones - DONT DO THAT because the overlord already got killed, why suicide another one? 1437 | # aliveOverlordTags = {x.tag for x in self.units(OVERLORD)} 1438 | # for key in self.overlordsSendlocation.keys(): 1439 | # self.overlordsSendlocation[key] = {x for x in self.overlordsSendlocation[key] if x in aliveOverlordTags} 1440 | 1441 | # send new overlords if we dont have an overlord at the location and if the location is not taken by either player 1442 | assignedOverlordTags = set().union(*list(self.overlordsSendlocation.values())) 1443 | unassignedOverlords = self.units(OVERLORD).filter(lambda x:x.tag not in assignedOverlordTags) 1444 | 1445 | myBasesAndEnemyBases = [x.position.to2 for x in self.townhalls | self.known_enemy_structures.filter(lambda x:x.type_id in [HATCHERY, LAIR, HIVE, COMMANDCENTER, PLANETARYFORTRESS, ORBITALCOMMAND, NEXUS] and x.build_progress > 0.5)] 1446 | if len(myBasesAndEnemyBases) > 0: 1447 | for key, value in self.overlordsSendlocation.items(): 1448 | closestBaseNearTargetLocation = key.closest(myBasesAndEnemyBases) 1449 | # move new overlord to target location to scout 1450 | if unassignedOverlords.exists and len(value) == 0 and closestBaseNearTargetLocation.distance_to(key) > 15: 1451 | ov = unassignedOverlords.pop() 1452 | value.add(ov.tag) 1453 | await self.do(ov.move(key)) 1454 | # move overlord back and unassign if base was taken by someone 1455 | elif len(value) > 0 and closestBaseNearTargetLocation.distance_to(key) <= 15 and self.townhalls.exists: 1456 | # TODO: make it move away if enemy shooting units nearby 1457 | ovTag = value.pop() 1458 | ov = self.units(OVERLORD).find_by_tag(ovTag) 1459 | if ov is not None: 1460 | await self.do(ov.move(self.townhalls.random.position)) 1461 | 1462 | if iteration % (self.workerTransferInterval * 10) == 0: 1463 | # make overlords shit if they have ability (when lair is done) 1464 | if self.units(OVERLORD).exists: 1465 | for ov in self.units(OVERLORD): 1466 | abilities = await self.get_available_abilities(ov) 1467 | if AbilityId.BEHAVIOR_GENERATECREEPON in abilities and self.can_afford(BEHAVIOR_GENERATECREEPON): 1468 | await self.do(ov(BEHAVIOR_GENERATECREEPON)) 1469 | 1470 | # make overseer create changeling 1471 | for ov in self.units(OVERSEER).idle.filter(lambda x:x.energy >= 50): 1472 | await self.do(ov(SPAWNCHANGELING_SPAWNCHANGELING)) 1473 | 1474 | # make changeling move to random enemy building if idle 1475 | for ch in self.units.filter(lambda x:x.type_id in [CHANGELING, CHANGELINGZEALOT, CHANGELINGMARINESHIELD, CHANGELINGMARINE, CHANGELINGZERGLINGWINGS, CHANGELINGZERGLING]).idle: 1476 | if self.known_enemy_structures.exists: 1477 | await self.do(ch.move(self.known_enemy_structures.random.position)) 1478 | 1479 | ignoreUnits = [OVERSEER, OBSERVERSIEGEMODE, OVERSEERSIEGEMODE, PHOTONCANNON, MISSILETURRET, RAVEN, SPORECRAWLER] 1480 | 1481 | # move overlords away from enemies 1482 | for ov in self.units(OVERLORD).filter(lambda x:x.tag not in assignedOverlordTags): 1483 | if ov.health < ov.health_max and (ov.is_idle or len(ov.orders) == 1 and ov.orders[0].ability.id in [AbilityId.MOVE]) and self.townhalls.exists: 1484 | closestTownhall = self.townhalls.closest_to(ov) 1485 | await self.do(ov.attack(closestTownhall.position)) # this way we know the overlord is fleeing? idk 1486 | 1487 | if iteration % self.workerTransferInterval * 2 == 0 and self.getTimeInSeconds() > 6*60: 1488 | # move drones away from enemies 1489 | for drone in self.units(DRONE): 1490 | if self.known_enemy_units.not_structure.exists: 1491 | nearbyEnemies = self.known_enemy_units.not_structure.closer_than(15, drone.position).filter(lambda x:x.type_id not in ignoreUnits) 1492 | if nearbyEnemies.exists and nearbyEnemies.amount >= 5: 1493 | townhallsWithoutEnemies = self.units & [] 1494 | for th in self.townhalls.ready: 1495 | if not self.known_enemy_units.not_structure.closer_than(30, th.position).exists: 1496 | townhallsWithoutEnemies.append(th) 1497 | if townhallsWithoutEnemies.exists: 1498 | closestTh = townhallsWithoutEnemies.closest_to(drone) 1499 | mf = self.state.mineral_field.closest_to(closestTh) 1500 | if mf is not None: 1501 | await self.do(drone.gather(mf)) 1502 | 1503 | # zergling scouting 1504 | if iteration % self.microInterval == 0 and hasattr(self, "scoutingUnits"): 1505 | if self.known_enemy_structures.exists: 1506 | self.opponentInfo["scoutingUnitsNeeded"] = 1 1507 | 1508 | # update scouting data 1509 | if not hasattr(self, "scoutingInfo"): 1510 | self.scoutingInfo = [] 1511 | # add spawn locations if they are not in the list 1512 | for base in self.enemy_start_locations: 1513 | isInList = next((x for x in self.scoutingInfo if x["location"].distance_to(base) < 10), None) 1514 | if isInList is None: 1515 | self.scoutingInfo.append({ 1516 | "location": base, 1517 | "lastScouted": 0, 1518 | "assignedScoutTag": None 1519 | }) 1520 | else: 1521 | # add existing enemy buildings 1522 | if self.known_enemy_structures.exists: 1523 | for struct in self.known_enemy_structures: 1524 | isInList = next((x for x in self.scoutingInfo if x["location"] == struct.position.to2), None) 1525 | if isInList is None: 1526 | self.scoutingInfo.append({ 1527 | "location": struct.position.to2, 1528 | "lastScouted": 0, # only start scouting hidden bases at 5 minutes 1529 | "assignedScoutTag": None 1530 | }) 1531 | 1532 | # TODO: remove enemy buildings that no longer exist 1533 | 1534 | # send scouting units 1535 | # objective: scout for hidden bases, scout for attacks, scout if opponent expanded 1536 | for lingTag in list(self.scoutingUnits)[:]: 1537 | ling = self.units(ZERGLING).find_by_tag(lingTag) 1538 | lingTarget = next((x for x in self.scoutingInfo if x["assignedScoutTag"] == lingTag), None) 1539 | 1540 | if ling is None: 1541 | if lingTarget is not None: 1542 | lingTarget["assignedScoutTag"] = None 1543 | self.scoutingUnits.discard(lingTag) 1544 | continue 1545 | 1546 | # flee from enemy units 1547 | scoutUnitInDanger = (self.known_enemy_units.not_structure.closer_than(10, ling).filter(lambda x: self.getSpecificUnitInfo(x)[0]["dps"] > 5) | self.known_enemy_structures.filter(lambda x: x.type_id in [BUNKER, SPINECRAWLER, PHOTONCANNON])).exists or ling.health < 20 1548 | if scoutUnitInDanger and self.townhalls.exists: 1549 | if ling.health < 20: 1550 | self.scoutingUnits.discard(lingTag) 1551 | if len(ling.orders) == 1 and ling.orders[0].ability.id in [AbilityId.ATTACK]: # here: attack-move means actively scouting a target 1552 | await self.do(ling.move(self.townhalls.closest_to(ling).position)) 1553 | if lingTarget is not None and lingTarget["location"].distance_to(ling.position.to2) < 15: 1554 | # enemy units nearby, that means this could be the enemy defending an expansion! 1555 | lingTarget["lastScouted"] = self.getTimeInSeconds() 1556 | lingTarget["assignedScoutTag"] = None 1557 | 1558 | else: 1559 | # assign new scouting target and move to it 1560 | if lingTarget is None: 1561 | scoutTargets = [base for base in self.scoutingInfo if base["assignedScoutTag"] is None] 1562 | if len(scoutTargets) > 0: 1563 | scoutTarget = sorted(scoutTargets, key=lambda x:x["lastScouted"])[0] 1564 | scoutTarget["assignedScoutTag"] = lingTag 1565 | await self.do(ling.attack(scoutTarget["location"])) 1566 | # if scouting target reached, mark current game time and unassign ling 1567 | elif lingTarget["location"].distance_to(ling.position.to2) < 10: 1568 | # TODO: add scouting info if ling runs by other base 1569 | lingTarget["lastScouted"] = self.getTimeInSeconds() 1570 | lingTarget["assignedScoutTag"] = None 1571 | # if ling is move commanding (which means fleeing here) it will a-move back to the scouting target 1572 | elif ling.is_idle or len(ling.orders) == 1 and ling.orders[0].ability.id in [AbilityId.MOVE]: 1573 | # print("scout-ling amoving towards scout locations, dist: {}".format(lingTarget["location"].distance_to(ling.position.to2))) 1574 | await self.do(ling.attack(lingTarget["location"])) 1575 | 1576 | # update scouting info depending on visible units that we can see 1577 | # if we see buildings that are not 1578 | # depot, rax, bunker, spine crawler, spore, nydus, pylon, cannon, gateway 1579 | # then assume that is the enemy spawn location 1580 | ignoreScoutingBuildings = [SUPPLYDEPOT, SUPPLYDEPOTLOWERED, BARRACKS, BUNKER, SPINECRAWLER, SPORECRAWLER, NYDUSNETWORK, NYDUSCANAL, PYLON, PHOTONCANNON, GATEWAY] 1581 | if self.opponentInfo["spawnLocation"] is None and len(self.enemy_start_locations) > 0: 1582 | if self.known_enemy_units.structure.exists: 1583 | enemyUnitsFiltered = self.known_enemy_units.structure.filter(lambda x:x.type_id not in ignoreScoutingBuildings) 1584 | if enemyUnitsFiltered.exists: 1585 | self.opponentInfo["spawnLocation"] = enemyUnitsFiltered.random.position.closest(self.enemy_start_locations) 1586 | 1587 | # figure out the race of the opponent 1588 | if self.opponentInfo["race"] is None and self.known_enemy_units.exists: 1589 | self.opponentInfo["race"] = self.getUnitInfo(self.known_enemy_units.random, "race") 1590 | racesDict = { 1591 | Race.Terran.value: "terran", 1592 | Race.Zerg.value: "zerg", 1593 | Race.Protoss.value: "protoss", 1594 | } 1595 | self.opponentInfo["race"] = racesDict[self.opponentInfo["race"]] 1596 | 1597 | # figure out how much army supply enemy has: 1598 | visibleEnemyUnits = self.known_enemy_units.not_structure.filter(lambda x:x.type_id not in [DRONE, SCV, PROBE, LARVA, EGG]) 1599 | for unit in visibleEnemyUnits: 1600 | isUnitInInfo = next((x for x in self.opponentInfo["armyTagsScouted"] if x["tag"] == unit.tag), None) 1601 | if isUnitInInfo is not None: 1602 | self.opponentInfo["armyTagsScouted"].remove(isUnitInInfo) 1603 | # if unit.tag not in self.opponentInfo["armyTagsScouted"]: 1604 | if self.townhalls.ready.exists: 1605 | self.opponentInfo["armyTagsScouted"].append({ 1606 | "tag": unit.tag, 1607 | "scoutTime": self.getTimeInSeconds(), 1608 | "supply": self.getUnitInfo(unit) or 0, 1609 | "distanceToBase": self.townhalls.ready.closest_to(unit).distance_to(unit), 1610 | }) 1611 | 1612 | # get opponent army supply (scouted / visible) 1613 | tempTime = self.getTimeInSeconds() - 30 # TODO: set the time on how long until the scouted army supply times out 1614 | self.opponentInfo["armySupplyScouted"] = sum(x["supply"] for x in self.opponentInfo["armyTagsScouted"] if x["scoutTime"] > tempTime) 1615 | self.opponentInfo["armySupplyScoutedClose"] = sum(x["supply"] for x in self.opponentInfo["armyTagsScouted"] if x["scoutTime"] > tempTime and x["distanceToBase"] < 60) 1616 | self.opponentInfo["armySupplyVisible"] = sum(self.getUnitInfo(x) or 0 for x in visibleEnemyUnits) 1617 | 1618 | # get opponent expansions 1619 | if iteration % 20 == 0: 1620 | enemyTownhalls = self.known_enemy_units.structure.filter(lambda x:x.type_id in [HATCHERY, LAIR, HIVE, COMMANDCENTER, PLANETARYFORTRESS, ORBITALCOMMAND, NEXUS]) 1621 | for th in enemyTownhalls: 1622 | if len(self.opponentInfo["expansions"]) > 0 and th.position.closest(self.opponentInfo["expansions"]).distance_to(th.position.to2) < 20: 1623 | continue 1624 | if th.tag not in self.opponentInfo["expansionsTags"]: 1625 | self.opponentInfo["expansionsTags"].add(th.tag) 1626 | self.opponentInfo["expansions"].append(th.position.to2) 1627 | print("found a new enemy base!") 1628 | print(self.opponentInfo["expansions"]) 1629 | 1630 | ################################ 1631 | ######### BUILDINGS MICRO 1632 | ################################ 1633 | 1634 | # cancel building if hp much lower than build progress to get some money back 1635 | if iteration % self.microInterval == 0: 1636 | ignoreBuildings = [CREEPTUMOR, CREEPTUMORBURROWED, CREEPTUMORQUEEN] 1637 | for building in self.units.structure.not_ready.filter(lambda x:x.type_id not in ignoreBuildings): 1638 | if building.health / building.health_max < building.build_progress - 0.5 or building.health / building.health_max < 0.05 and building.build_progress > 0.1: 1639 | await self.do(building(CANCEL)) 1640 | 1641 | ################################ 1642 | ######### ROACH MICRO 1643 | ################################ 1644 | 1645 | # roach burrow micro when on low hp, unburrow when at high hp again 1646 | if iteration % self.microInterval == 0: 1647 | for roach in self.units(ROACH): 1648 | # burrow when low hp 1649 | if roach.health / roach.health_max < 5/10: 1650 | abilities = await self.get_available_abilities(roach) 1651 | if AbilityId.BURROWDOWN_ROACH in abilities and self.can_afford(BURROWDOWN_ROACH): 1652 | await self.do(roach(BURROWDOWN_ROACH)) 1653 | 1654 | for roach in self.units(ROACHBURROWED): 1655 | if 9/10 <= roach.health / roach.health_max <= 2 and roach.is_burrowed: 1656 | abilities = await self.get_available_abilities(roach) 1657 | # print(abilities) 1658 | if AbilityId.BURROWUP_ROACH in abilities and self.can_afford(BURROWUP_ROACH): 1659 | await self.do(roach(BURROWUP_ROACH)) 1660 | else: 1661 | nearbyDetection = self.known_enemy_units.filter(lambda x:x.type_id in [OVERSEER, OBSERVERSIEGEMODE, OVERSEERSIEGEMODE, PHOTONCANNON, MISSILETURRET, RAVEN, SPORECRAWLER]) 1662 | if nearbyDetection.exists: 1663 | pass # TODO: implement, move away from enemy, or dont and tank damage 1664 | elif iteration % self.microInterval * 5 == 0: 1665 | nearbyGroundEnemies = self.known_enemy_units.not_structure.filter(lambda x: not x.is_flying) 1666 | if nearbyGroundEnemies.exists: 1667 | closest = nearbyGroundEnemies.closest_to(roach) 1668 | await self.do(roach.move(closest.position)) 1669 | 1670 | if iteration % self.microInterval == 0: 1671 | ################################ 1672 | ######### SKYZERG (preparation) 1673 | ################################ 1674 | 1675 | # skyzerg and queen management preparation 1676 | if self.units(QUEEN).exists: 1677 | if not hasattr(self, "transfuseProcess"): # prevent mass transfusion (like overheal) 1678 | self.transfuseProcess = {} 1679 | else: 1680 | # clear expired transfuses 1681 | for key in list(self.transfuseProcess.keys())[:]: 1682 | value = self.transfuseProcess[key] 1683 | if value["expiryTime"] < self.getTimeInSeconds(): 1684 | self.transfuseProcess.pop(key) 1685 | 1686 | queensWith50Energy = self.units(QUEEN).filter(lambda x:x.energy >= 50) 1687 | queensWith190Energy = queensWith50Energy.filter(lambda x:x.energy > 190) 1688 | targetsBeingTransfusedTags = {x["target"] for x in self.transfuseProcess.values()} 1689 | transfuseTargets = self.units.filter(lambda x: x.tag not in targetsBeingTransfusedTags and x.type_id in [QUEEN, BROODLORD, CORRUPTOR, SPINECRAWLER, OVERSEER, ROACH] and (x.health_max - x.health >= 125 or x.health / x.health_max < 1/4)) 1690 | transfuseTargetsBuildings = self.units.structure.filter(lambda x: x.health_max - x.health > 125 and x.type_id not in [SPINECRAWLER]) 1691 | 1692 | for q in queensWith50Energy: 1693 | if q.tag in self.transfuseProcess: # already transfusing 1694 | continue 1695 | targetsWithoutThisQueen = transfuseTargets.filter(lambda x: x.tag != q.tag) 1696 | if not targetsWithoutThisQueen.exists: 1697 | continue 1698 | transfuseTarget = targetsWithoutThisQueen.closest_to(q) 1699 | if transfuseTarget.distance_to(q) < 7: # replace with general queen transfuse range 1700 | abilities = await self.get_available_abilities(q) 1701 | if AbilityId.TRANSFUSION_TRANSFUSION in abilities and self.can_afford(TRANSFUSION_TRANSFUSION): 1702 | self.transfuseProcess[q.tag] = {"target": transfuseTarget.tag, "expiryTime": self.getTimeInSeconds() + 1} 1703 | targetsBeingTransfusedTags.add(transfuseTarget.tag) 1704 | transfuseTargets = transfuseTargets.filter(lambda x:x.tag not in targetsBeingTransfusedTags) 1705 | await self.do(q(TRANSFUSION_TRANSFUSION, transfuseTarget)) 1706 | 1707 | # queen transfuse buildings when in range and >= 190 energy 1708 | for q in queensWith190Energy: 1709 | if q.tag in self.transfuseProcess: # already transfusing 1710 | continue 1711 | if not transfuseTargetsBuildings.exists: 1712 | continue 1713 | transfuseTarget = transfuseTargetsBuildings.closest_to(q) 1714 | if transfuseTarget.distance_to(q) < 7: # replace with general queen transfuse range 1715 | abilities = await self.get_available_abilities(q) 1716 | if AbilityId.TRANSFUSION_TRANSFUSION in abilities and self.can_afford(TRANSFUSION_TRANSFUSION): 1717 | self.transfuseProcess[q.tag] = {"target": transfuseTarget.tag, "expiryTime": self.getTimeInSeconds() + 1} 1718 | targetsBeingTransfusedTags.add(transfuseTarget.tag) 1719 | transfuseTargetsBuildings = transfuseTargetsBuildings.filter(lambda x:x.tag not in targetsBeingTransfusedTags) 1720 | await self.do(q(TRANSFUSION_TRANSFUSION, transfuseTarget)) 1721 | 1722 | for vp in self.units(VIPER).filter(lambda x:x.is_idle or len(x.orders) == 1 and x.orders[0].ability.id in [AbilityId.MOVE, AbilityId.SCAN_MOVE, AbilityId.ATTACK]): 1723 | # if self.units(VIPER).exists: 1724 | # if len(vp.orders) == 1: 1725 | # abilities = await self.get_available_abilities(self.units(VIPER).random) 1726 | # print(vp.orders) 1727 | # print(abilities) 1728 | 1729 | # abduct targets: carrier, tempest, mothership, battlecruiser, siege tank, thor, lurker 1730 | if vp.energy < 125: 1731 | # get energy 1732 | highHpBuildings = self.units.structure.filter(lambda x:x.health > 400) 1733 | if highHpBuildings.exists: 1734 | highHpBuilding = highHpBuildings.closest_to(vp) 1735 | await self.do(vp(VIPERCONSUMESTRUCTURE_VIPERCONSUME, highHpBuilding)) 1736 | else: 1737 | viperChoices = [] 1738 | # para bomb, priotize! 1739 | if vp.energy >= 125: 1740 | enemyAirUnits = self.known_enemy_units.not_structure.filter(lambda x:x.type_id in [MUTALISK, VIKINGFIGHTER, CORRUPTOR, BANSHEE, RAVEN, VOIDRAY, PHOENIX, ORACLE, VIPER] and not x.has_buff(PARASITICBOMB) and x.is_flying).closer_than(13, vp.position) 1741 | if enemyAirUnits.exists: 1742 | for target in enemyAirUnits: 1743 | viperChoices.append(vp(PARASITICBOMB_PARASITICBOMB, target)) 1744 | # blinding cloud 1745 | if vp.energy >= 100: 1746 | targets = [SIEGETANKSIEGED, THOR, HYDRALISK, ARCHON] 1747 | targets2 = [MARINE, QUEEN, STALKER, SIEGETANK, MARAUDER, CYCLONE] 1748 | blindCloudTargets = self.known_enemy_units.not_structure.filter(lambda x:x.type_id in targets and not x.has_buff(BLINDINGCLOUD)).closer_than(12, vp.position) 1749 | if not blindCloudTargets.exists: 1750 | blindCloudTargets = self.known_enemy_units.not_structure.filter(lambda x:x.type_id.value in targets2 and not x.has_buff(BLINDINGCLOUD)).closer_than(12, vp.position) 1751 | if blindCloudTargets.exists: 1752 | for target in blindCloudTargets: 1753 | viperChoices.append(vp(BLINDINGCLOUD_BLINDINGCLOUD, target.position)) 1754 | # abduct 1755 | if vp.energy >= 75: 1756 | targets = [SIEGETANKSIEGED, THOR, LURKER, LURKERMPBURROWED, ARCHON, MOTHERSHIP, CARRIER, TEMPEST, LIBERATOR] 1757 | targets2 = [QUEEN, STALKER, SIEGETANK, MARAUDER, CYCLONE] 1758 | abductTargets = self.known_enemy_units.not_structure.filter(lambda x:x.type_id in targets).closer_than(11, vp.position) 1759 | if not abductTargets.exists: 1760 | abductTargets = self.known_enemy_units.not_structure.filter(lambda x:x.type_id in targets2).closer_than(11, vp.position) 1761 | if abductTargets.exists: 1762 | for target in abductTargets: 1763 | viperChoices.append(vp(EFFECT_ABDUCT, target)) 1764 | 1765 | if len(viperChoices) > 0: 1766 | choice = random.choice(viperChoices) 1767 | await self.do(choice) 1768 | 1769 | 1770 | 1771 | # move corruptors to random position on map if we see no enemy structures 1772 | if iteration % self.microInterval*10 == 0: 1773 | if (not self.known_enemy_structures.exists): 1774 | stepSize = 3 1775 | pointsOnPlayableMap = [Point2((x, y)) \ 1776 | for x in range(self._game_info.playable_area[0]+stepSize, self._game_info.playable_area[0] + self._game_info.playable_area[2], stepSize) \ 1777 | for y in range(self._game_info.playable_area[1]+stepSize, self._game_info.playable_area[1] + self._game_info.playable_area[3], stepSize)] 1778 | for cr in self.units(CORRUPTOR).idle: 1779 | await self.do(cr.attack(random.choice(pointsOnPlayableMap))) 1780 | 1781 | 1782 | elif self.known_enemy_structures.filter(lambda x:x.is_flying).exists: 1783 | flyingBuildings = self.known_enemy_structures.filter(lambda x:x.is_flying) 1784 | # make idle corruptors amove to them 1785 | for cr in self.units(CORRUPTOR).idle: 1786 | await self.do(cr.attack(flyingBuildings.random.position)) 1787 | 1788 | ################################ 1789 | ######### UNIT MANAGEMENT 1790 | ################################ 1791 | 1792 | unitsAssignedToGroups = set().union(*[group.getMyUnitTags() for group in [self.myDefendGroup, self.myAttackGroup, self.mySupportGroup] if group is not None]) 1793 | 1794 | # clear defendgroup, attackgroup, supportgroup if they are empty, this way a new attackgroup will be formed 1795 | if self.myDefendGroup is not None and len(self.myDefendGroup.getMyUnitTags()) < 1: 1796 | self.myDefendGroup = None 1797 | if self.myAttackGroup is not None and (len(self.myAttackGroup.getMyUnitTags()) < 1 or self.supply_used < 140): 1798 | print("attackgroup disbanded because lowish on supply") 1799 | self.myAttackGroup = None 1800 | self.mySupportGroup = None 1801 | # if self.mySupportGroup is not None and len(self.mySupportGroup.getMyUnitTags()) < 1: 1802 | # self.mySupportGroup = None 1803 | 1804 | # make queens and raoches defend stuff nearby 1805 | if self.myDefendGroup is None: 1806 | self.myDefendGroup = ManageThreats(self._client, self._game_data) 1807 | self.myDefendGroup.maxAssignedPerUnit = 2 1808 | self.myDefendGroup.retreatWhenHp = 0.25 1809 | self.myDefendGroup.mode = "distributeEqually" 1810 | 1811 | if iteration % self.microInterval*5 == 0: 1812 | # add threats: all enemy units that are closer than 30 to nearby townhalls (which are completed) 1813 | thisRoundAddedThreats = set() 1814 | oldThreats = self.myDefendGroup.getThreatTags() 1815 | for th in self.townhalls.ready: 1816 | enemiesCloseToTh = self.known_enemy_units.closer_than(self.defendRangeToTownhalls, th.position) 1817 | self.myDefendGroup.addThreat(enemiesCloseToTh) 1818 | thisRoundAddedThreats |= {x.tag for x in enemiesCloseToTh} 1819 | # threats that are outside the 30 range or no longer visible on map: 1820 | notVisibleOrOutsideRangeThreats = oldThreats - thisRoundAddedThreats 1821 | self.myDefendGroup.clearThreats(notVisibleOrOutsideRangeThreats) 1822 | 1823 | # add units to defense 1824 | myDefendUnits = self.units & [] 1825 | myDefendUnits += self.units(ROACH) 1826 | myDefendUnits += self.units(OVERSEER) 1827 | myDefendUnits += self.units(ROACHBURROWED) 1828 | myDefendUnits += self.units(BROODLORD) 1829 | myDefendUnits += self.units(BROODLORDCOCOON) 1830 | myDefendUnits += self.units(VIPER) 1831 | myDefendUnits += self.units(CORRUPTOR) 1832 | myDefendUnits += self.units(ZERGLING).filter(lambda x:x.tag not in self.scoutingUnits) 1833 | # remove queens after 5 mins (used for early game defense): 1834 | if self.getTimeInSeconds() < 5*60 and self.opponentInfo["armySupplyScoutedClose"] > 2: 1835 | self.myDefendGroup.addDefense(self.units(QUEEN)) 1836 | elif hasattr(self, "queensAssignedHatcheries"): 1837 | myDefendUnits += self.units(QUEEN).filter(lambda q: q.tag not in self.queensAssignedHatcheries) 1838 | self.myDefendGroup.removeDefense(self.units(QUEEN).filter(lambda q: q.tag in self.queensAssignedHatcheries)) 1839 | # self.myDefendGroup.addDefense(self.units(ROACH)) 1840 | # self.myDefendGroup.addDefense(self.units(ZERGLING).filter(lambda x:x.tag not in self.scoutingUnits)) 1841 | myDefendUnits = myDefendUnits.filter(lambda x:x.tag not in unitsAssignedToGroups) 1842 | self.myDefendGroup.addDefense(myDefendUnits) 1843 | self.myDefendGroup.removeDefense(self.units(ZERGLING).filter(lambda x:x.tag in self.scoutingUnits)) 1844 | 1845 | # setting retreat location 1846 | if self.units(SPINECRAWLER).ready.exists: 1847 | self.myDefendGroup.setRetreatLocations(self.units(SPINECRAWLER), removePreviousLocations=True) 1848 | elif self.townhalls.exists: 1849 | self.myDefendGroup.setRetreatLocations(self.townhalls, removePreviousLocations=True) 1850 | 1851 | # attack when we have a big army 1852 | if self.myAttackGroup is None: 1853 | if all([ 1854 | # check if we have: requirement to attack = roach burrow 1855 | not self.waitForRoachBurrowBeforeAttacking or (self.units(DRONE).exists and AbilityId.BURROWDOWN_DRONE in (await self.get_available_abilities(self.units(DRONE).random))), 1856 | # check if we have: requirement to attack = at least 1 broodlord 1857 | not self.waitForBroodlordsBeforeAttacking or self.units(BROODLORD).exists 1858 | ]): 1859 | myArmy = self.units(ROACH) | self.units(ROACHBURROWED) | self.units(BROODLORD) | self.units(BROODLORDCOCOON) | self.units(EGG).filter(lambda x: x.orders[0].ability in [ROACH]) 1860 | mySupportUnits = self.units(VIPER) | self.units(CORRUPTOR) | self.units(OVERSEER) | self.units(EGG).filter(lambda x: x.orders[0].ability in [CORRUPTOR, VIPER]) 1861 | if hasattr(self, "queensAssignedHatcheries"): 1862 | mySupportUnits.extend(self.units(QUEEN).filter(lambda x: x.tag not in self.queensAssignedHatcheries)) 1863 | 1864 | if self.supply_used >= 195: 1865 | # disband defendgroup once when creating attackgroup, reassign new units to it when they spawn 1866 | self.myDefendGroup = None 1867 | 1868 | # create attackgroup, has no retreat 1869 | knownEnemyUnitsFiltered = self.known_enemy_units.filter(lambda x: x.type_id not in [OVERLORD, OVERSEER, OVERLORDTRANSPORT, OVERSEERSIEGEMODE]) 1870 | self.myAttackGroup = ManageThreats(self._client, self._game_data) 1871 | print("{} - attack started!".format(self.getTimeInSeconds())) 1872 | self.myAttackGroup.clumpUpEnabled = True 1873 | self.myAttackGroup.addThreat(knownEnemyUnitsFiltered) 1874 | self.myAttackGroup.maxAssignedPerUnit = 2 1875 | self.myAttackGroup.addDefense(myArmy) 1876 | self.myAttackGroup.attackLocations = self.enemy_start_locations 1877 | 1878 | # create support group that helps the attackgroup 1879 | self.mySupportGroup = ManageThreats(self._client, self._game_data) 1880 | self.mySupportGroup.addThreat(myArmy) 1881 | self.mySupportGroup.addDefense(mySupportUnits) 1882 | self.mySupportGroup.maxAssignedPerUnit = 5 1883 | self.mySupportGroup.mode = "distributeEqually" 1884 | self.mySupportGroup.retreatWhenHp = 0.25 1885 | self.mySupportGroup.treatThreatsAsAllies = True 1886 | if self.townhalls.exists: 1887 | self.mySupportGroup.setRetreatLocations(self.townhalls) 1888 | 1889 | else: 1890 | # keep adding corruptors, broodlords and vipers to the groups, because corruptors will constantly morph 1891 | myArmy = self.units(ROACH) | self.units(ROACHBURROWED) | self.units(BROODLORD) | self.units(BROODLORDCOCOON) 1892 | mySupportUnits = self.units(VIPER) | self.units(CORRUPTOR) | self.units(OVERSEER) 1893 | knownEnemyUnitsFilteredInRange = self.known_enemy_units.filter(lambda x: x.type_id not in [OVERLORD, OVERSEER, OVERLORDTRANSPORT, OVERSEERSIEGEMODE] and myArmy.closer_than(25, x.position).exists) 1894 | knownEnemyUnitsFilteredOutsideRange = self.known_enemy_units.filter(lambda x: x.type_id not in [OVERLORD, OVERSEER, OVERLORDTRANSPORT, OVERSEERSIEGEMODE] and not myArmy.closer_than(30, x.position).exists) 1895 | knownEnemyUnitsFilteredOutsideRangeTags = {x.tag for x in knownEnemyUnitsFilteredOutsideRange} 1896 | if hasattr(self, "queensAssignedHatcheries"): 1897 | mySupportUnits.extend(self.units(QUEEN).filter(lambda x: x.tag not in self.queensAssignedHatcheries)) 1898 | if self.myAttackGroup is not None: 1899 | # keep updating the enemy unit list 1900 | self.myAttackGroup.addThreat(knownEnemyUnitsFilteredInRange) 1901 | self.myAttackGroup.clearThreats(knownEnemyUnitsFilteredOutsideRangeTags) 1902 | if self.allowedToResupplyAttackGroups: 1903 | self.myAttackGroup.addDefense(myArmy) 1904 | 1905 | if self.mySupportGroup is not None: 1906 | # keep updating the support list 1907 | self.mySupportGroup.addThreat(myArmy) 1908 | self.mySupportGroup.removeDefense(myArmy) # removes corruptors that morphed to broodlords 1909 | if self.allowedToResupplyAttackGroups: 1910 | self.mySupportGroup.addDefense(mySupportUnits) 1911 | 1912 | # micro units, assign them targets etc (from the assigned unit tags above) 1913 | if iteration % self.microInterval == 0: 1914 | if self.myDefendGroup is not None: 1915 | # all units (that can shoot) except scouting units and inject queens are put in here 1916 | await self.myDefendGroup.update(self.units, self.known_enemy_units, self.enemy_start_locations, iteration) 1917 | if self.myAttackGroup is not None: 1918 | # contains roaches, burrow roaches, broodlords 1919 | # this group will not retreat 1920 | await self.myAttackGroup.update(self.units, self.known_enemy_units, self.enemy_start_locations, iteration) 1921 | if self.mySupportGroup is not None: 1922 | # contains queens, corruptors, vipers 1923 | await self.mySupportGroup.update(self.units, self.known_enemy_units, self.enemy_start_locations, iteration) 1924 | 1925 | ################################ 1926 | ######### DEFENSIVE REACTIONS (buildings) 1927 | ################################ 1928 | 1929 | # make spines if we scouted that opponent has large army supply at <5 mins 1930 | if iteration % self.buildStuffInverval == 0 and 120 < self.getTimeInSeconds() < 240 and self.opponentInfo["armySupplyScoutedClose"] > 2 and len(self.opponentInfo["expansions"]) < 2 and self.townhalls.ready.amount > 1 and self.units(SPINECRAWLER).amount < min(3, self.opponentInfo["armySupplyScoutedClose"] // 2) and self.already_pending(SPINECRAWLER) < 1: 1931 | if self.currentDroneCountIncludingPending > 10: 1932 | self.workerProductionEnabled.append(False) 1933 | furthestExpansion = self.opponentInfo["furthestAwayExpansion"] or random.choice(self.enemy_start_locations) 1934 | 1935 | if self.can_afford(SPINECRAWLER) and self.workers.exists and self.townhalls.exists and furthestExpansion is not None: 1936 | newestBase = max(self.townhalls.ready, key=lambda x:x.tag) 1937 | 1938 | if furthestExpansion.position.to2 == newestBase.position.to2: 1939 | print("ERROR in spine placement") 1940 | else: 1941 | loc = newestBase.position.to2.towards(furthestExpansion.position.to2, 3) 1942 | loc = await self.find_placement(SPINECRAWLER, loc, placement_step=3, minDistanceToResources=4) 1943 | w = self.workers.closest_to(loc) 1944 | if self.can_afford(SPINECRAWLER) and loc is not None: 1945 | err = await self.do(w.build(SPINECRAWLER, loc)) 1946 | 1947 | # make spores if we scouted oracle / banshee / mutas / corruptors 1948 | if iteration % self.buildStuffInverval == 0: 1949 | airUnits = self.known_enemy_units.filter(lambda x:x.type_id in [ORACLE, BANSHEE, MUTALISK, CORRUPTOR, DARKSHRINE, STARGATE, PHOENIX, DARKTEMPLAR]) # STARPORTTECHLAB 1950 | if airUnits.exists and self.already_pending(SPORECRAWLER) < 1: 1951 | if self.getTimeInSeconds() < 6*60: 1952 | self.workerProductionEnabled.append(False) 1953 | if self.can_afford(SPORECRAWLER): 1954 | for th in self.townhalls.ready: 1955 | # spore already exists? 1956 | if self.units(SPORECRAWLER).exists and self.units(SPORECRAWLER).closest_to(th).distance_to(th) < 10: 1957 | continue 1958 | mfs = self.state.mineral_field.closer_than(10, th.position.to2) 1959 | # if mineral fields are nearby? dont make spores at a mined out base 1960 | if not mfs.exists or not self.workers.exists: 1961 | continue 1962 | if self.centerOfUnits(mfs).to2 != th.position.to2: 1963 | loc = self.centerOfUnits(mfs).towards(th.position, 4) 1964 | loc = await self.find_placement(SPORECRAWLER, loc, placement_step=1, minDistanceToResources=0) 1965 | w = self.workers.closest_to(loc) 1966 | if loc is not None and self.can_afford(SPORECRAWLER): 1967 | await self.do(w.build(SPORECRAWLER, loc)) 1968 | 1969 | ################################ 1970 | ######### DEFENSIVE REACTION (worker rush, cannon rush) 1971 | ################################ 1972 | 1973 | # worker rush defense 1974 | # cannon rush defense: if a probe + pylon were scouted near any of our building and gametime < 4:00: attack probe with 1 drone and each scouted cannon with 4 drones 1975 | if self.townhalls.exists and self.getTimeInSeconds() < 5*60: 1976 | 1977 | cannonRushUnits = self.units & [] 1978 | for th in self.townhalls: 1979 | cannonRushUnits |= self.known_enemy_units.closer_than(30, th.position) 1980 | pylons = cannonRushUnits(PYLON) 1981 | probes = cannonRushUnits.filter(lambda x:x.type_id in [PROBE, SCV, DRONE, ZERGLING]) 1982 | cannons = cannonRushUnits.filter(lambda x:x.type_id in [PHOTONCANNON, SPINECRAWLER]) 1983 | 1984 | if (pylons.amount + probes.amount > 0 or cannons.amount > 0) and self.opponentInfo["armySupplyVisible"] < 3 and self.units(SPINECRAWLER).ready.amount < 1 and self.units(QUEEN).amount < 4: 1985 | if not hasattr(self, "defendCannonRushProbes"): 1986 | self.defendCannonRushProbes = {} 1987 | self.defendCannonRushCannons = {} 1988 | 1989 | assignedDroneTagsSets = [x for x in self.defendCannonRushProbes.values()] + [x for x in self.defendCannonRushCannons.values()] 1990 | assignedDroneTags = set() 1991 | for sett in assignedDroneTagsSets: 1992 | assignedDroneTags |= sett 1993 | unassignedDrones = self.units(DRONE).filter(lambda x: x.tag not in assignedDroneTags and x.health > 6) 1994 | unassignedDroneTags = set((x.tag for x in unassignedDrones)) 1995 | 1996 | # adding probe and cannons as threats 1997 | for probe in probes: 1998 | if probe.tag not in self.defendCannonRushProbes: 1999 | self.defendCannonRushProbes[probe.tag] = set() 2000 | for cannon in cannons: 2001 | if cannon.tag not in self.defendCannonRushCannons: 2002 | self.defendCannonRushCannons[cannon.tag] = set() 2003 | 2004 | # filter out dead units chasing probe 2005 | for probeTag, droneTags in self.defendCannonRushProbes.items(): 2006 | drones = self.units(DRONE).filter(lambda x:x.tag in droneTags) 2007 | lowHpDrones = drones.filter(lambda x:x.health < 7) 2008 | for drone in lowHpDrones: 2009 | mf = self.state.mineral_field.closest_to(self.townhalls.closest_to(drone)) 2010 | await self.do(drone.gather(mf)) 2011 | drones.remove(drone) 2012 | self.defendCannonRushProbes[probeTag] = set(x.tag for x in drones) # clear dead drones 2013 | # if probe not alive anymore or outside of range, send drones to mining 2014 | # print(drones, unassignedDrones) 2015 | if probeTag not in [x.tag for x in probes]: 2016 | self.defendCannonRushProbes.pop(probeTag) 2017 | break # iterating over a changing dictionary 2018 | 2019 | # if probe still alive, check if it has a drone chasing it 2020 | elif drones.amount < 1 and unassignedDrones.amount > 0: 2021 | # if no drones chasing it, get a random drone and assign it to probe 2022 | probe = probes.find_by_tag(probeTag) 2023 | newDrone = unassignedDrones.closest_to(probe) 2024 | mf = self.state.mineral_field.closest_to(self.townhalls.closest_to(newDrone)) 2025 | if probe is not None and newDrone is not None: 2026 | unassignedDroneTags.remove(newDrone.tag) 2027 | unassignedDrones.remove(newDrone) # TODO: need to test if this works 2028 | # unassignedDrones = unassignedDrones.filter(lambda x: x.tag in unassignedDroneTags) 2029 | self.defendCannonRushProbes[probeTag].add(newDrone.tag) 2030 | await self.do(newDrone.attack(probe)) 2031 | await self.do(newDrone.gather(mf, queue=True)) 2032 | 2033 | # filter out dead units attacking a cannon 2034 | for cannonTag, droneTags in self.defendCannonRushCannons.items(): 2035 | drones = self.units(DRONE).filter(lambda x:x.tag in droneTags) 2036 | self.defendCannonRushCannons[cannonTag] = set(x.tag for x in drones) # clear dead drones 2037 | # if cannon not alive anymore or outside of range, send drones to mining 2038 | if cannonTag not in [x.tag for x in cannons]: 2039 | self.defendCannonRushCannons.pop(cannonTag) 2040 | break # iterating over a changing dictionary 2041 | # if probe still alive, check if it has a drone chasing it 2042 | elif drones.amount < 4 and unassignedDrones.amount > 0: 2043 | # if no drones chasing it, get a random drone and assign it to probe 2044 | for i in range(4 - drones.amount): 2045 | if unassignedDrones.amount <= 0: 2046 | break 2047 | cannon = cannons.find_by_tag(cannonTag) 2048 | newDrone = unassignedDrones.closest_to(cannon) 2049 | mf = self.state.mineral_field.closest_to(self.townhalls.closest_to(newDrone)) 2050 | if cannon is not None and newDrone is not None: 2051 | unassignedDroneTags.remove(newDrone.tag) 2052 | unassignedDrones.remove(newDrone) 2053 | self.defendCannonRushCannons[cannonTag].add(newDrone.tag) 2054 | await self.do(newDrone.attack(cannon)) 2055 | await self.do(newDrone.gather(mf, queue=True)) 2056 | 2057 | ################################ 2058 | ######### DEFENSIVE REACTION (mass lings / 1base allin) 2059 | ################################ 2060 | 2061 | # TODO: implement defensive maneuvers: 2062 | # proxy rax 2063 | # 12 pool 2064 | # proxy gate 2065 | 2066 | # if pool is done, make queens 2067 | if all(self.queenProductionEnabled) and self.units(SPAWNINGPOOL).ready.exists and self.units(QUEEN).amount + self.already_pending(QUEEN) < self.totalQueenLimit and (self.units(HATCHERY) | self.units(HIVE)).ready.idle.exists and iteration % self.buildStuffInverval == 0 and self.supply_left > 1 and self.supply_used < 197: 2068 | # pause worker production to squeeze out first N queens 2069 | if len(self.units(QUEEN)) + self.already_pending(QUEEN) < self.priotizeFirstNQueens: 2070 | self.workerProductionEnabled.append(False) 2071 | for hatch in (self.units(HATCHERY) | self.units(HIVE)).ready.idle: 2072 | if self.can_afford(QUEEN): 2073 | err = await self.do(hatch.train(QUEEN)) 2074 | if not err: 2075 | break 2076 | 2077 | # make more drones 2078 | # print(self.workerProductionEnabled) 2079 | if all(self.workerProductionEnabled) and iteration % self.buildStuffInverval == 0: 2080 | if self.supply_left > 0 and self.supply_used < 198 and self.can_afford(DRONE) and self.units(LARVA).exists and self.currentDroneCountIncludingPending < self.droneLimit: 2081 | dronesAffordable = min(198 - self.supply_used, self.droneLimit - self.units(DRONE).amount + self.already_pending(DRONE), self.supply_left // 1, self.minerals // 50, self.units(LARVA).amount) 2082 | # print("current drones: {}, affordable: {}".format(self.units(DRONE).amount + self.already_pending(DRONE), dronesAffordable)) 2083 | for count, larva in enumerate(self.units(LARVA)): 2084 | if count >= dronesAffordable or count + self.currentDroneCountIncludingPending >= self.droneLimit: # because drones in extractor appear as "missing" and dont get counted 2085 | break 2086 | if self.can_afford(DRONE): 2087 | await self.do(larva.train(DRONE)) 2088 | 2089 | ################################ 2090 | ######### TESTING 2091 | ################################ 2092 | 2093 | 2094 | # print(1, self.getUnitInfo(self.units(HATCHERY).first)) 2095 | # print(2, self.getUnitInfo(HATCHERY)) 2096 | 2097 | # self._client.leave() 2098 | 2099 | 2100 | 2101 | def main(): 2102 | # sc2.run_game(sc2.maps.get("(2)RedshiftLE"), [ 2103 | # Bot(Race.Zerg, CreepyBot()), 2104 | # Bot(Race.Protoss, CannonRushBot()) 2105 | # ], realtime=False) 2106 | 2107 | # sc2.run_game(sc2.maps.get("(2)16-BitLE"), [ 2108 | # Bot(Race.Zerg, CreepyBot()), 2109 | # Bot(Race.Protoss, CannonRushBot()) 2110 | # ], realtime=False) 2111 | 2112 | # sc2.run_game(sc2.maps.get("(2)RedshiftLE"), [ 2113 | # Bot(Race.Zerg, CreepyBot()), 2114 | # Bot(Race.Protoss, ThreebaseVoidrayBot()) 2115 | # ], realtime=False) 2116 | 2117 | # sc2.run_game(sc2.maps.get("(2)16-BitLE"), [ 2118 | # Bot(Race.Zerg, CreepyBot()), 2119 | # Bot(Race.Zerg, ZergRushBot()) 2120 | # ], realtime=False) 2121 | 2122 | # sc2.run_game(sc2.maps.get("(2)RedshiftLE"), [ 2123 | # Bot(Race.Zerg, CreepyBot()), 2124 | # Bot(Race.Zerg, ZergRushBot()) 2125 | # ], realtime=False) 2126 | 2127 | # sc2.run_game(sc2.maps.get("(2)RedshiftLE"), [ 2128 | # Bot(Race.Zerg, CreepyBot()), 2129 | # Computer(Race.Zerg, Difficulty.Easy) 2130 | # ], realtime=False) 2131 | 2132 | # sc2.run_game(sc2.maps.get("(2)RedshiftLE"), [ 2133 | # Bot(Race.Zerg, CreepyBot()), 2134 | # Computer(Race.Random, Difficulty.VeryHard) 2135 | # ], realtime=False) 2136 | 2137 | # sc2.run_game(sc2.maps.get("(4)DarknessSanctuaryLE"), [ 2138 | # Bot(Race.Zerg, CreepyBot()), 2139 | # Computer(Race.Random, Difficulty.CheatMoney) 2140 | # ], realtime=False) 2141 | 2142 | sc2.run_game(sc2.maps.get("(2)16-BitLE"), [ 2143 | Bot(Race.Zerg, CreepyBot()), 2144 | Computer(Race.Protoss, Difficulty.CheatInsane) 2145 | ], realtime=False) 2146 | 2147 | # sc2.run_game(sc2.maps.get("(2)RedshiftLE"), [ 2148 | # Human(Race.Protoss), 2149 | # Bot(Race.Zerg, CreepyBot()) 2150 | # ], realtime=True) 2151 | 2152 | if __name__ == '__main__': 2153 | main() 2154 | -------------------------------------------------------------------------------- /CreepyBot/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hannes Rydén 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CreepyBot/README.txt: -------------------------------------------------------------------------------- 1 | API: https://github.com/Dentosal/python-sc2 2 | 3 | bot: https://github.com/Hannessa/python-sc2-ladderbot -------------------------------------------------------------------------------- /CreepyBot/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import argparse 3 | 4 | import sys 5 | import asyncio 6 | import logging 7 | 8 | import sc2 9 | from sc2 import Race, Difficulty 10 | from sc2.player import Bot, Computer 11 | 12 | from sc2.sc2process import SC2Process 13 | from sc2.client import Client 14 | 15 | # Run ladder game 16 | # This lets python-sc2 connect to a LadderManager game: https://github.com/Cryptyc/Sc2LadderServer 17 | # Based on: https://github.com/Dentosal/python-sc2/blob/master/examples/run_external.py 18 | def run_ladder_game(bot): 19 | # Load command line arguments 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument('--GamePort', type=int, nargs="?", help='Game port') 22 | parser.add_argument('--StartPort', type=int, nargs="?", help='Start port') 23 | parser.add_argument('--LadderServer', type=str, nargs="?", help='Ladder server') 24 | parser.add_argument('--ComputerOpponent', type=str, nargs="?", help='Computer opponent') 25 | parser.add_argument('--ComputerRace', type=str, nargs="?", help='Computer race') 26 | parser.add_argument('--ComputerDifficulty', type=str, nargs="?", help='Computer difficulty') 27 | args, unknown = parser.parse_known_args() 28 | 29 | if args.LadderServer == None: 30 | host = "127.0.0.1" 31 | else: 32 | host = args.LadderServer 33 | 34 | host_port = args.GamePort 35 | lan_port = args.StartPort 36 | 37 | # Versus Computer doesn't work yet 38 | computer_opponent = False 39 | if args.ComputerOpponent: 40 | computer_opponent = True 41 | computer_race = args.ComputerRace 42 | computer_difficulty = args.ComputerDifficulty 43 | 44 | # Port config 45 | ports = [lan_port + p for p in range(1,6)] 46 | 47 | portconfig = sc2.portconfig.Portconfig() 48 | portconfig.shared = ports[0] # Not used 49 | portconfig.server = [ports[1], ports[2]] 50 | portconfig.players = [[ports[3], ports[4]]] 51 | 52 | # Join ladder game 53 | g = join_ladder_game( 54 | host=host, 55 | port=host_port, 56 | players=[bot], 57 | realtime=False, 58 | portconfig=portconfig 59 | ) 60 | 61 | # Run it 62 | result = asyncio.get_event_loop().run_until_complete(g) 63 | print(result) 64 | 65 | # Modified version of sc2.main._join_game to allow custom host and port. 66 | async def join_ladder_game(host, port, players, realtime, portconfig, save_replay_as=None, step_time_limit=None, game_time_limit=None): 67 | async with SC2Process(host=host, port=port) as server: 68 | await server.ping() 69 | client = Client(server._ws) 70 | 71 | try: 72 | result = await sc2.main._play_game(players[0], client, realtime, portconfig, step_time_limit, game_time_limit) 73 | if save_replay_as is not None: 74 | await client.save_replay(save_replay_as) 75 | await client.leave() 76 | await client.quit() 77 | except ConnectionAlreadyClosed: 78 | logging.error(f"Connection was closed before the game ended") 79 | return None 80 | 81 | return result 82 | -------------------------------------------------------------------------------- /CreepyBot/example_bot.py: -------------------------------------------------------------------------------- 1 | import sc2 2 | 3 | class ExampleBot(sc2.BotAI): 4 | async def on_step(self, iteration): 5 | # On first step, send all workers to attack enemy start location 6 | if iteration == 0: 7 | for worker in self.workers: 8 | await self.do(worker.attack(self.enemy_start_locations[0])) 9 | -------------------------------------------------------------------------------- /CreepyBot/run.py: -------------------------------------------------------------------------------- 1 | import sc2, sys 2 | from __init__ import run_ladder_game 3 | from sc2 import Race, Difficulty 4 | from sc2.player import Bot, Computer 5 | 6 | # Load bot 7 | from CreepyBot import CreepyBot 8 | bot = Bot(Race.Zerg, CreepyBot()) 9 | 10 | # Start game 11 | if __name__ == '__main__': 12 | if "--LadderServer" in sys.argv: 13 | # Ladder game started by LadderManager 14 | print("Starting ladder game...") 15 | run_ladder_game(bot) 16 | else: 17 | # Local game 18 | print("Starting local game...") 19 | sc2.run_game(sc2.maps.get("Abyssal Reef LE"), [ 20 | bot, 21 | Computer(Race.Protoss, Difficulty.VeryHard) 22 | ], realtime=True) 23 | -------------------------------------------------------------------------------- /DragonBot/burny_basic_ai.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic bot layer made by Burny 3 | Bot layer version: 0.1 4 | Date of Upload: 2018-06-22 5 | 6 | Started working on this basic desgin on 2018-06-18 7 | """ 8 | 9 | # pylint: disable=E0602,E1102 10 | 11 | 12 | 13 | 14 | import random, json, time, math 15 | # import re, os 16 | # download maps from https://github.com/Blizzard/s2client-proto#map-packs 17 | 18 | import sc2 # pip install sc2 19 | from sc2.data import race_gas, race_worker, race_townhalls, ActionResult, Attribute, Race 20 | from sc2 import Race, Difficulty 21 | from sc2.constants import * # for autocomplete 22 | # from sc2.ids.unit_typeid import * 23 | # from sc2.ids.ability_id import * 24 | import sc2.ids.unit_typeid 25 | import sc2.ids.ability_id 26 | import sc2.ids.buff_id 27 | import sc2.ids.upgrade_id 28 | import sc2.ids.effect_id 29 | 30 | from sc2.unit import Unit 31 | from sc2.units import Units 32 | from sc2.position import Point2, Point3 33 | 34 | from sc2 import Race, Difficulty 35 | from sc2.constants import * 36 | from sc2.player import Bot, Computer, Human 37 | 38 | class BehaviorManager(object): 39 | def __init__(self, unit): 40 | assert isinstance(unit, (Unit, int)) 41 | if isinstance(unit, Unit): 42 | self.unitTag = unit.tag 43 | else: 44 | self.unitTag = unit 45 | 46 | self.removeCondition = lambda other: other.units.find_by_tag(self.unitTag) is None 47 | 48 | self.priorityTypes = None # units that should be focussed first of all units in attackrange, e.g. [SCV, MULE, DRONE, PROBE] 49 | self.priorityMode = "lowhp" # "lowhp" = attack units with lowest hp first 50 | # "closest" = attack units closest to this unit first 51 | # probably best to have high fire rate (or no projectile) units attack lowest hp and others attack closest 52 | 53 | self.attackRange = None # distance it will be scanned for enemies to attack # TODO: set this automatically to furthest weapon range, e.g. thor = 10, tempest = 15, marine = 5, zergling = i dont know 54 | 55 | self.ignoreTypes = None # units that should be ignored, e.g. banshee should ignore [ZERGLING, HATCHERY, LARVA, EGG] 56 | 57 | self.avoidTypes = None # units that should be avoided, e.g. banshee should avoid [SPORECRAWLER, PHOTONCANNON, MISSILETURRET] 58 | self.avoidRange = 9 # e.g. marine should avoid baneling and run away from it first, only then split # TODO: look up max vision range of detectors 59 | 60 | self.kitingRange = None # if any enemy is closer than this range, start moving back 61 | 62 | self.splitAllyTypes = None # list of unit types that should be kept distance to 63 | self.splitEnemyTypes = None # start splitting when enemy of this type is in range (parameter below) to this unit 64 | self.splitWhenEnemyInRangeOf = 4 # only start splitting when an enemy unit of splitTypes is in range, e.g. start splitting when a tank is in range 13 of this unit # TODO: always split when this is set to None, use 0 when you dont want splitting 65 | self.splitDistance = 2 # keep distance to other units from splitTypes TODO: look up splash of tanks, banes 66 | 67 | self.attackLocations = None # go to closest location if is not None 68 | self.attackCondition = None # only attack when condition is met 69 | 70 | self.attackRandomLocations = None # go to random location to find targets 71 | self.attackRandomCondition = None # only start going to location when condition is met 72 | 73 | self.scoutLocations = None # start scouting any of these locations when scoutCondition is met 74 | self.scoutCondition = None 75 | 76 | self.retreatLocations = None # retreats to closest location when retreatCondition is met 77 | self.retreatCondition = None 78 | 79 | # self.idleCondition = None # when to set status to idle? 80 | 81 | self.status = "idle" # vary between ["idle", "retreating", "priotizing", "attacking", "defending", "kiting", "splitting", "avoiding", "casting", "scouting"] 82 | 83 | self.timeLastKiteSplitAvoidIssued = 0 84 | self.kiteSplitAvoidInterval = 0.001 # how long the bot waits until it issues a new kite/split/avoid command 85 | 86 | self.timeLastAttackIssued = 0 87 | self.attackRate = 0.61 # TODO: set this automatically to shooting cooldown 88 | self.attackDelay = 0.1 # leave it 100 ms time to execute the attack before issueing a new command 89 | 90 | self.timeLastCommandIssued = 0 # set this to in-game time when the last command was issued 91 | self.issueCommandInterval = 0.1 # how long the bot waits until it issues this unit a new command # TODO: automatically set this to the shooting cooldown 92 | 93 | # TODO: use abilities when conditions are met 94 | self.abilityCondition1 = None 95 | self.abilityCondition2 = None 96 | self.abilityCondition3 = None 97 | 98 | # parameters 99 | self.attackRangeFix = 0.25 100 | 101 | # variables managed by class / instance: 102 | self.currentTarget = None 103 | self.actionsThisIteration = [] 104 | 105 | async def update(self, bot): 106 | self.actionsThisIteration = [] 107 | gameTime = bot.getTimeInSeconds() 108 | # if self.timeLastCommandIssued + self.issueCommandInterval > gameTime or self.timeLastAttackIssued + self.attackDelay > gameTime: 109 | if self.timeLastCommandIssued + self.issueCommandInterval > gameTime: 110 | return # trying to issue commands too fast 111 | unit = bot.units.find_by_tag(self.unitTag) 112 | # if self.removeCondition(bot): 113 | if not unit: 114 | print("bot is trying to update a dead unit") 115 | return # unit is dead 116 | issuedCommand = False 117 | 118 | """ unit behavior priority 119 | - set status to idle if idle condition met 120 | - (use ability if we have energy and ability off cooldown and ability condition is met) 121 | - retreat if we are not retreating and condition is met (or only condition is met?) 122 | - kite if conditions met 123 | - split if conditions met 124 | - avoid units in avoidTypes 125 | - attack priority targets if available 126 | - attack any other targets in range if not in ignoreTypes 127 | - move to random target from attackLocations if attackcondition is met, the timeLastIssued will prevent spamming too hard 128 | - move to random scoutlocation if scout condition is met 129 | - else do nothing 130 | """ 131 | 132 | 133 | 134 | if self.status == "movingToTarget": 135 | if unit.distance_to(self.currentTarget) < 2: 136 | self.status = "idle" 137 | elif unit.is_idle: 138 | self.status = "idle" 139 | 140 | # TODO: ability usage 141 | 142 | if self.status != "retreating" \ 143 | and self.retreatLocations \ 144 | and self.retreatCondition \ 145 | and self.retreatLocations(bot) \ 146 | and self.retreatCondition(bot): 147 | 148 | retreatLocation = random.choice(self.retreatLocations(bot)) 149 | # TODO: change this so it moves away from units that can attack it first, then move to retreat location 150 | self.status = "retreating" 151 | self.timeLastCommandIssued = gameTime 152 | self.actionsThisIteration.append(unit.move(retreatLocation)) 153 | issuedCommand = True 154 | return 155 | 156 | if unit.weapon_cooldown != 0:# and self.timeLastKiteSplitAvoidIssued + self.kiteSplitAvoidInterval < gameTime: 157 | if self.kitingRange \ 158 | and bot.known_enemy_units.not_structure.closer_than(self.kitingRange, unit).filter(lambda u: not u.is_snapshot).exists: 159 | 160 | self.status = "kiting" 161 | closeEnemies = bot.known_enemy_units.not_structure.closer_than(self.kitingRange, unit).filter(lambda u: not u.is_snapshot) 162 | closestEnemy = closeEnemies.closest_to(unit) 163 | distClosestEnemy = unit.distance_to(closestEnemy) 164 | distToMaxRange = round(self.attackRange - distClosestEnemy, 3) 165 | distToMaxRange = max(1, min(distToMaxRange, self.attackRange)) 166 | locations = bot.getPointsAroundUnit(unit, minDistance=distToMaxRange/4, maxDistance=distToMaxRange, stepSize=distToMaxRange/4, pointsPerCircle=32) 167 | terrainHeightAtUnit = bot.getTerrainHeight(unit.position.to2) 168 | locationsFiltered = {p for p in locations if bot.inPathingGrid(p) and (unit.distance_to(p) < 2.5 or abs(bot.getTerrainHeight(p) - terrainHeightAtUnit) < 10)} 169 | if locationsFiltered: 170 | self.timeLastCommandIssued = gameTime 171 | self.timeLastKiteSplitAvoidIssued = gameTime 172 | kiteLocation = closestEnemy.position.furthest(locationsFiltered) 173 | issuedCommand = True 174 | self.actionsThisIteration.append(unit.move(kiteLocation)) 175 | 176 | elif self.status != "splitting" \ 177 | and (self.splitWhenEnemyInRangeOf is None \ 178 | or self.splitWhenEnemyInRangeOf > 0 \ 179 | and self.splitEnemyTypes \ 180 | and bot.known_enemy_units.of_type(self.splitEnemyTypes).closer_than(self.splitWhenEnemyInRangeOf, unit).exists) \ 181 | and self.splitAllyTypes \ 182 | and bot.units.tags_not_in([unitTag]).of_type(self.splitAllyTypes).closer_than(self.splitDistance, unit): 183 | 184 | self.status = "splitting" 185 | self.timeLastCommandIssued = gameTime 186 | self.timeLastKiteSplitAvoidIssued = gameTime 187 | locations = bot.getPointsAroundUnit(unit, minDistance=0.5, maxDistance=1, stepSize=0.5, pointsPerCircle=16) 188 | locationsFiltered = {p for p in locations if bot.inPathingGrid(p)} 189 | if locationsFiltered: 190 | closeAllies = bot.units.tags_not_in([unitTag]).of_type(self.splitAllyTypes).closer_than(self.splitDistance, unit) 191 | closestAlliedUnit = closeAllies.closest_to(unit) 192 | splitLocation = closestAlliedUnit.position.furthest(locationsFiltered) 193 | issuedCommand = True 194 | self.actionsThisIteration.append(unit.move(splitLocation)) 195 | 196 | elif self.status != "avoiding" \ 197 | and self.avoidTypes \ 198 | and self.avoidRange \ 199 | and bot.known_enemy_units.of_type(self.avoidTypes).closer_than(self.avoidRange, unit).exists: 200 | 201 | self.status = "avoiding" 202 | self.timeLastCommandIssued = gameTime 203 | self.timeLastKiteSplitAvoidIssued = gameTime 204 | locations = bot.getPointsAroundUnit(unit, minDistance=3, maxDistance=5, stepSize=1, pointsPerCircle=8) 205 | locationsFiltered = {p for p in locations if bot.inPathingGrid(p)} 206 | if locationsFiltered: 207 | closeEnemies = bot.known_enemy_units.of_type(self.avoidTypes).closer_than(self.avoidRange, unit) 208 | enemiesCenter = closeEnemies.center 209 | avoidLocation = enemiesCenter.furthest(locationsFiltered) 210 | issuedCommand = True 211 | self.actionsThisIteration.append(unit.move(avoidLocation)) 212 | 213 | if unit.weapon_cooldown == 0: 214 | # priority attacks 215 | if self.priorityTypes \ 216 | and self.attackRange \ 217 | and bot.known_enemy_units.of_type(self.priorityTypes).closer_than(self.attackRange + self.attackRangeFix, unit).exists: 218 | 219 | priotizeUnits = bot.known_enemy_units.of_type(self.priorityTypes).closer_than(self.attackRange + self.attackRangeFix, unit) 220 | if self.priorityMode == "lowhp": 221 | priotizeUnits = priotizeUnits.sorted(lambda u: u.health_percentage, reverse=True) 222 | elif self.priorityMode == "closest": 223 | priotizeUnits = priotizeUnits.sorted(lambda u: u.distance_to(unit), reverse=True) 224 | 225 | if not issuedCommand:# and self.timeLastAttackIssued + self.attackRate < gameTime: 226 | self.status = "priotizing" 227 | self.timeLastCommandIssued = gameTime 228 | self.timeLastAttackIssued = gameTime 229 | issuedCommand = True 230 | target = priotizeUnits.pop() # it will use the last target in list, thats why i used reverse=True 231 | self.actionsThisIteration.append(unit.attack(target)) 232 | 233 | for i in range(priotizeUnits.amount): 234 | if i > 3 or i+1 == priotizeUnits.amount: 235 | self.actionsThisIteration.append(unit.attack(priotizeUnits[-(i+1)].position, queue=True)) 236 | break 237 | self.actionsThisIteration.append(unit.attack(priotizeUnits[-(i+1)], queue=True)) 238 | return 239 | 240 | # attack units in range 241 | if self.attackRange \ 242 | and (self.ignoreTypes 243 | and bot.known_enemy_units.exclude_type(self.ignoreTypes).closer_than(self.attackRange + self.attackRangeFix, unit).exists 244 | or bot.known_enemy_units.closer_than(self.attackRange + self.attackRangeFix, unit).exists): 245 | # or bot.known_enemy_units.closer_than(self.attackRange + self.attackRangeFix, unit).exists): 246 | 247 | if self.ignoreTypes: 248 | targetUnits = bot.known_enemy_units.exclude_type(self.ignoreTypes).closer_than(self.attackRange + self.attackRangeFix, unit) 249 | else: 250 | targetUnits = bot.known_enemy_units.closer_than(self.attackRange + self.attackRangeFix, unit) 251 | 252 | if self.priorityMode == "lowhp": 253 | targetUnits = targetUnits.sorted(lambda u: u.health_percentage, reverse=True) 254 | elif self.priorityMode == "closest": 255 | targetUnits = targetUnits.sorted(lambda u: u.distance_to(unit), reverse=True) 256 | 257 | # targetUnits = targetUnits.sorted(lambda u: u.distance_to(unit), reverse=True) # sort by closest 258 | 259 | if targetUnits: # sometimes it is empty and i dont know why 260 | if not issuedCommand:# and self.timeLastAttackIssued + self.attackRate < gameTime: 261 | self.status = "attacking" 262 | self.timeLastCommandIssued = gameTime 263 | self.timeLastAttackIssued = gameTime 264 | issuedCommand = True 265 | target = targetUnits.pop() # it will use the last target in list, thats why i used reverse=True 266 | if target.is_structure: 267 | self.actionsThisIteration.append(unit.attack(target.position)) 268 | else: 269 | self.actionsThisIteration.append(unit.attack(target)) 270 | 271 | for i in range(targetUnits.amount): 272 | if i > 3 or i+1 == targetUnits.amount: 273 | self.actionsThisIteration.append(unit.attack(targetUnits[-(i+1)].position, queue=True)) 274 | break 275 | self.actionsThisIteration.append(unit.attack(targetUnits[-(i+1)], queue=True)) 276 | return 277 | 278 | if unit.weapon_cooldown == 0 and self.status != "movingToTarget": 279 | if self.attackLocations \ 280 | and self.attackCondition \ 281 | and self.attackLocations(bot) \ 282 | and self.attackCondition(bot): 283 | 284 | self.status = "movingToTarget" 285 | self.timeLastCommandIssued = gameTime 286 | issuedCommand = True 287 | self.currentTarget = unit.position.closest(self.attackLocations(bot)).position.to2 288 | self.actionsThisIteration.append(unit.move(self.currentTarget)) 289 | return 290 | 291 | if self.attackRandomLocations \ 292 | and self.attackRandomCondition \ 293 | and self.attackRandomLocations(bot) \ 294 | and self.attackRandomCondition(bot): 295 | 296 | self.status = "movingToTarget" 297 | self.timeLastCommandIssued = gameTime 298 | issuedCommand = True 299 | self.currentTarget = random.choice(list(self.attackRandomLocations(bot))).position.to2 300 | self.actionsThisIteration.append(unit.move(self.currentTarget)) 301 | return 302 | 303 | if self.scoutLocations \ 304 | and self.scoutCondition \ 305 | and self.scoutLocations(bot) \ 306 | and self.scoutCondition(bot): 307 | 308 | self.status = "movingToTarget" 309 | self.timeLastCommandIssued = gameTime 310 | issuedCommand = True 311 | self.currentTarget = random.choice(list(self.scoutLocations(bot))).position.to2 312 | self.actionsThisIteration.append(unit.move(self.currentTarget)) 313 | return 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | class BurnyBasicAI(sc2.BotAI): 347 | def __init__(self): 348 | self.allowedHandleIdleWorkers = True 349 | self.allowedHandleLongDistanceMining = True 350 | self.allowedBalanceSaturation = True 351 | self.allowedHandleMules = True 352 | self.allowedSplitWorkers = True 353 | self.allowedSetRallyPoints = True 354 | self.allowedHandleDepots = True 355 | self.allowedHandleRepairs = True 356 | self.allowedBuildingCancelMicro = True 357 | self.allowedFlyingCoommandCenters = True 358 | 359 | self.baseToMineralsCenterDistance = 10 # set this to lower later so we can detect misplaced commandcenters 360 | 361 | self.microActions = [] 362 | self.macroActions = [] 363 | 364 | self.expandCondition = lambda other: \ 365 | any([ 366 | # either we are in late game, then count mineral fields near townhalls 367 | all([ 368 | other.townhalls, 369 | other.getTimeInSeconds() > 10*60, 370 | other.state.mineral_field.amount > 0, 371 | 372 | sum([other.state.mineral_field.closer_than(10, x).amount for x in other.townhalls]) 373 | >= 2 * (other.workers.amount + other.geysers.filter(lambda g: g.has_vespene).amount) 374 | - 3 * other.geysers.filter(lambda g: g.has_vespene).amount, 375 | 376 | sum(other.already_pending(x) for x in [COMMANDCENTER, NEXUS, HATCHERY]) < 1, 377 | other.units.of_type([COMMANDCENTER, NEXUS, HATCHERY]).not_ready.amount < 1, 378 | ]), 379 | # or early game (count gas too because workers in gas are not part of self.workers) 380 | all([ 381 | other.townhalls, 382 | other.getTimeInSeconds() <= 10*60, 383 | # any([ 384 | # hasattr(other, "buildExpansionDict") and not other.buildExpansionDict, 385 | # hasattr(other, "buildExpansionDict") and other.buildExpansionDict and any([other.can_afford(x) for x in race_townhalls[other.race]]), 386 | # ]), 387 | other.workers.amount + other.already_pending(race_worker[other.race]) 388 | >= 18 + 1 * int(other.race == Race.Terran) + 2 * int(other.race == Race.Protoss), 389 | 390 | other.workers.amount + other.geysers.ready.filter(lambda g: g.has_vespene).amount + other.already_pending(race_worker[other.race]) + 8 >= 16 * other.townhalls.amount + 3 * other.geysers.ready.filter(lambda g: g.has_vespene).amount, # the +8 acts as a parameter here, can vary from 0 to +16 391 | sum(other.already_pending(x) for x in [COMMANDCENTER, NEXUS, HATCHERY]) < 1, 392 | # other.units.of_type([COMMANDCENTER, NEXUS, HATCHERY]).not_ready.amount < 1, # TODO: comment out if you only want to expand when not making a main building already 393 | ]) 394 | ]) 395 | 396 | self.makeWorkersCondition = lambda other: \ 397 | all([ 398 | other.supply_left > 0, 399 | other.supply_used < 198, 400 | any([ 401 | all([ 402 | self.getTimeInSeconds() > 25*60, 403 | other.workers.amount + other.geysers.ready.filter(lambda g: g.has_vespene).amount + other.already_pending(race_worker[other.race]) < 20 + 14 * int(other.race == Race.Zerg), # TODO: change back to 66 404 | ]), 405 | all([ 406 | self.getTimeInSeconds() > 15*60, 407 | other.workers.amount + other.geysers.ready.filter(lambda g: g.has_vespene).amount + other.already_pending(race_worker[other.race]) < 50 + 14 * int(other.race == Race.Zerg), # TODO: change back to 66 408 | ]), 409 | all([ 410 | self.getTimeInSeconds() <= 15*60, 411 | other.workers.amount + other.geysers.ready.filter(lambda g: g.has_vespene).amount + other.already_pending(race_worker[other.race]) < 100 + 14 * int(other.race == Race.Zerg), # TODO: change back to 66 412 | ]) 413 | ]), 414 | any([ 415 | other.race != Race.Zerg 416 | and (other.townhalls.ready.idle.exists or other.townhalls.ready.filter(lambda x: x.is_idle or len(x.orders) == 1 and x.orders[0].progress > 0.75).exists), # the "progress" is a fix so that an scv is always in the queue 417 | other.race == Race.Zerg 418 | and other.units(LARVA).exists 419 | ]) 420 | ]) 421 | 422 | self.timeLastGasTaken = 0 423 | self.takeGasCondition = lambda other: \ 424 | all([ 425 | other.supply_used >= 16 + 2 * int(other.race == Race.Protoss) + 4 * int(other.race == Race.Zerg), 426 | other.timeLastGasTaken > 30 - 20 * int(other.race == Race.Zerg), 427 | other.already_pending(race_gas[other.race]) < 1, 428 | other.geysers.filter(lambda g: g.has_vespene).amount <= 6 + 2 * int(other.race != Race.Terran), 429 | self.state.vespene_geyser.filter(lambda x: x.has_vespene).closer_than(10, self.townhalls.filter(lambda x: x.build_progress > 0.6).random).exists 430 | ]) 431 | 432 | productionBuildings = {BARRACKS, GATEWAY, WARPGATE, HATCHERY, LAIR, HIVE} # 1 supply per 18 sec 433 | productionBuildings2 = {FACTORY, STARPORT, ROBOTICSFACILITY, STARGATE} # 2 supply per 18 sec 434 | supplyGiving = {SUPPLYDEPOT, SUPPLYDEPOTLOWERED, OVERLORD, PYLON} # units giving 8 supply, dont add supplyDrop here 435 | self.lowSupplyLeftCondition = lambda other: \ 436 | all([ 437 | 8 * sum(other.already_pending(x) for x in supplyGiving) 438 | + 8 * other.units.of_type(supplyGiving).not_ready.amount 439 | + other.supply_cap 440 | < 200, 441 | other.townhalls.exists, 442 | other.supply_used >= 13 + int(other.race != Race.Zerg), 443 | 444 | (other.units.of_type(set(race_townhalls[other.race]) | productionBuildings).filter(lambda u: u.build_progress > 0.6).amount 445 | + 2 * other.units.of_type(productionBuildings2).filter(lambda u: u.build_progress > 0.6).amount 446 | # - 8 * sum(other.already_pending(x) for x in supplyGiving) 447 | - 8 * other.units.of_type(supplyGiving).not_ready.amount # in construction / overlords training 448 | - 15 * other.units.of_type([COMMANDCENTER, NEXUS, HATCHERY]).not_ready.filter(lambda u: u.build_progress > 0.8).amount / (1 + 3 * int(other.race == Race.Zerg))) 449 | # - 15 * sum(other.already_pending(x) for x in [COMMANDCENTER, NEXUS, HATCHERY]) / (5 * (1 + 3 * int(other.race == Race.Zerg))) 450 | 451 | * ((19 + 3 * int(other.race == Race.Terran)) / 18) # ideally should be 21 / 18 for terran because of 18 marine build time and 21 depot build time 452 | > other.supply_left, # because terran for some reason has 3 sec longer build time on depots -> build depots earlier by fraction 453 | 454 | sum(other.already_pending(x) for x in supplyGiving) < 1 455 | ]) 456 | 457 | async def firstIterationInit(self): 458 | self.splitWorkers() 459 | 460 | async def on_step(self, iteration): 461 | if iteration == 0: 462 | await self.firstIterationInit() 463 | self.handleIdleWorkers() 464 | if iteration % 50 == 0: # save performance 465 | await self.distribute_workers() 466 | if (iteration + 25) % 50 == 0: 467 | self.handleLongDistanceMining() 468 | if iteration % 10 == 0: # save performance 469 | await self.distribute_workers(onlySaturateGas=True) 470 | self.handleMules() # terran 471 | self.setWorkerRallyPoint() 472 | self.handleDepots() # terran 473 | self.handleRepair() # terran 474 | self.handleBuildingCancelMicro() 475 | # self.handleMisplacedCommandCenter() 476 | 477 | 478 | 479 | 480 | 481 | async def step_end(self, iteration): 482 | if self.microActions: 483 | await self.do_actions(self.microActions) 484 | if self.macroActions: 485 | await self.do_actions(self.macroActions) 486 | 487 | def getTerrainHeight(self, pos): 488 | # returns terrain height at pos, good for walling (at chokes and ramps) and to find out where the main base ends 489 | assert isinstance(pos, (Point2, Point3, Unit)) 490 | pos = pos.position.to2.rounded 491 | return self._game_info.terrain_height[(pos)] # returns int 492 | 493 | def inPlacementGrid(self, pos): 494 | # returns True if it is possible to build a structure at pos 495 | assert isinstance(pos, (Point2, Point3, Unit)) 496 | pos = pos.position.to2.rounded 497 | return self._game_info.placement_grid[(pos)] == 0 498 | 499 | def inPathingGrid(self, pos): 500 | # returns True if it is possible for a ground unit to move to pos 501 | assert isinstance(pos, (Point2, Point3, Unit)) 502 | pos = pos.position.to2.rounded 503 | return self._game_info.pathing_grid[(pos)] != 0 504 | 505 | def isVisible(self, pos): 506 | # returns True if the area at pos is visible 507 | assert isinstance(pos, (Point2, Point3, Unit)) 508 | pos = pos.position.to2.rounded 509 | return self.state.visibility[(pos)] #== 0 # TODO: have to talk to dentosal how to fix 1 and 0 values 510 | 511 | def hasCreep(self, pos): 512 | # returns True if there is creep at position 513 | assert isinstance(pos, (Point2, Point3, Unit)) 514 | pos = pos.position.to2.rounded 515 | return self.state.creep[(pos)] #== 1 # TODO: have to talk to dentosal how to fix 1 and 0 values 516 | 517 | def getTimeInSeconds(self): 518 | # returns real time if game is played on "faster" 519 | return self.state.game_loop * 0.725 * (1/16) 520 | 521 | def getPointsAroundUnit(self, unit, minDistance=10, maxDistance=10, stepSize=1, pointsPerCircle=8): 522 | # e.g. locationAmount=4 would only consider 4 points: north, west, east, south 523 | assert isinstance(unit, (Unit, Point2, Point3)) 524 | loc = unit.position.to2 525 | # minDistance = max(1, round(minDistance)) 526 | # maxDistance = max(1, minDistance, round(maxDistance)) 527 | stepSize = max(0.001, stepSize) 528 | pointsPerCircle = max(1, round(pointsPerCircle)) 529 | positions = [] 530 | distance = minDistance 531 | while distance <= maxDistance: 532 | positions += [Point2(( \ 533 | loc.x + distance * math.cos(math.pi * 2 * alpha / pointsPerCircle), \ 534 | loc.y + distance * math.sin(math.pi * 2 * alpha / pointsPerCircle))) \ 535 | for alpha in range(pointsPerCircle)] 536 | distance += stepSize 537 | # positions = [Point2(( \ 538 | # loc.x + distance * math.cos(math.pi * 2 * alpha / pointsPerCircle), \ 539 | # loc.y + distance * math.sin(math.pi * 2 * alpha / pointsPerCircle))) \ 540 | # for alpha in range(pointsPerCircle) # alpha is the angle 541 | # for distance in range(minDistance, maxDistance+1, stepSize)] 542 | return positions 543 | 544 | @property 545 | def getMapCenter(self): 546 | return self._game_info.map_center 547 | 548 | @property 549 | def getPlayableArea(self): 550 | return self._game_info.playable_area # returns x0, y0, x1, y1 coordinates of the playable area 551 | 552 | @property 553 | def enemyRace(self): 554 | self.enemy_id = 3 - self.player_id 555 | return Race(self._game_info.player_races[self.enemy_id]) 556 | 557 | # async def getEnemyRace(self): # only works in 1 vs 1 558 | # players = (await self._client.get_game_info()).players 559 | # myPlayerId = next((x.id for x in players if x.race == self.getUnitInfo(self.units.random, "race")), 1) 560 | # enemyId = 3 - myPlayerId # id is 1 or 2 for the players, so the sum is 3 561 | # enemyRace = players[enemyId - 1].race 562 | # # make a dictionary with 563 | # # races = {Race.Terran.Value: "t"} # or "terran" and so forth 564 | # # => races[enemyRace] returns string of race then 565 | # return enemyRace 566 | 567 | async def getRampPoints(self, returnOneSet=True): 568 | ramps = (await self._client.get_game_info()).map_ramps # list 569 | if returnOneSet: 570 | allRampPoints = set().union(*[ramp._points for ramp in ramps]) # a set of tuples 571 | return allRampPoints 572 | else: 573 | rampsWithPoints = [ramp._points for ramp in ramps] # a list of sets of tuples 574 | return rampsWithPoints # each list entry is one ramp, each ramp has multiple points as tuples 575 | 576 | # TODO: from the score_pb2.py, get collection rate minerals and vespene and other score data (but the latter is optional) 577 | 578 | ################################ 579 | ######### OVERWRITING DEFAULT FUNCTIONS 580 | ################################ 581 | 582 | async def find_placement(self, building, near, max_distance=20, random_alternative=False, placement_step=3, min_distance=0, minDistanceToResources=3): 583 | """Finds a placement location for building.""" 584 | 585 | assert isinstance(building, (AbilityId, UnitTypeId)) 586 | assert isinstance(near, (Point2, Point3, Unit)) 587 | near = near.position.to2 588 | 589 | if isinstance(building, UnitTypeId): 590 | building = self._game_data.units[building.value].creation_ability 591 | else: # AbilityId 592 | building = self._game_data.abilities[building.value] 593 | 594 | if await self.can_place(building, near): 595 | return near 596 | 597 | for distance in range(min_distance, max_distance, placement_step): 598 | possible_positions = [Point2(p).offset(near).to2 for p in ( 599 | [(dx, -distance) for dx in range(-distance, distance+1, placement_step)] + 600 | [(dx, distance) for dx in range(-distance, distance+1, placement_step)] + 601 | [(-distance, dy) for dy in range(-distance, distance+1, placement_step)] + 602 | [( distance, dy) for dy in range(-distance, distance+1, placement_step)] 603 | )] 604 | if (self.townhalls | self.state.mineral_field | self.state.vespene_geyser).exists and minDistanceToResources > 0: 605 | possible_positions = [x for x in possible_positions if (self.state.mineral_field | self.state.vespene_geyser).closest_to(x).distance_to(x) >= minDistanceToResources] # filter out results that are too close to resources 606 | 607 | res = await self._client.query_building_placement(building, possible_positions) 608 | possible = [p for r, p in zip(res, possible_positions) if r == ActionResult.Success] 609 | if not possible: 610 | continue 611 | 612 | if random_alternative: 613 | return random.choice(possible) 614 | else: 615 | return min(possible, key=lambda p: p.distance_to(near)) 616 | return None 617 | 618 | async def basicBuild(self, buildingType, location=None, worker=None, buildAsWall=False, expandOnLocation=True): 619 | if not hasattr(self, "buildExpansionDict"): 620 | self.buildExpansionDict = {} 621 | if self.buildExpansionDict: 622 | wTag = list(self.buildExpansionDict.keys())[0] 623 | if self.buildExpansionDict[wTag]["expireTime"] < self.getTimeInSeconds(): 624 | self.buildExpansionDict.pop(wTag) 625 | 626 | townhallTypes = [NEXUS, HATCHERY, COMMANDCENTER] 627 | 628 | if buildAsWall: 629 | pass 630 | elif buildingType in townhallTypes: 631 | # is townhall, build it on expansion 632 | if not self.buildExpansionDict: # if dict empty 633 | if not expandOnLocation and not self.can_afford(buildingType): 634 | return 635 | 636 | if expandOnLocation: 637 | if not location: 638 | location = await self.get_next_expansion() 639 | if location: 640 | 641 | loc = await self.find_placement(buildingType, near=location, random_alternative=False, minDistanceToResources=5, placement_step=1) 642 | w = worker or self.selectWorker(loc) 643 | if w: 644 | # print("adding worker to dict", w.tag, loc, buildingType) 645 | self.buildExpansionDict[w.tag] = { 646 | "location": loc, 647 | "buildingType": buildingType, 648 | "expireTime": self.getTimeInSeconds() + 30} 649 | # print(self.buildExpansionDict) 650 | action = w.move(loc) 651 | self.macroActions.append(action) 652 | else: 653 | print("SHOULD NEVER GET HERE - NOT IMPLEMENTED") 654 | # build expansion (command center) closest to worker and fly it to expansion afterwards 655 | pass 656 | else: 657 | # print("retrieving worker from dict") 658 | wTag = list(self.buildExpansionDict.keys())[0] 659 | w = worker or self.units.find_by_tag(wTag) 660 | expandInfo = self.buildExpansionDict[wTag] 661 | buildingType = expandInfo["buildingType"] 662 | if w and self.can_afford(buildingType): 663 | # print("removing entry from dict") 664 | expandInfo = self.buildExpansionDict.pop(wTag) 665 | loc = expandInfo["location"] 666 | action = w.build(buildingType, loc) 667 | self.macroActions.append(action) 668 | 669 | else: 670 | # is normal building that can be built anywhere where there is room 671 | if not location and self.townhalls.ready.exists: 672 | location = self.townhalls.ready.random 673 | 674 | loc = await self.find_placement(buildingType, near=location, random_alternative=False, minDistanceToResources=3, placement_step=1) 675 | w = worker or self.selectWorker(loc) 676 | if w and loc: 677 | action = w.build(loc) 678 | self.macroActions.append(action) 679 | 680 | # select workers that are mining minerals or are idle 681 | def selectWorker(self, pos=None, excludeTags=None): 682 | if not hasattr(self, "recentlySelectedWorkers"): 683 | self.recentlySelectedWorkers = {} 684 | for key in list(self.recentlySelectedWorkers.keys()): # remove expired items 685 | value = self.recentlySelectedWorkers[key] 686 | if value["expireTime"] < self.getTimeInSeconds(): 687 | self.recentlySelectedWorkers.pop(key) 688 | 689 | w = None 690 | ws = None 691 | if excludeTags: 692 | ws = self.workers.gathering.tags_not_in(excludeTags).tags_not_in(self.recentlySelectedWorkers) 693 | if not ws: 694 | ws = self.workers.idle.tags_not_in(excludeTags).tags_not_in(self.recentlySelectedWorkers) 695 | elif not ws: 696 | ws = self.workers.gathering.tags_not_in(self.recentlySelectedWorkers) 697 | if not ws: 698 | ws = self.workers.idle.tags_not_in(self.recentlySelectedWorkers) 699 | if ws: 700 | if pos: 701 | w = ws.closest_to(pos) 702 | else: 703 | w = ws.random 704 | if w: 705 | self.recentlySelectedWorkers[w.tag] = {"expireTime": self.getTimeInSeconds() + 0.5} 706 | return w 707 | 708 | async def distribute_workers(self, performanceHeavy=True, onlySaturateGas=False): 709 | # expansion_locations = self.expansion_locations 710 | # owned_expansions = self.owned_expansions 711 | 712 | 713 | mineralTags = [x.tag for x in self.state.units.mineral_field] 714 | # gasTags = [x.tag for x in self.state.units.vespene_geyser] 715 | geyserTags = [x.tag for x in self.geysers] 716 | 717 | workerPool = self.units & [] 718 | workerPoolTags = set() 719 | 720 | # find all geysers that have surplus or deficit 721 | deficitGeysers = {} 722 | surplusGeysers = {} 723 | for g in self.geysers.filter(lambda x:x.vespene_contents > 0): 724 | # only loop over geysers that have still gas in them 725 | deficit = g.ideal_harvesters - g.assigned_harvesters 726 | if deficit > 0: 727 | deficitGeysers[g.tag] = {"unit": g, "deficit": deficit} 728 | elif deficit < 0: 729 | surplusWorkers = self.workers.closer_than(10, g).filter(lambda w:w not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in geyserTags) 730 | # workerPool.extend(surplusWorkers) 731 | for i in range(-deficit): 732 | if surplusWorkers.amount > 0: 733 | w = surplusWorkers.pop() 734 | workerPool.append(w) 735 | workerPoolTags.add(w.tag) 736 | surplusGeysers[g.tag] = {"unit": g, "deficit": deficit} 737 | 738 | # find all townhalls that have surplus or deficit 739 | deficitTownhalls = {} 740 | surplusTownhalls = {} 741 | if not onlySaturateGas: 742 | for th in self.townhalls: 743 | deficit = th.ideal_harvesters - th.assigned_harvesters 744 | if deficit > 0: 745 | deficitTownhalls[th.tag] = {"unit": th, "deficit": deficit} 746 | elif deficit < 0: 747 | surplusWorkers = self.workers.closer_than(10, th).filter(lambda w:w.tag not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in mineralTags) 748 | # workerPool.extend(surplusWorkers) 749 | for i in range(-deficit): 750 | if surplusWorkers.amount > 0: 751 | w = surplusWorkers.pop() 752 | workerPool.append(w) 753 | workerPoolTags.add(w.tag) 754 | surplusTownhalls[th.tag] = {"unit": th, "deficit": deficit} 755 | 756 | if all([len(deficitGeysers) == 0, len(surplusGeysers) == 0, len(surplusTownhalls) == 0 or deficitTownhalls == 0]): 757 | # cancel early if there is nothing to balance 758 | return 759 | 760 | # check if deficit in gas less or equal than what we have in surplus, else grab some more workers from surplus bases 761 | deficitGasCount = sum(gasInfo["deficit"] for gasTag, gasInfo in deficitGeysers.items() if gasInfo["deficit"] > 0) 762 | surplusCount = sum(-gasInfo["deficit"] for gasTag, gasInfo in surplusGeysers.items() if gasInfo["deficit"] < 0) 763 | surplusCount += sum(-thInfo["deficit"] for thTag, thInfo in surplusTownhalls.items() if thInfo["deficit"] < 0) 764 | 765 | if deficitGasCount - surplusCount > 0: 766 | # grab workers near the gas who are mining minerals 767 | for gTag, gInfo in deficitGeysers.items(): 768 | if workerPool.amount >= deficitGasCount: 769 | break 770 | workersNearGas = self.workers.closer_than(10, gInfo["unit"]).filter(lambda w:w.tag not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in mineralTags) 771 | while workersNearGas.amount > 0 and workerPool.amount < deficitGasCount: 772 | w = workersNearGas.pop() 773 | workerPool.append(w) 774 | workerPoolTags.add(w.tag) 775 | 776 | # now we should have enough workers in the pool to saturate all gases, and if there are workers left over, make them mine at townhalls that have mineral workers deficit 777 | for gTag, gInfo in deficitGeysers.items(): 778 | if performanceHeavy: 779 | # sort furthest away to closest (as the pop() function will take the last element) 780 | workerPool.sort(key=lambda x:x.distance_to(gInfo["unit"]), reverse=True) 781 | for i in range(gInfo["deficit"]): 782 | if workerPool.amount > 0: 783 | w = workerPool.pop() 784 | if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: 785 | self.macroActions.append(w.gather(gInfo["unit"], queue=True)) 786 | else: 787 | self.macroActions.append(w.gather(gInfo["unit"])) 788 | 789 | if not onlySaturateGas: 790 | # if we now have left over workers, make them mine at bases with deficit in mineral workers 791 | for thTag, thInfo in deficitTownhalls.items(): 792 | if performanceHeavy: 793 | # sort furthest away to closest (as the pop() function will take the last element) 794 | workerPool.sort(key=lambda x:x.distance_to(thInfo["unit"]), reverse=True) 795 | for i in range(thInfo["deficit"]): 796 | if workerPool.amount > 0: 797 | w = workerPool.pop() 798 | mf = self.state.mineral_field.closer_than(10, thInfo["unit"]).closest_to(w) 799 | if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: 800 | self.macroActions.append(w.gather(mf, queue=True)) 801 | else: 802 | self.macroActions.append(w.gather(mf)) 803 | 804 | # TODO: check if a drone is mining from a destroyed base (= if nearest townhalf from the GATHER target is >10 away) -> make it mine at another mineral patch 805 | 806 | # TODO: if we have too much gas, mine minerals again 807 | 808 | def chooseClosestMineralField(self, unit): 809 | mfs = self.state.mineral_field.closer_than(10, unit) 810 | if not mfs: 811 | for th in self.townhalls.ready: 812 | mfs += self.state.mineral_field.closer_than(10, th) 813 | if not mfs: 814 | mfs = self.state.mineral_field.filter(lambda x: x.mineral_contents > 0) 815 | if mfs: 816 | mf = mfs.closest_to(unit) 817 | return mf 818 | return None 819 | 820 | # split workers evenly across mineral patches 821 | def splitWorkers(self): 822 | if self.allowedSplitWorkers: 823 | for w in self.workers: 824 | mf = self.chooseClosestMineralField(w) 825 | if mf: 826 | action = w.gather(mf) 827 | self.microActions.append(action) 828 | 829 | # send idle workers back to mining 830 | def handleIdleWorkers(self): 831 | if self.allowedHandleIdleWorkers: 832 | if self.townhalls.exists: 833 | assignedTags = [] 834 | if hasattr(self, "buildExpansionDict") and self.buildExpansionDict: 835 | assignedTags = list(self.buildExpansionDict.keys()) 836 | # for w in self.workers.tags_not_in(assignedTags).idle: 837 | for w in self.units.of_type([MULE, race_worker[self.race]]).tags_not_in(assignedTags).idle: 838 | th = self.townhalls.ready.closest_to(w) 839 | mf = self.chooseClosestMineralField(th) 840 | if mf: 841 | action = w.gather(mf) 842 | self.macroActions.append(action) 843 | 844 | # if a base got destroyed, the workers keep long distance mining from those mineral patches usually 845 | def handleLongDistanceMining(self): 846 | if self.allowedHandleLongDistanceMining: 847 | if self.townhalls.exists: 848 | for w in self.workers.gathering: 849 | mfTag = w.orders[0].target 850 | mf = self.state.mineral_field.find_by_tag(mfTag) 851 | if mf: # worker is mining minerals 852 | th = self.townhalls.closest_to(mf) 853 | d = th.distance_to(mf) 854 | if d > 10: 855 | mf = self.chooseClosestMineralField(w) 856 | self.microActions.append(w.gather(mf)) 857 | 858 | elif not mf: # is not mineral field, must be geyser 859 | g = self.geysers.find_by_tag(mfTag) 860 | if g: 861 | th = self.townhalls.closest_to(g) 862 | d = th.distance_to(g) 863 | if d > 10: 864 | mf = self.chooseClosestMineralField(w) 865 | self.microActions.append(w.gather(mf)) 866 | 867 | 868 | 869 | 870 | # call down mules asap 871 | def handleMules(self): 872 | if self.allowedHandleMules and self.race == Race.Terran: 873 | for oc in self.units(UnitTypeId.ORBITALCOMMAND).ready.filter(lambda x: x.energy >= 50): 874 | mfs = self.state.mineral_field.closer_than(10, oc) 875 | if mfs: # mule the mineral patch with higehst amount 876 | mf = max(mfs, key=lambda x: x.mineral_contents) 877 | action = oc(AbilityId.CALLDOWNMULE_CALLDOWNMULE, mf) 878 | self.macroActions.append(action) 879 | else: # mule any base 880 | mf = self.chooseClosestMineralField(oc) 881 | if mf: 882 | action = oc(AbilityId.CALLDOWNMULE_CALLDOWNMULE, mf) 883 | self.macroActions.append(action) 884 | 885 | # set worker rally on new townhalls 886 | def setWorkerRallyPoint(self): 887 | if self.allowedSetRallyPoints: 888 | if not hasattr(self, "townhallsRallyPointsIssued"): 889 | self.townhallsRallyPointsIssued = set() 890 | 891 | for th in self.townhalls.ready.tags_not_in(self.townhallsRallyPointsIssued): 892 | mfs = self.state.mineral_field.closer_than(10, th) 893 | if mfs: 894 | mf = mfs.closest_to(mfs.center) 895 | action = th(AbilityId.RALLY_WORKERS, mf) 896 | self.macroActions.append(action) 897 | 898 | # lift and lower depots if we are terran and enemies are nearby 899 | def handleDepots(self): 900 | if self.allowedHandleDepots and self.race == Race.Terran: 901 | for depot in self.units.of_type([SUPPLYDEPOT, SUPPLYDEPOTDROP]).ready: 902 | if not self.known_enemy_units.closer_than(4, depot).exists: 903 | action = depot(AbilityId.MORPH_SUPPLYDEPOT_LOWER) 904 | self.microActions.append(action) 905 | for depot in self.units(UnitTypeId.SUPPLYDEPOTLOWERED).ready: 906 | if self.known_enemy_units.closer_than(3, depot).exists: 907 | action = depot(AbilityId.MORPH_SUPPLYDEPOT_RAISE) 908 | self.microActions.append(action) 909 | 910 | # make workers repair if we are terran 911 | def handleRepair(self): 912 | if self.allowedHandleRepairs and self.race == Race.Terran: 913 | importantDefensiveBuildings = [BUNKER, PLANETARYFORTRESS, MISSILETURRET] 914 | # refresh alive buildings, clear assigned dead workers, make assigned alive workers repair again 915 | if not hasattr(self, "assignedRepairs"): 916 | self.assignedRepairs = {} 917 | for previouslyBurningTag in list(self.assignedRepairs.keys()): 918 | building = self.units.find_by_tag(previouslyBurningTag) 919 | if not building or building \ 920 | and (building.health_percentage >= 1 or building.type_id not in importantDefensiveBuildings and building.health_percentage >= 0.35): 921 | wsTags = self.assignedRepairs.pop(previouslyBurningTag) 922 | for wTag in wsTags: 923 | w = self.units.find_by_tag(wTag) 924 | if w: 925 | self.microActions.append(w.stop()) 926 | # idle workers are being handled by another function 927 | elif building and (building.health_percentage < 0.35 or building.type_id in importantDefensiveBuildings and building.health_percentage < 1): 928 | wsTags = self.assignedRepairs[previouslyBurningTag] 929 | for wTag in list(wsTags)[:]: # because we are removing while iterating over it 930 | w = self.units.find_by_tag(wTag) 931 | if w: 932 | # make alive worker repair again if order[0] isnt repair 933 | if w.is_idle or w.orders[0].ability.id not in [AbilityId.EFFECT_REPAIR]: 934 | action = w(AbilityId.EFFECT_REPAIR, building) 935 | self.microActions.append(action) 936 | else: 937 | wsTags.remove(wTag) # remove dead worker 938 | 939 | for burningBuilding in ( \ 940 | self.units.structure.ready.filter(lambda x: x.health_percentage - 0.001 < 1/3) # burning buildings 941 | | self.units.ready.of_type(importantDefensiveBuildings).filter(lambda x: x.health_percentage < 0.8) # defensive structures 942 | # mechanical units close to townhalls 943 | ): # TODO: add mechanical units to repair list 944 | 945 | if burningBuilding.tag not in self.assignedRepairs: 946 | self.assignedRepairs[burningBuilding.tag] = set() 947 | 948 | newlyAssignedWorkers = self.units & [] 949 | if self.assignedRepairs[burningBuilding.tag] == set() and burningBuilding.type_id not in importantDefensiveBuildings: 950 | # assign worker to repair 951 | w = self.selectWorker(burningBuilding) 952 | if w: 953 | newlyAssignedWorkers.append(w) 954 | 955 | elif burningBuilding.type_id in importantDefensiveBuildings: 956 | # assign up to 8 workers to repair important stuff 957 | for i in range(8 - len(self.assignedRepairs[burningBuilding.tag])): 958 | w = self.selectWorker(burningBuilding) 959 | if w: 960 | newlyAssignedWorkers.append(w) 961 | 962 | # make new assigned scvs repair 963 | for w in newlyAssignedWorkers: 964 | action = w(AbilityId.EFFECT_REPAIR, burningBuilding) 965 | self.microActions.append(action) 966 | self.assignedRepairs[burningBuilding.tag].add(w.tag) 967 | 968 | # cancel buildings when they are basically under attack and about to die 969 | def handleBuildingCancelMicro(self): 970 | if self.allowedBuildingCancelMicro: 971 | for b in self.units.structure.not_ready.filter(lambda x: x.health_percentage + 0.05 < x.build_progress and x.health_percentage < 0.1): 972 | self.microActions.append(b(CANCEL)) 973 | 974 | # flies command center and orbitals closer to base - or flies to a new base if mined out! 975 | def handleMisplacedCommandCenter(self): 976 | if self.allowedFlyingCoommandCenters: 977 | # if self.townhalls.exists: 978 | if not self.units.of_type([ORBITALCOMMANDFLYING, COMMANDCENTERFLYING]).exists: 979 | for th in self.townhalls: 980 | mfs = self.state.mineral_field.closer_than(self.baseToMineralsCenterDistance + 3, th) 981 | if mfs: 982 | mfsCenter = mfs.center 983 | d = th.distance_to(mfsCenter) 984 | if d > self.baseToMineralsCenterDistance + 1: 985 | # how? 986 | pass 987 | elif not mfs: 988 | # find a base that has no allied or enemy base nearby 989 | for exp in self.expansion_locations.keys(): 990 | # is dict, key contains center (Point2) and value contains all mineral fields as set? 991 | pass 992 | 993 | 994 | 995 | else: 996 | for th in self.units.of_type([ORBITALCOMMANDFLYING, COMMANDCENTERFLYING]): 997 | pass 998 | 999 | 1000 | 1001 | def already_pending(self, unit_type): 1002 | # print("hellooo") 1003 | ability = self._game_data.units[unit_type.value].creation_ability 1004 | unitAttributes = self._game_data.units[unit_type.value].attributes 1005 | 1006 | # # the following checks for construction of buildings, i think 8 in unitAttributes stands for "structure" tag 1007 | # # i commented the following out because i think that is not what is meant with "already pending", but rather having a worker queued up to place a building, or having units in production queue 1008 | # if self.units(unit_type).not_ready.exists and 8 in unitAttributes: 1009 | # return len(self.units(unit_type).not_ready) 1010 | # the following checks for units being made from eggs and trained units in general 1011 | 1012 | buildings_in_construction = self.units.structure(unit_type).not_ready 1013 | 1014 | # a = [[ability, Point2((o.target.x, o.target.y)).rounded, s.position.rounded] for w in self.workers for o in w.orders for s in buildings_in_construction if o.ability==ability] 1015 | # if a != []: 1016 | # print(a) 1017 | 1018 | if 8 not in unitAttributes and any(o.ability == ability for w in (self.units.not_structure) for o in w.orders): 1019 | return sum([o.ability == ability for w in (self.units - self.workers) for o in w.orders]) 1020 | # following checks for unit production in a building queue, like queen, also checks if hatch is morphing to LAIR 1021 | elif any(o.ability.id == ability.id for w in (self.units.structure) for o in w.orders): 1022 | return sum([o.ability.id == ability.id for w in (self.units.structure) for o in w.orders]) 1023 | # the following checks if a worker is about to start a construction (and for scvs still constructing if not checked for structures with same position as target) 1024 | elif any(o.ability == ability for w in self.workers for o in w.orders): 1025 | # return sum([o.ability == ability and Point2((o.target.x, o.target.y)).rounded == s.position.rounded for w in self.workers for o in w.orders for s in buildings_in_construction]) # either this one 1026 | 1027 | return sum([o.ability == ability for w in self.workers for o in w.orders]) \ 1028 | - buildings_in_construction.amount 1029 | elif any(egg.orders[0].ability == ability for egg in self.units(EGG)): 1030 | return sum([egg.orders[0].ability == ability for egg in self.units(EGG)]) 1031 | return 0 1032 | 1033 | def getUnitInfo(self, unit, field="food_required"): 1034 | # get various unit data, see list below 1035 | # usage: getUnitInfo(ROACH, "mineral_cost") 1036 | assert isinstance(unit, (Unit, UnitTypeId)) 1037 | if isinstance(unit, Unit): 1038 | # unit = unit.type_id 1039 | unit = unit._type_data._proto 1040 | else: 1041 | unit = self._game_data.units[unit.value]._proto 1042 | # unit = self._game_data.units[unit.value] 1043 | # print(vars(unit)) # uncomment to get the list below 1044 | if hasattr(unit, field): 1045 | return getattr(unit, field) 1046 | else: 1047 | return None 1048 | """ 1049 | name: "Drone" 1050 | available: true 1051 | cargo_size: 1 1052 | attributes: Light 1053 | attributes: Biological 1054 | movement_speed: 2.8125 1055 | armor: 0.0 1056 | weapons { 1057 | type: Ground 1058 | damage: 5.0 1059 | attacks: 1 1060 | range: 0.10009765625 1061 | speed: 1.5 1062 | } 1063 | mineral_cost: 50 1064 | vespene_cost: 0 1065 | food_required: 1.0 1066 | ability_id: 1342 1067 | race: Zerg 1068 | build_time: 272.0 1069 | sight_range: 8.0 1070 | """ 1071 | 1072 | def main(): 1073 | sc2.run_game(sc2.maps.get("Abyssal Reef LE"), [ 1074 | Bot(Race.Zerg, BurnyBasicAI()), 1075 | Computer(Race.Terran, Difficulty.Medium) 1076 | ], realtime=False) 1077 | 1078 | if __name__ == '__main__': 1079 | main() 1080 | -------------------------------------------------------------------------------- /DragonBot/dragon_bot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bot made by Burny 3 | Bot version: 0.1 4 | Date of Upload: 2018-06-22 5 | 6 | Started working on this bot desgin on 2018-06-19 7 | """ 8 | 9 | # pylint: disable=E0602,E1102 10 | 11 | 12 | 13 | 14 | import random, json, time, math 15 | # import re, os 16 | # download maps from https://github.com/Blizzard/s2client-proto#map-packs 17 | 18 | import sc2 # pip install sc2 19 | from sc2.data import race_gas, race_worker, race_townhalls, ActionResult, Attribute, Race 20 | from sc2 import Race, Difficulty 21 | # from sc2.constants import * # for autocomplete 22 | # from sc2.ids.unit_typeid import * 23 | # from sc2.ids.ability_id import * 24 | import sc2.ids.unit_typeid 25 | import sc2.ids.ability_id 26 | import sc2.ids.buff_id 27 | import sc2.ids.upgrade_id 28 | import sc2.ids.effect_id 29 | 30 | # AbilityId.ATTACK 31 | 32 | from sc2.unit import Unit 33 | from sc2.units import Units 34 | from sc2.position import Point2, Point3 35 | 36 | from sc2 import Race, Difficulty 37 | from sc2.constants import * 38 | from sc2.player import Bot, Computer, Human 39 | 40 | 41 | from examples import burny_basic_ai 42 | from examples.burny_basic_ai import BehaviorManager 43 | 44 | 45 | class BuildOrderManager(object): 46 | def __init__(self): 47 | 48 | ################################ 49 | ######### MACRO (BUILDINGS) 50 | ################################ 51 | 52 | self.buildBarracksCondition = lambda other: \ 53 | all([ 54 | other.units.of_type([SUPPLYDEPOT, SUPPLYDEPOTDROP, SUPPLYDEPOTLOWERED]).ready.exists, 55 | # and other.townhalls.ready.exists, 56 | other.already_pending(BARRACKS) < 1, 57 | other.can_afford(BARRACKS), 58 | other.units(BARRACKS).amount != 1 or other.townhalls.amount != 1, # TODO: do this if you want to fast expand 59 | other.supply_used < 180 60 | ]) 61 | 62 | self.morphOrbitalCondition = lambda other: other.units(BARRACKS).ready.exists and other.units(COMMANDCENTER).ready.idle.exists 63 | 64 | # self.expandCondition => SEE BURNY BASIC AI FOR EXPANDCONDITION 65 | 66 | ################################ 67 | ######### MACRO (UNITS) 68 | ################################ 69 | 70 | 71 | # self.makeWorkersCondition => SEE BURNY BASIC AI 72 | self.trainMarineCondition = lambda other: other.supply_left > 0 and other.units(BARRACKS).ready.idle.exists 73 | 74 | ################################ 75 | ######### MACRO (UPGRADES) 76 | ################################ 77 | 78 | async def doStuff(self, bot): 79 | pass 80 | 81 | 82 | 83 | 84 | class DragonBot(burny_basic_ai.BurnyBasicAI): 85 | def __init__(self): 86 | super().__init__() 87 | pass 88 | 89 | async def firstIterationInit(self): 90 | await super().firstIterationInit() 91 | self.bom = BuildOrderManager() 92 | 93 | self.unitTagsInBehaviorManager = set() 94 | self.behaviorManagers = set() 95 | 96 | # TESTING 97 | 98 | # d = await self._client.query_pathing(self.townhalls.random, self.workers.random.position) 99 | # d = await self._client.query_pathing(self.townhalls.random.position, self.workers.random.position) 100 | # d = await self._client.query_pathing(self.workers.random, self.workers.random.position) 101 | d = await self._client.debug_create_unit(MARINE, 1, self.getMapCenter, 1) 102 | 103 | # d = await self._client.debug_create_unit(MARINE, 2, self.getMapCenter, 1) 104 | 105 | async def on_step(self, iteration): 106 | self.microActions = [] 107 | self.macroActions = [] 108 | if iteration == 0: 109 | # await super().firstIterationInit() 110 | await self.firstIterationInit() 111 | await super().on_step(iteration) 112 | 113 | # testing 114 | 115 | # if iteration == 10: 116 | # # testing 117 | # thLoc = self.townhalls.random.position3d.towards(self.getMapCenter, 3) 118 | # await self._client.debug_text(["THIS LOCATION"], [thLoc]) 119 | # print("unit height", self.townhalls.random.position3d.z) 120 | # print("terrain", self.getTerrainHeight(thLoc)) 121 | # print("placement", self.inPlacementGrid(thLoc)) 122 | # print("pathing", self.inPathingGrid(thLoc)) 123 | # print("visible", self.isVisible(thLoc)) 124 | # print("hasCreep", self.hasCreep(thLoc)) 125 | 126 | # print() 127 | 128 | # # thLoc = self.townhalls.random.position3d.towards(self.getMapCenter, 13) 129 | # thLoc = self.townhalls.random.position3d 130 | # await self._client.debug_text(["THIS LOCATION"], [thLoc]) 131 | # print("unit height", self.townhalls.random.position3d.z) 132 | # print("terrain", self.getTerrainHeight(thLoc)) 133 | # print("placement", self.inPlacementGrid(thLoc)) 134 | # print("pathing", self.inPathingGrid(thLoc)) 135 | # print("visible", self.isVisible(thLoc)) 136 | # print("hasCreep", self.hasCreep(thLoc)) 137 | 138 | # self._game_info.terrain_height.save_image("terrain_height.png") 139 | # self._game_info.placement_grid.save_image("placement_grid.png") 140 | # self._game_info.pathing_grid.save_image("pathing_grid.png") 141 | # self.state.visibility.save_image("visibility.png") 142 | # self.state.creep.save_image("creep.png") 143 | 144 | 145 | # add new units and set their behavior 146 | for u in self.units: 147 | if u.tag not in self.unitTagsInBehaviorManager: 148 | self.unitTagsInBehaviorManager.add(u.tag) 149 | manager = BehaviorManager(u.tag) 150 | 151 | if u.type_id == MARINE: 152 | manager.attackRange = 5 153 | manager.kitingRange = 4.8 154 | manager.priorityTypes = [BANELINGCOCOON, BANELING, RAVAGER] 155 | manager.ignoreTypes = [OVERLORD, LARVA, EGG] 156 | manager.attackRandomLocations = lambda other: set(other.enemy_start_locations) | {x for x in other.known_enemy_structures} 157 | manager.attackRandomCondition = lambda other: other.supply_used > 160 158 | 159 | self.behaviorManagers.add(manager) 160 | 161 | # update managers and remove if unit is dead 162 | deadManagers = [] 163 | microActions = [] 164 | for manager in self.behaviorManagers: 165 | if manager.removeCondition(self): 166 | deadManagers.append(manager) 167 | else: 168 | await manager.update(self) 169 | if manager.actionsThisIteration: 170 | microActions.extend(manager.actionsThisIteration) 171 | # print(microActions) 172 | await self.do_actions(microActions) 173 | 174 | for deadManager in deadManagers: 175 | self.behaviorManagers.remove(deadManager) 176 | 177 | # testing 178 | # for u in self.known_enemy_units: 179 | # print("enemy unit:", vars(u)) 180 | 181 | 182 | 183 | ################################ 184 | ######### MACRO - in order of priority 185 | ################################ 186 | 187 | # print([ 188 | # self.units.of_type([SUPPLYDEPOT, SUPPLYDEPOTDROP, SUPPLYDEPOTLOWERED]).ready.exists, 189 | # # and self.townhalls.ready.exists, 190 | # self.already_pending(BARRACKS) < 1, 191 | # bool(self.can_afford(BARRACKS)), 192 | # self.units(BARRACKS).amount != 1 or self.townhalls.amount != 1 # TODO: do this if you want to fast expand 193 | # ]) 194 | 195 | 196 | # buildings_in_construction = self.units(SUPPLYDEPOT).not_ready 197 | # if buildings_in_construction.amount > 0: 198 | # print(self.already_pending(SUPPLYDEPOT)) 199 | # ability = self._game_data.units[SUPPLYDEPOT.value].creation_ability 200 | # a = sum([o.ability == ability for w in self.workers for o in w.orders]), \ 201 | # sum([o.ability == ability and Point2((o.target.x, o.target.y)).rounded == s.position.rounded for w in self.workers for o in w.orders for s in buildings_in_construction]), \ 202 | # buildings_in_construction.amount # might have to end .rounded to points 203 | # print(a) 204 | 205 | if self.bom.morphOrbitalCondition(self): 206 | print("ORBITAL") 207 | # skip self afford because of orbital "bug" 208 | # if self.can_afford(ORBITALCOMMAND): 209 | # if self.can_afford(UPGRADETOORBITAL_ORBITALCOMMAND): 210 | cc = self.units(COMMANDCENTER).ready.idle.random 211 | self.macroActions.append(cc(UPGRADETOORBITAL_ORBITALCOMMAND)) 212 | 213 | elif self.lowSupplyLeftCondition(self): 214 | print("DEPOT") 215 | if self.can_afford(SUPPLYDEPOT): 216 | ws = self.workers.gathering 217 | if ws: 218 | w = ws.furthest_to(ws.center) 219 | loc = await self.find_placement(SUPPLYDEPOT, w, minDistanceToResources=0, placement_step=1) 220 | if loc: 221 | self.macroActions.append(w.build(SUPPLYDEPOT, loc)) 222 | 223 | elif self.makeWorkersCondition(self): 224 | print("SCV") 225 | if self.can_afford(SCV): 226 | cc = self.townhalls.ready.filter(lambda x: x.is_idle or len(x.orders) == 1 and x.orders[0].progress > 0.6).random 227 | # cc = self.townhalls.ready.idle.random 228 | self.macroActions.append(cc.train(SCV)) 229 | 230 | elif self.expandCondition(self): 231 | print("EXPAND") 232 | # if self.can_afford(COMMANDCENTER): 233 | await self.basicBuild(COMMANDCENTER) 234 | 235 | # elif self.takeGasCondition(self): 236 | # print("GAS") 237 | # if self.can_afford(race_gas[self.race]): 238 | # vgs = None 239 | # for i in range(10): 240 | # vgs = self.state.vespene_geyser.closer_than(10, self.townhalls.filter(lambda x: x.build_progress > 0.6).random) 241 | # if vgs: 242 | # break 243 | # for vg in vgs: 244 | # w = self.selectWorker(vg) 245 | # if w: 246 | # self.macroActions.append(w.build(race_gas[self.race], vg)) 247 | 248 | elif self.bom.trainMarineCondition(self): 249 | print("MARINE") 250 | if self.can_afford(MARINE): 251 | rax = self.units(BARRACKS).ready.idle.random 252 | self.macroActions.append(rax.train(MARINE)) 253 | 254 | elif self.bom.buildBarracksCondition(self): 255 | print("BARRACKS") 256 | if self.can_afford(BARRACKS): 257 | loc = await self.find_placement(BARRACKS, self.townhalls.ready.random, min_distance=5, minDistanceToResources=5, placement_step=4) 258 | ws = self.workers.gathering 259 | if loc and ws.exists: 260 | w = ws.closest_to(loc) 261 | if w: 262 | self.macroActions.append(w.build(BARRACKS, loc)) 263 | 264 | 265 | 266 | 267 | await self.step_end(iteration) 268 | 269 | async def step_end(self, iteration): 270 | await super().step_end(iteration) 271 | 272 | 273 | def main(): 274 | # sc2.run_game(sc2.maps.get("(2)CatalystLE"), [ 275 | # Bot(Race.Terran, DragonBot()), 276 | # Computer(Race.Zerg, Difficulty.VeryHard 277 | # ) 278 | # ], realtime=False) 279 | 280 | sc2.run_game(sc2.maps.get("(2)CatalystLE"), [ 281 | Bot(Race.Terran, DragonBot()), 282 | Computer(Race.Zerg, Difficulty.CheatInsane 283 | ) 284 | ], realtime=True) 285 | 286 | # sc2.run_game(sc2.maps.get("(2)CatalystLE"), [ 287 | # Bot(Race.Zerg, DragonBot()), 288 | # Computer(Race.Zerg, Difficulty.Medium) 289 | # ], realtime=False) 290 | 291 | if __name__ == '__main__': 292 | main() -------------------------------------------------------------------------------- /LadderBots.json: -------------------------------------------------------------------------------- 1 | { 2 | "Bots": { 3 | "CreepyBot": { 4 | "Race": "Zerg", 5 | "Type": "Python", 6 | "RootPath": "Bots/CreepyBot", 7 | "FileName": "run.py" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bots made by Burny 2 | 3 | Bots can be launched against each other as described on [this page](https://github.com/Hannessa/python-sc2-ladderbot). 4 | 5 | These bots should work on the AI LadderServer by executing "run.py" 6 | 7 | The [LadderBots.json](https://raw.githubusercontent.com/BurnySc2/burny-bots-python-sc2/master/LadderBots.json) shows you how to set up the bot for the LadderServer. --------------------------------------------------------------------------------