본문 바로가기

AI/강화학습

강화학습으로 StarCraft II 하기 - 2) PySC2 간단한 유닛 제어

DeepStar : 핸즈온 RL - 1회차 (2)

 

본 글은 싸이버스의 DeepStar 핸즈온 후 정리하는 블로그 이다.

강화학습을 이용하여 스타크래프트2를 학습시키는 것이 목적이다.

 

내용 
* (스타2) 
StarCraft II Learning Environment 튜토리얼 (2) - 간단한 제어 예시
reference

https://github.com/psygrammer/DeepStar/tree/master/season02_star2_handson_rl/pysc2_handson

수정

https://github.com/keep-steady/DeepStar/tree/master/season02_star2_handson_rl/pysc2_handson

 

 

 

 

 

Main Reference

  • How To: PySC2
  • 목표: 자신의 SC2 봇 만들기
  • 여기에서는 일단 저그로 기술
  • 작성: 조남운

목차

 

Building a Zerg Bot with PySC2 2.0

  • Link 구버젼 (pysc2 ver1.x) 테란
  • 원저자가 PySC2 2.0 버젼으로 문서를 업데이트하면서 저그로 만듦 Link
 

1. 기본 agent 만들기

전체 과정

  • 주의사항: 아래 코드는 튜토리얼 특성상 부분 실행보다는 통으로 실행하길 권장
  1. 필수 모듈 불러오기(Importing Essential Modules)
  • 수행 코드 넛기(Add the Run Code)
  • 드론 선택하기(Select a Drone)
  • 스포닝풀 짓기(Build a Spawning Pool)
  • 저글링 뽑기(Build Zerglings)
  • 오버로드 뽑기(Spawn More Overloads)
  • 공격가기!!!(Attack!)

1. 필수 모듈 불러오기(Importing Essential Modules

 
pyrhon
from pysc2.agents import base_agent
from pysc2.env import sc2_env
from pysc2.lib import actions, features
from absl import app
 

Creating Agent

 
class ZergAgent(base_agent.BaseAgent):
  def step(self, obs):
    super(ZergAgent, self).step(obs)

    return actions.FUNCTIONS.no_op()
 
  • The step method is the core part of our agent, it’s where all of our decision making takes place. At the end of every step you must return an action, in this case the action is to do nothing. We will add some more actions soon.
  • If you followed my previous tutorials you will notice that the format for the action has changed, previously the last line would have been:
 

Add the Run Code

 
def main(unused_argv):
    agent = ZergAgent()
    try:
        while True:
            with sc2_env.SC2Env(
                map_name="Automaton", # 맵을 오토메이톤으로 세팅
                players=[sc2_env.Agent(sc2_env.Race.zerg), # 저그 선택. (zerg, protoss, terran, random 선택 가능)
                        sc2_env.Bot(sc2_env.Race.random,
                        sc2_env.Difficulty.very_easy)],
                        # 상대를 Bot으로 할 것이며, 종족은 random, 난이도:very_easy
                        # 여기에 다른 agent를 설정할 수 있음
                agent_interface_format=features.AgentInterfaceFormat(
                        feature_dimensions=features.Dimensions(screen=84, minimap=64)),
                        step_mul=16,
                        # 액션 설정. 8 -> 300APM, 16 --> 150APM 
                        game_steps_per_episode=0,
                        # 디폴트는 30분인데, 여기에서는 endless로 설정
                        visualize=True) as env:
                        # 스크린과 미니맵 해상도 설정
                        # PySC2 2.0에서는 RGB 레이어를 추가할 수 있음 
                agent.setup(env.observation_spec(), env.action_spec())

                timesteps = env.reset()
                agent.reset()

                while True:
                    step_actions = [agent.step(timesteps[0])]
                    if timesteps[0].last():
                        break
                    timesteps = env.step(step_actions)

    except KeyboardInterrupt:
        pass

    if __name__ == "__main__":
        app.run(main)
 
  • 실행전에 [Startraft2 root]/Maps/Ladder2019Season1/ 폴더 등 필요 맵을 복사해야 함.
  • 존재하지 않는 경우 에러 메시지를 보고 블리자드 SC2 API 링크에서 연도 시즌에 맞는 맵 다운받아 SC2 루트/Maps 폴더에 폴더채로 복사할 것. 비번은 iagreetotheeula
  • python zerg_agent.py 실행
  • zerg_agent.py 파일은 본 문서가 있는 폴더에 있음.
 

zerg_agent.py 수행!!

아무것도 하지않는 저그 게임화면이 수행됨

In [1]:
!python zerg_agent.py
 
^C
 

2. Select a Drone

  • 드론을 선택해서 건물을 짓기 위해, 드론을 선택한다
  • 최초 공격 유닛인 저글링을 생산하기 전 단계로 필요 건물인 스포닝 풀(산란못)이 필요함.
  • 저그의 경우에는 건물을 드론(일벌레)이 변태하는 방식으로 짓기 때문에 이를 위해서 필요한 최초 행동은 드론을 선택하는 것임
 
from pysc2.lib import actions, features, units
import random
 

1) feature unit 사용가능하게 만들기

 
agent_interface_format=features.AgentInterfaceFormat(   feature_dimensions=features.Dimensions(screen=84, minimap=64),
                                                        use_feature_units=True),
 

2) step() 안에 화면내 모든 드론 리스트를 포착

 
def step(self, obs):
    super(ZergAgent, self).step(obs)

    # 화면 내에서 관찰된 것들을 unit list에 담고, 이 중 드론만 추출
    drones = [unit for unit in obs.observation.feature_units
              if unit.unit_type == units.Zerg.Drone]
    # 드론이 한마리 이싱이면, 그중 한마리를 랜덤으로 골라서 좌표를 리턴한다
    if len(drones) > 0:
        drone = random.choice(drones)
        return actions.FUNCTIONS.select_point("select_all_type", (drone.x, drone.y))
 
  • 일벌레 중에 아무거나 하나 집기. 이것은 게임 내에서 CTRL + 클릭과 동일함
 

3) 스포닝풀 짓기(Build a Spawning Pool)

 
# 리스트 중 첫번째 유닛이 원하는 타입인지 체크함
    def unit_type_is_selected(self, obs, unit_type):
        if (len(obs.observation.single_select) > 0 and
            obs.observation.single_select[0].unit_type == unit_type):
            return True

        if (len(obs.observation.multi_select) > 0 and
            obs.observation.multi_select[0].unit_type == unit_type):
            return True

        return False
 
  • unit_type_is_selected()는 리스트 중 첫번째 유닛이 원하는 타입인지 체크함
  • step() 에서 사용할 것임
 
def step(self, obs):
    super(ZergAgent, self).step(obs)

    if self.unit_type_is_selected(obs, units.Zerg.Drone):
        if (actions.FUNCTIONS.Build_SpawningPool_screen.id in 
          obs.observation.available_actions):

            x = random.randint(0, 83)
            y = random.randint(0, 83)

        return actions.FUNCTIONS.Build_SpawningPool_screen("now", (x, y))
 
  • 크립위이길 바라며 랜덤 포인트를 선정 --> 스포닝 풀을 짓기.
  • 몇 가지 상황이 있을 수 있음 (자원부족, 크립 위가 아님, 다른 건물과 겹침, 지상 유닛이 그 위에 있음 등)
  • 또한 스포닝 풀이 여러개 건설 가능한 관계로 무척 많은 스포닝 풀이 건설될 가능성도 있음. 이를 피하기 위해서 아래 코드를 사용
 
def get_units_by_type(self, obs, unit_type):
    return [unit for unit in obs.observation.feature_units
            if unit.unit_type == unit_type]
 
  • 그러한 결과는 아래와 같음
    spawning_pools = self.get_units_by_type(obs, units.Zerg.SpawningPool)
      # 스포닝풀이 없을때만 짓기
      if len(spawning_pools) == 0:
          # 드론이 골라지면
          if self.unit_type_is_selected(obs, units.Zerg.Drone):
              if (actions.FUNCTIONS.Build_SpawningPool_screen.id in obs.observation.available_actions):
                  # 화면안에서 랜덤으로 x, y좌표를 선택하고
                  x = random.randint(0, 83)
                  y = random.randint(0, 83)
                  # 그 위치에다가 스포닝풀을 짓는다
                  return actions.FUNCTIONS.Build_SpawningPool_screen("now", (x, y))
          # 드론을 선택해서
          drones = self.get_units_by_type(obs, units.Zerg.Drone)
              # 한마리 이상 드론이 선택되면
              if len(drones) > 0:
                  # 그중 랜덤으로 한마리를 고르고
                  drone = random.choice(drones)
                  # 수행
                  return actions.FUNCTIONS.select_point("select_all_type", (drone.x, drone.y))
    
  • 전체 코드는 아래와 같음 (> python zerg_agent_step4.py)
  • 맵이 없다고 하면 콘솔 잘 보고 블리자드 SC2 API 링크에서 해당 맵 다운로드 하여 Maps 폴더 설치
In [2]:
!python zerg_agent_step4.py
 
^C
 
from pysc2.agents import base_agent
from pysc2.env import sc2_env
from pysc2.lib import actions, features, units
from absl import app
import random

class ZergAgent(base_agent.BaseAgent):
  def unit_type_is_selected(self, obs, unit_type):
    if (len(obs.observation.single_select) > 0 and
        obs.observation.single_select[0].unit_type == unit_type):
      return True

    if (len(obs.observation.multi_select) > 0 and
        obs.observation.multi_select[0].unit_type == unit_type):
      return True

    return False

  def get_units_by_type(self, obs, unit_type):
    return [unit for unit in obs.observation.feature_units
            if unit.unit_type == unit_type]

  def step(self, obs):
    super(ZergAgent, self).step(obs)

    spawning_pools = self.get_units_by_type(obs, units.Zerg.SpawningPool)
    if len(spawning_pools) == 0:
      if self.unit_type_is_selected(obs, units.Zerg.Drone):
        if (actions.FUNCTIONS.Build_SpawningPool_screen.id in 
            obs.observation.available_actions):
          x = random.randint(0, 83)
          y = random.randint(0, 83)

          return actions.FUNCTIONS.Build_SpawningPool_screen("now", (x, y))

      drones = self.get_units_by_type(obs, units.Zerg.Drone)
      if len(drones) > 0:
        drone = random.choice(drones)

        return actions.FUNCTIONS.select_point("select_all_type", (drone.x,
                                                                  drone.y))

    return actions.FUNCTIONS.no_op()

def main(unused_argv):
  agent = ZergAgent()
  try:
    while True:
      with sc2_env.SC2Env(
          map_name="AbyssalReef",
          players=[sc2_env.Agent(sc2_env.Race.zerg),
                   sc2_env.Bot(sc2_env.Race.random,
                               sc2_env.Difficulty.very_easy)],
          agent_interface_format=features.AgentInterfaceFormat(
              feature_dimensions=features.Dimensions(screen=84, minimap=64),
              use_feature_units=True),
          step_mul=16,
          game_steps_per_episode=0,
          visualize=True) as env:

        agent.setup(env.observation_spec(), env.action_spec())

        timesteps = env.reset()
        agent.reset()

        while True:
          step_actions = [agent.step(timesteps[0])]
          if timesteps[0].last():
            break
          timesteps = env.step(step_actions)

  except KeyboardInterrupt:
    pass

if __name__ == "__main__":
  app.run(main)
 

Build Zerglings

  • 스포닝풀 (산란못)이 완성되면 저글링 만들 준비가 됨.
  • 저그유닛은 해처리(부화장)에서 시간마다 생성되는 라바(애벌래)를 선택하여 원하는 가능한 유닛을 변태하는 과정으로 생성이 진행됨
  • 따라서 이를 위해 필요한 첫 행동은 모든 라바를 선택하는 것임

    larvae = self.get_units_by_type(obs, units.Zerg.Larva)
      if len(larvae) > 0:
        larva = random.choice(larvae)
    
        return actions.FUNCTIONS.select_point("select_all_type",
                                              (larva.x, larva.y))
    
  • 저글링을 만드는 코드는 다음과 같이 설정

    if self.unit_type_is_selected(obs, units.Zerg.Larva):
        if (actions.FUNCTIONS.Train_Zergling_quick.id in 
            obs.observation.available_actions):
          return actions.FUNCTIONS.Train_Zergling_quick("now")
    

Spawn More Overloads

  • 인구수가 부족할 경우 유닛 생성이 안되므로 저그의 인구수 증가를 위해 오버로드 (대군주)를 일부 변태시켜야 함

    free_supply = (obs.observation.player.food_cap -
                       obs.observation.player.food_used)
        if free_supply == 0:
          if (actions.FUNCTIONS.Train_Overlord_quick.id in
              obs.observation.available_actions):
            return actions.FUNCTIONS.Train_Overlord_quick("now")
    
  • 행동이 가능한지 체크하는 함수

    def can_do(self, obs, action):
      return action in obs.observation.available_actions
    
  • 이 함수를 앞에서 사용했던 액션에 모두 적용 (불가능할 경우 하지 않게 하기 위해서임). 예를 들면:
    if self.can_do(obs, actions.FUNCTIONS.Build_SpawningPool_screen.id):
    
    여기까지의 전체 코드는 다음과 같음. > python zerg_agent_step6.py
In [12]:
!python zerg_agent_step6.py
 
^C
 
from pysc2.agents import base_agent
from pysc2.env import sc2_env
from pysc2.lib import actions, features, units
from absl import app
import random

class ZergAgent(base_agent.BaseAgent):
  def unit_type_is_selected(self, obs, unit_type):
    if (len(obs.observation.single_select) > 0 and
        obs.observation.single_select[0].unit_type == unit_type):
      return True

    if (len(obs.observation.multi_select) > 0 and
        obs.observation.multi_select[0].unit_type == unit_type):
      return True

    return False

  def get_units_by_type(self, obs, unit_type):
    return [unit for unit in obs.observation.feature_units
            if unit.unit_type == unit_type]

  def can_do(self, obs, action):
    return action in obs.observation.available_actions

  def step(self, obs):
    super(ZergAgent, self).step(obs)

    spawning_pools = self.get_units_by_type(obs, units.Zerg.SpawningPool)
    if len(spawning_pools) == 0:
      if self.unit_type_is_selected(obs, units.Zerg.Drone):
        if self.can_do(obs, actions.FUNCTIONS.Build_SpawningPool_screen.id):
          x = random.randint(0, 83)
          y = random.randint(0, 83)

          return actions.FUNCTIONS.Build_SpawningPool_screen("now", (x, y))

      drones = self.get_units_by_type(obs, units.Zerg.Drone)
      if len(drones) > 0:
        drone = random.choice(drones)

        return actions.FUNCTIONS.select_point("select_all_type", (drone.x,
                                                                  drone.y))

    if self.unit_type_is_selected(obs, units.Zerg.Larva):
      free_supply = (obs.observation.player.food_cap -
                     obs.observation.player.food_used)
      if free_supply == 0:
        if self.can_do(obs, actions.FUNCTIONS.Train_Overlord_quick.id):
          return actions.FUNCTIONS.Train_Overlord_quick("now")

      if self.can_do(obs, actions.FUNCTIONS.Train_Zergling_quick.id):
        return actions.FUNCTIONS.Train_Zergling_quick("now")

    larvae = self.get_units_by_type(oba, units.Zerg.Larva)
    if len(larvae) > 0:
      larva = random.choice(larvae)

      return actions.FUNCTIONS.select_point("select_all_type", (larva.x,
                                                                larva.y))

    return actions.FUNCTIONS.no_op()

def main(unused_argv):
  agent = ZergAgent()
  try:
    while True:
      with sc2_env.SC2Env(
          map_name="AbyssalReef",
          players=[sc2_env.Agent(sc2_env.Race.zerg),
                   sc2_env.Bot(sc2_env.Race.random,
                               sc2_env.Difficulty.very_easy)],
          agent_interface_format=features.AgentInterfaceFormat(
              feature_dimensions=features.Dimensions(screen=84, minimap=64),
              use_feature_units=True),
          step_mul=16,
          game_steps_per_episode=0,
          visualize=True) as env:

        agent.setup(env.observation_spec(), env.action_spec())

        timesteps = env.reset()
        agent.reset()

        while True:
          step_actions = [agent.step(timesteps[0])]
          if timesteps[0].last():
            break
          timesteps = env.step(step_actions)

  except KeyboardInterrupt:
    pass

if __name__ == "__main__":
  app.run(main)
 

Attack

 
  • 공격을 가기 전에 공격 대상 위치를 알아야 함.
  • 여기에서는 문제를 단순화하기 위해 좌상단 우하단에만 기지가 생성(어비셜 리프 기준. 오토메이톤은 다를 수 있음) 되므로 자기가 어느 위치인지만 파악하여 공격가는 것으로 함.
  • __init()__ 생성하여 관련 변수 (attack_coordinates) 설정
def __init__(self):
    super(ZergAgent, self).__init__()

    self.attack_coordinates = None
  • step() 를 수정
def step(self, obs):
    super(ZergAgent, self).step(obs)

    if obs.first():  # 첫 스텝이라면 ==> 우리 유닛들의 중심점 좌표를 구함
      player_y, player_x = (obs.observation.feature_minimap.player_relative ==
                            features.PlayerRelative.SELF).nonzero()
      xmean = player_x.mean()
      ymean = player_y.mean()

      if xmean <= 31 and ymean <= 31:
        self.attack_coordinates = (49, 49)
      else:
        self.attack_coordinates = (12, 16)
  • 공격 지시
zerglings = self.get_units_by_type(obs, units.Zerg.Zergling)
    if len(zerglings) > 0:
      if self.can_do(obs, actions.FUNCTIONS.select_army.id):
        return actions.FUNCTIONS.select_army("select")
  • 하지만 이렇게 하면 뽑는 대로 공격감. 따라서 일정 수 이상 모인 뒤에 갈 필요가 있음
if len(zerglings) >= 10:
 

이 파트까지의 내용은 같은 폴더의 zerg_agent_step7.py 에 있음

>python zerg_agent_step7.py

In [11]:
!python zerg_agent_step7.py
 
^C
 

다음 주제

  • 여기까지는 모두 단순 제어였음
  • 다음에는 간단한 RL 모듈의 추가 튜토리얼 해설 진행
In [6]:
from IPython.core.display import display, HTML

display(HTML("<style> .container{width:90% !important;}</style>"))
 
 
In [ ]:
 

'AI > 강화학습' 카테고리의 다른 글

강화학습으로 StarCraft II 하기 - 1) PySC2 환경설정  (2) 2019.05.27