Hey, so I’ve been playing StarCraft a little bit lately, and thought it would be cool to create an AI that could play the game and win against bots. So, after a little bit of googling I came across the Python StarCraft package, sc2. This thing is awesome. It gives you full control over every aspect of the game. Then I found someone who had done just this. So as a disclamer, I did not create this code that I am posting. I wrote it, but I did not come up with it. That credit goes to Sentdex who runs pythonprogramming.net, check him out. But anyways, lets get started.

This project assumes you are somewhat comfortable using Python and navigating with the terminal. If not check out my Hello World Python intro tutorial.

  • First off you will need to download StarCraft. It is free to play, but you will need to make an account. So go ahead and download the game now so we can get started on some other stuff in the mean time, because it can take a while.
  • Next up, we will need to install sc2. So head over to the sc2 GitHub page and click the “clone or download” button to copy the url. Now create a new file directory to store the repository, then open a terminal and navigate to the new folder. In the terminal type: git clone https://github.com/Dentosal/python-sc2 and it will clone the contents of sc2 into your folder.
  • Sweet, now we need get matplotlib, numpy and OpenCV. Numpy an matplotlib are easy, so lets get them first. Just type pip install numpy and then pip install matplotlib in the terminal, hit enter after each one, and they should download. If there is an issue, try pip install numpy --user. Ok, now for OpenCV. Go to this link and download the OpenCV file that corresponds to your operating system and version of Python. I’m using Python 3.6 and I’m on a 64-bit windows computer so I will use: “opencv_python‑3.4.3‑cp36‑cp36m‑win_amd64.whl”.
  • To confirm the instillation worked properly, type python in your terminal, then type import sc2, import numpy, and import matplotlib, hitting enter after each one. If they were installed correctly, then nothing should happen, the cursor will just move to the next line.

  • Good work. So StarCraftII comes with a few maps, but we’re going to be using a different one which you can get from here. At the very least download the “Ladder 2017 Season 1” map, but I would recommend just getting them all. Once you’ve got them, and assuming the game is done downloading by now, open the games folder. It should look like this but with out the “Maps” folder
  • You will need to create a folder in the games directory called “Maps” and put the downloaded map files in it. Once you do that, you’re basically ready. So here’s the full code so far. Sorry I don’t have time to explain it all but you just need to copy it, put it in a python file, save it, and then run it in the terminal with python your_file_name.py. So have fun and enjoy!
  • import sc2 
    from sc2 import run_game, maps, Race, Difficulty, position
    from sc2.player import Bot, Computer  
    import random
    from sc2.constants import NEXUS, PROBE, PYLON, ASSIMILATOR, GATEWAY,\
     CYBERNETICSCORE, STALKER, STARGATE, VOIDRAY, OBSERVER, ROBOTICSFACILITY
    from examples.protoss.cannon_rush import CannonRushBot
    import cv2
    import numpy as np
    
    
    #165 iterations pre minute
    class SentdeBot(sc2.BotAI):
        def __init__(self):
            self.ITERATIONS_PER_MINUTE = 165
            self.MAX_WORKERS = 65
    
        async def on_step(self, iteration):
            self.iteration = iteration
            await self.scout()
            await self.distribute_workers()
            await self.build_workers()
            await self.build_pylons()
            await self.build_assimilators()
            await self.expand()
            await self.offensive_force_buildings()
            await self.build_offensive_force()
            await self.intel()
            await self.attack()
    
        def random_location_variance(self, enemy_start_location):
            x = enemy_start_location[0]
            y = enemy_start_location[1]
    
            x += ((random.randrange(-20, 20))/100) * enemy_start_location[0]
            y += ((random.randrange(-20, 20))/100) * enemy_start_location[1]
    
            if x < 0:
                x = 0
            if y < 0:
                y = 0
            if x > self.game_info.map_size[0]:
                x = self.game_info.map_size[0]
            if y > self.game_info.map_size[1]:
                y = self.game_info.map_size[1]
    
            go_to = position.Point2(position.Pointlike((x,y)))
            return go_to
    
        async def scout(self):
            if len(self.units(OBSERVER)) > 0:
                scout = self.units(OBSERVER)[0]
                if scout.is_idle:
                    enemy_location = self.enemy_start_locations[0]
                    move_to = self.random_location_variance(enemy_location)
                    print(move_to)
                    await self.do(scout.move(move_to))
    
            else:
                for rf in self.units(ROBOTICSFACILITY).ready.noqueue:
                    if self.can_afford(OBSERVER) and self.supply_left > 0:
                        await self.do(rf.train(OBSERVER))
    
        async def intel(self):
            game_data = np.zeros((self.game_info.map_size[1], self.game_info.map_size[0], 3), np.uint8)
    
            # UNIT: [SIZE, (BGR COLOR)]
            '''from sc2.constants import NEXUS, PROBE, PYLON, ASSIMILATOR, GATEWAY, \
     CYBERNETICSCORE, STARGATE, VOIDRAY'''
            draw_dict = {
                         NEXUS: [15, (0, 255, 0)],
                         PYLON: [3, (20, 235, 0)],
                         PROBE: [1, (55, 200, 0)],
                         ASSIMILATOR: [2, (55, 200, 0)],
                         GATEWAY: [3, (200, 100, 0)],
                         CYBERNETICSCORE: [3, (150, 150, 0)],
                         STARGATE: [5, (255, 0, 0)],
                         ROBOTICSFACILITY: [5, (215, 155, 0)],
    
                         VOIDRAY: [3, (255, 100, 0)],
                         #OBSERVER: [3, (255, 255, 255)],
                        }
    
            for unit_type in draw_dict:
                for unit in self.units(unit_type).ready:
                    pos = unit.position
                    cv2.circle(game_data, (int(pos[0]), int(pos[1])), draw_dict[unit_type][0], draw_dict[unit_type][1], -1)
    
    
    
            main_base_names = ["nexus", "supplydepot", "hatchery"]
            for enemy_building in self.known_enemy_structures:
                pos = enemy_building.position
                if enemy_building.name.lower() not in main_base_names:
                    cv2.circle(game_data, (int(pos[0]), int(pos[1])), 5, (200, 50, 212), -1)
            for enemy_building in self.known_enemy_structures:
                pos = enemy_building.position
                if enemy_building.name.lower() in main_base_names:
                    cv2.circle(game_data, (int(pos[0]), int(pos[1])), 15, (0, 0, 255), -1)
    
            for enemy_unit in self.known_enemy_units:
    
                if not enemy_unit.is_structure:
                    worker_names = ["probe",
                                    "scv",
                                    "drone"]
                    # if that unit is a PROBE, SCV, or DRONE... it's a worker
                    pos = enemy_unit.position
                    if enemy_unit.name.lower() in worker_names:
                        cv2.circle(game_data, (int(pos[0]), int(pos[1])), 1, (55, 0, 155), -1)
                    else:
                        cv2.circle(game_data, (int(pos[0]), int(pos[1])), 3, (50, 0, 215), -1)
    
            for obs in self.units(OBSERVER).ready:
                pos = obs.position
                cv2.circle(game_data, (int(pos[0]), int(pos[1])), 1, (255, 255, 255), -1)
    
            line_max = 50
            mineral_ratio = self.minerals / 1500
            if mineral_ratio > 1.0:
                mineral_ratio = 1.0
    
    
            vespene_ratio = self.vespene / 1500
            if vespene_ratio > 1.0:
                vespene_ratio = 1.0
    
            population_ratio = self.supply_left / self.supply_cap
            if population_ratio > 1.0:
                population_ratio = 1.0
    
            plausible_supply = self.supply_cap / 200.0
    
            military_weight = len(self.units(VOIDRAY)) / (self.supply_cap-self.supply_left)
            if military_weight > 1.0:
                military_weight = 1.0
    
    
            cv2.line(game_data, (0, 19), (int(line_max*military_weight), 19), (250, 250, 200), 3)  # worker/supply ratio
            cv2.line(game_data, (0, 15), (int(line_max*plausible_supply), 15), (220, 200, 200), 3)  # plausible supply (supply/200.0)
            cv2.line(game_data, (0, 11), (int(line_max*population_ratio), 11), (150, 150, 150), 3)  # population ratio (supply_left/supply)
            cv2.line(game_data, (0, 7), (int(line_max*vespene_ratio), 7), (210, 200, 0), 3)  # gas / 1500
            cv2.line(game_data, (0, 3), (int(line_max*mineral_ratio), 3), (0, 255, 25), 3)  # minerals minerals/1500
    
            # flip horizontally to make our final fix in visual representation:
            flipped = cv2.flip(game_data, 0)
            resized = cv2.resize(flipped, dsize=None, fx=2, fy=2)
    
            cv2.imshow('Intel', resized)
            cv2.waitKey(1)
            
    
        async def build_workers(self):
            if len(self.units(NEXUS))*16 > len(self.units(PROBE)):
                if len(self.units(PROBE)) < self.MAX_WORKERS:
                    for nexus in self.units(NEXUS).ready.noqueue:
                        if self.can_afford(PROBE):
                            await self.do(nexus.train(PROBE))
    
        async def build_pylons(self):
            if self.supply_left < 5 and not self.already_pending(PYLON):
                nexuses = self.units(NEXUS).ready()
                if self.can_afford(PYLON):
                    await self.build(PYLON, near=nexuses.first)
    
        async def build_assimilators(self):
            for nexus in self.units(NEXUS).ready:
                vaspenes = self.state.vespene_geyser.closer_than(15.0, nexus)
                for vaspene in vaspenes:
                    if not self.can_afford(ASSIMILATOR):
                        break
                    worker = self.select_build_worker(vaspene.position)
                    if worker is None:
                        break
                    if not self.units(ASSIMILATOR).closer_than(1.0, vaspene).exists:
                        await self.do(worker.build(ASSIMILATOR, vaspene))
    
        async def expand(self):
            if self.units(NEXUS).amount < 3 and self.can_afford(NEXUS):
                await self.expand_now()
    
        async def offensive_force_buildings(self):
            if self.units(PYLON).ready.exists:
                pylon = self.units(PYLON).ready.random
    
                if self.units(GATEWAY).ready.exists and not self.units(CYBERNETICSCORE):
                    if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE):
                        await self.build(CYBERNETICSCORE, near=pylon)
    
                elif len(self.units(GATEWAY)) < 1:
                    if self.can_afford(GATEWAY) and not self.already_pending(GATEWAY):
                        await self.build(GATEWAY, near=pylon)
                
                if self.units(CYBERNETICSCORE).ready.exists:
                    if len(self.units(ROBOTICSFACILITY)) < 1:
                        if self.can_afford(ROBOTICSFACILITY) and not self.already_pending(ROBOTICSFACILITY):
                            await self.build(ROBOTICSFACILITY, near=pylon)
    
                if self.units(CYBERNETICSCORE).ready.exists:
                    if len(self.units(STARGATE)) < (self.iteration / self.ITERATIONS_PER_MINUTE):
                        if self.can_afford(STARGATE) and not self.already_pending(STARGATE):
                            await self.build(STARGATE, near=pylon)
        
        async def build_offensive_force(self):
            for sg in self.units(STARGATE).ready.noqueue:
                if self.can_afford(VOIDRAY) and self.supply_left > 0:
                    await self.do(sg.train(VOIDRAY))
    
        def find_target(self, state):
            if len(self.known_enemy_units)> 0:
                return random.choice(self.known_enemy_units)
            elif len(self.known_enemy_structures) > 0:
                return random.choice(self.known_enemy_structures)
            else:
                return self.enemy_start_locations[0]
    
        async def attack(self):
            # {UNIT: [n to fight, n to defend]}
            aggressive_units = {VOIDRAY: [8, 3]}
    
            for UNIT in aggressive_units:
                if self.units(UNIT).amount > aggressive_units[UNIT][0] and self.units(UNIT).amount > aggressive_units[UNIT][1]:
                    for s in self.units(UNIT).idle:
                        await self.do(s.attack(self.find_target(self.state)))
    
                elif self.units(UNIT).amount > aggressive_units[UNIT][1]:
                    if len(self.known_enemy_units) > 0:
                        for s in self.units(UNIT).idle:
                            await self.do(s.attack(random.choice(self.known_enemy_units)))
    
    run_game(maps.get("AbyssalReefLE"), [
            Bot(Race.Protoss, SentdeBot()),
            Computer(Race.Protoss, Difficulty.Hard) #set Bot to Computer and CannonRushBot() to difficulty=Hard 
            ], realtime=False) 
    
    # x = 0
    # while x < 5:
    #     run_game(maps.get("AbyssalReefLE"), [
    #         Bot(Race.Protoss, SentdeBot()),
    #         Computer(Race.Protoss, Difficulty.Hard) #set Bot to Computer and CannonRushBot() to difficulty=Hard 
    #         ], realtime=False) # set realtime to false to speed up process
    #     x += 1
    
  • When you run the code, It should look like this:
  • The low-res image with colored shapes in it is the data-visualization of the game. This is what we needed OpenCV for. The colored bars that show up at the bottom of the small window represent the quantity of resources your bot has. The green circles show where your bases are, and the small dots that move around are the workers and fighters. The enemy will show up as red dots.
  • To increase the difficulty, go to the bottom of the code inside the run_game method and change Difficulty.Hard to Difficulty.Easy or Difficulty.Medium