|
| 1 | +import sc2 |
| 2 | +from sc2 import Race |
| 3 | +from sc2.player import Bot |
| 4 | + |
| 5 | +from sc2.units import Units |
| 6 | +from sc2.unit import Unit |
| 7 | +from sc2.position import Point2, Point3 |
| 8 | + |
| 9 | +from sc2.ids.unit_typeid import UnitTypeId |
| 10 | +from sc2.ids.upgrade_id import UpgradeId |
| 11 | +from sc2.ids.buff_id import BuffId |
| 12 | +from sc2.ids.ability_id import AbilityId |
| 13 | + |
| 14 | +from typing import List, Dict, Set, Tuple, Any, Optional, Union # mypy type checking |
| 15 | + |
| 16 | +""" |
| 17 | +To play an arcade map, you need to download the map first. |
| 18 | +
|
| 19 | +Open the StarCraft2 Map Editor through the Battle.net launcher, in the top left go to |
| 20 | +File -> Open -> (Tab) Blizzard -> Log in -> with "Source: Map/Mod Name" search for your desired map, in this example "Marine Split Challenge-LOTV" map created by printf |
| 21 | +Hit "Ok" and confirm the download. Now that the map is opened, go to "File -> Save as" to store it on your hard drive. |
| 22 | +Now load the arcade map by entering your map name below in |
| 23 | +sc2.maps.get("YOURMAPNAME") without the .SC2Map extension |
| 24 | +
|
| 25 | +
|
| 26 | +Map info: |
| 27 | +You start with 30 marines, level N has 15+N speed banelings on creep |
| 28 | +
|
| 29 | +Type in game "sling" to activate zergling+baneling combo |
| 30 | +Type in game "stim" to activate stimpack |
| 31 | +
|
| 32 | +
|
| 33 | +Improvements that could be made: |
| 34 | +- Make marines constantly run if they have a ling/bane very close to them |
| 35 | +- Split marines before engaging |
| 36 | +""" |
| 37 | + |
| 38 | +class MarineSplitChallenge(sc2.BotAI): |
| 39 | + async def on_step(self, iteration): |
| 40 | + if iteration == 0: |
| 41 | + await self.on_first_iteration() |
| 42 | + |
| 43 | + actions = [] |
| 44 | + |
| 45 | + # do marine micro vs zerglings |
| 46 | + for unit in self.units(UnitTypeId.MARINE): |
| 47 | + |
| 48 | + if self.known_enemy_units: |
| 49 | + |
| 50 | + # attack (or move towards) zerglings / banelings |
| 51 | + if unit.weapon_cooldown <= self._client.game_step / 2: |
| 52 | + enemies_in_range = self.known_enemy_units.filter(lambda u: unit.target_in_range(u)) |
| 53 | + |
| 54 | + # attack lowest hp enemy if any enemy is in range |
| 55 | + if enemies_in_range: |
| 56 | + # Use stimpack |
| 57 | + if self.already_pending_upgrade(UpgradeId.STIMPACK) == 1 and not unit.has_buff(BuffId.STIMPACK) and unit.health > 10: |
| 58 | + actions.append(unit(AbilityId.EFFECT_STIM)) |
| 59 | + |
| 60 | + |
| 61 | + # attack baneling first |
| 62 | + filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.BANELING) |
| 63 | + |
| 64 | + if not filtered_enemies_in_range: |
| 65 | + filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.ZERGLING) |
| 66 | + # attack lowest hp unit |
| 67 | + lowest_hp_enemy_in_range = min(filtered_enemies_in_range, key=lambda u: u.health) |
| 68 | + actions.append(unit.attack(lowest_hp_enemy_in_range)) |
| 69 | + |
| 70 | + # no enemy is in attack-range, so give attack command to closest instead |
| 71 | + else: |
| 72 | + closest_enemy = self.known_enemy_units.closest_to(unit) |
| 73 | + actions.append(unit.attack(closest_enemy)) |
| 74 | + |
| 75 | + |
| 76 | + # move away from zergling / banelings |
| 77 | + else: |
| 78 | + stutter_step_positions = self.position_around_unit(unit, distance=4) |
| 79 | + |
| 80 | + # filter in pathing grid |
| 81 | + stutter_step_positions = {p for p in stutter_step_positions if self.in_pathing_grid(p)} |
| 82 | + |
| 83 | + # find position furthest away from enemies and closest to unit |
| 84 | + enemies_in_range = self.known_enemy_units.filter(lambda u: unit.target_in_range(u, -0.5)) |
| 85 | + |
| 86 | + if stutter_step_positions and enemies_in_range: |
| 87 | + retreat_position = max(stutter_step_positions, key=lambda x: x.distance_to(enemies_in_range.center) - x.distance_to(unit)) |
| 88 | + actions.append(unit.move(retreat_position)) |
| 89 | + |
| 90 | + else: |
| 91 | + print("No retreat positions detected for unit {} at {}.".format(unit, unit.position.rounded)) |
| 92 | + |
| 93 | + await self.do_actions(actions) |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | + async def on_first_iteration(self): |
| 98 | + await self.chat_send("Edit this message for automatic chat commands.") |
| 99 | + self._client.game_step = 4 # do actions every X frames instead of every 8th |
| 100 | + |
| 101 | + |
| 102 | + |
| 103 | + def position_around_unit(self, pos: Union[Unit, Point2, Point3], distance: int=1, step_size: int=1, exclude_out_of_bounds: bool=True): |
| 104 | + pos = pos.position.to2.rounded |
| 105 | + positions = {pos.offset(Point2((x, y))) |
| 106 | + for x in range(-distance, distance+1, step_size) |
| 107 | + for y in range(-distance, distance+1, step_size) |
| 108 | + if (x, y) != (0, 0)} |
| 109 | + # filter positions outside map size |
| 110 | + if exclude_out_of_bounds: |
| 111 | + positions = {p for p in positions if 0 <= p[0] < self._game_info.pathing_grid.width and 0 <= p[1] < self._game_info.pathing_grid.height} |
| 112 | + return positions |
| 113 | + |
| 114 | + |
| 115 | +def main(): |
| 116 | + sc2.run_game(sc2.maps.get("Marine Split Challenge"), [ |
| 117 | + Bot(Race.Terran, MarineSplitChallenge()), |
| 118 | + ], realtime=False, save_replay_as="Example.SC2Replay") |
| 119 | + |
| 120 | +if __name__ == '__main__': |
| 121 | + main() |
0 commit comments