스타크래프트2 봇

스타크래프트2 인공지능 봇을 만들어보자 (#3)

오잎 클로버 2021. 8. 5. 12:00
728x90

필요한 내용들도 있고, 구현하고자 하는 초반 기능들이 있었기에

보급고를 짓는 코드는 다음번으로 미루었다.

 

먼저 세분화를 먼저 시켜야 추후 작업할 때 편할 것을 생각하여

Config과 Absinthe, 그리고 main으로 나누었다

 

Config에서는 맵 설정, 스크린 및 미니맵 사이즈 조절, APM 조절 및 자신의 종족, 상대의 종족 등을 세팅하는 곳이다

Absinthe는 Config에서 필요한 내용을 가져와 사용을 하여 모든 것을 조종하는 곳이며,

main은 실행하는 곳이다.

 

먼저 Config 부터 살펴보자면

from pysc2.env import sc2_env
from pysc2.lib import features

# 맵 이름
MAPNAME = "Simple64"

# 스크린 및 미니맵 사이즈 조절
SCREEN_SIZE = 84
MINIMAP_SIZE = 64

# APM 조절
APM = 300
APM = int(APM / 18.75)
UNLIMIT = 0
VISUALIZE = True
REALTIME = True


players = [sc2_env.Agent(sc2_env.Race.terran),
           sc2_env.Bot(sc2_env.Race.random,
                       sc2_env.Difficulty.very_easy)]

interface = features.AgentInterfaceFormat(
    feature_dimensions=features.Dimensions(
        screen=SCREEN_SIZE, minimap=MINIMAP_SIZE
    ),
    use_feature_units=True
)

자신 (에이전트)의 종족은 테란으로 할 것이며, 상대방은 봇으로 아주 쉬우며 종족은 랜덤이다.

인터페이스에서는 사이즈 조절 등을 조절한다. (전체적인 세팅 중 하나)

 

Absinthe는 Config의 내용을 가져와서 처리하도록 하였다.

from pysc2.env import sc2_env
from pysc2.agents import base_agent
from pysc2.lib import actions

import Config


class AgentAbsinthe(base_agent.BaseAgent):
    def step(self, obs):
        super(AgentAbsinthe, self).step(obs)
        return actions.FUNCTIONS.no_op()


def main(args):
    agent = AgentAbsinthe()
    try:
        with sc2_env.SC2Env(map_name=Config.MAPNAME, players=Config.players,
                            agent_interface_format=Config.interface,
                            step_mul=Config.APM, game_steps_per_episode=Config.UNLIMIT,
                            visualize=Config.VISUALIZE, realtime=Config.REALTIME) as env:
            agent.setup(env.observation_spec(), env.action_spec())

            timeStep = env.reset()
            agent.reset()

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

    except KeyboardInterrupt:
        pass

마지막으로 main에서 실행하도록 한다.

from absl import app

import Absinthe

if __name__ == '__main__':
    app.run(Absinthe.main)

 

정찰을 하는 것을 구현하도록 하겠다.

 

천천히 구현을 해보자면

1) SCV 한 기를 선택한다

2) 해당 SCV를 부대지정

3) 해당 SCV를 카메라로 따라다니며 및 컨트롤

 

카메라 무브라는 메소드가 존재하기는 하나, 사용하기 귀찮기도 하고 어렵기에

pynput을 사용하여 편법처럼 카메라가 따라다니도록 구현할 수 있습니다.

 

Config에서 필요한 변수들을 미리 지정하도록 한다

SCV 부대 지정을 위한 변수

CONTROL_GROUP_SET = 1
CONTROL_GROUP_RECALL = 0
SCV_GROUP_ORDER = 1

카메라 따라다니기 위한 변수

KEYBOARD_BUTTON = pynput.keyboard.Controller()
KEYBOARD_KEY = pynput.keyboard.Key
NOT_QUEUED = [0]
MOVE_SCREEN = 331
MOVE_MINIMAP = 332
SCREEN_ENEMY = 4
PLAYER_SELF = features.PlayerRelative.SELF

 

위 변수들을 사용하여 Absinthe에 적용시킨다.

 

먼저 특정 유닛의 종류가 선택되었는지 확인하는 메소드를 구현해보자

def unitTypeIsSelected(self, obs, unit_type):
        if len(obs.observation.single_select) > 0 and obs.observation.single_select[0].unit_type == unit_type:
            return True
        elif len(obs.observation.multi_select) > 0 and obs.observation.multi_select[0].unit_type == unit_type:
            return True
        else:
            return False

하나만 선택되었든, 2기 이상 선택되었든 같은 종류기만 하면 True를 반환하도록 한다.

 

그리고 모든 SCV들을 담은 리스트를 하나 생성한다.

scvs = [unit for unit in obs.observation.feature_units
                if unit.unit_type == units.Terran.SCV]

해당 리스트를 사용하여 여러 가지를 구현한다.

if len(scvs) > 0 and not self.unitTypeIsSelected(obs, units.Terran.SCV):
	scv = scvs[0]
    return actions.FUNCTIONS.select_point("select", (scv.x, scv.y))
elif self.unitTypeIsSelected(obs, units.Terran.SCV) and obs.observation.control_groups[Config.SCV_GROUP_ORDER][0] == 0:
    return actions.FUNCTIONS.select_control_group([Config.CONTROL_GROUP_SET], [Config.SCV_GROUP_ORDER])
elif len([x for x in obs.observation.feature_units
          if x.is_selected == 1]) == 0:
    Config.KEYBOARD_BUTTON.press(str(Config.SCV_GROUP_ORDER))
    Config.KEYBOARD_BUTTON.release(str(Config.SCV_GROUP_ORDER))
    Config.KEYBOARD_BUTTON.press(str(Config.SCV_GROUP_ORDER))
    Config.KEYBOARD_BUTTON.release(str(Config.SCV_GROUP_ORDER))
    return actions.FUNCTIONS.select_control_group([Config.CONTROL_GROUP_RECALL], [Config.SCV_GROUP_ORDER])
elif len([x for x in obs.observation.feature_units
          if ((x.is_selected == 1) and x.order_length == 0)]) == 1\
          and Config.SCREEN_ENEMY not in[x.alliance for x in obs.observation.feature_units]:
    x, y = random.randint(0, 64), random.randint(0, 64)
    return actions.FunctionCall(Config.MOVE_SCREEN, [Config.NOT_QUEUED, [x, y]])
elif len([x for x in obs.observation.feature_units if (x.is_selected == 1)]) == 1 \
          and Config.SCREEN_ENEMY not in [x.alliance for x in obs.observation.feature_units]:
    x, y = self.scout_coordinates
    return actions.FunctionCall(Config.MOVE_MINIMAP, [Config.NOT_QUEUED, [x, y]])
else:
    return actions.FUNCTIONS.no_op()

순서대로 설명을 하자면

맨 위의 if문에서는 유닛을 선택하는 코드이고

첫 번째 elif (else if)에서는 해당 유닛을 부대지정을 하는 코드이고

두 번째 elif (else if)에서는 화면 안에 아군이 하나도 없다면 부대 지정한 키를 2번 눌러 해당 위치로 이동하는 코드이고

세 번째 elif (else if)에서는 위 동일한 조건이지만 더불어 화면 내 랜덤 한 위치로 움직이도록 하는 코드이고

마지막 elif (else if)에서는 적군 위치로 이동하도록 하는 코드이다.

 

돌아가는 코드를 만들지 않은 이유는 어차피 놀고 있는 일꾼을 다시 일하도록 하는 코드를 사용하면 

문제없이 돌아가기 때문에 굳이 만들지 않았다.

 

전체 코드는 필자의 깃허브를 확인해주기를 바란다.