原作者: Yvan Scher
链接: https://medium.com/@yvanscher/making-a-game-ai-with-deep-learning-963bb549b3d5

 
用pysc2和Q Learning制作小型游戏AI
 
星际争霸2最强大的单位之一:The Laser Giraffe
 
要为游戏制作人工智能,您需要:
 
1 — 人工智能逻辑,不管是脚本行为还是人工智能
 
2 — 把你的游戏世界转换成你的人工智能可以理解和执行的东西
 
本文的目标是向您展示构建AL逻辑的方法,从脚本行为到可以学习几乎任何任务的方法。对于那些有编程技巧的人来说,这应该被认为是一篇介绍bot机器人构建的文章。我们将建立一个可以玩星际争霸2小游戏的人工智能。我们将使用python,numpy,pysc2。Let’s go!
 

设置pysc2

 
如果你想成为一个游戏AI,你需要为你的游戏创建一个接口api,一个AI可以用来在你的游戏世界中查看、玩和采取行动。我们将使用星际争霸2,特别是由deepmind和google发布的一个名为pysc2的环境。首先我们需要安装星际争霸2(它是免费的);我使用的是linux,所以:
 

 cd ~
 curl -O http://blzdistsc2-a.akamaihd.net/Linux/SC2.3.17.zip
 unzip -q SC2.3.17.zip

 
确保您得到了游戏的3.17版本,因为较新的版本似乎不适用于某些pysc2函数(查看 run_configs/platforms).解压后大约需要7GB的空间,解压密码是“iagreetotheeula”。我在ubuntu上渲染3D视图时遇到了一些问题。
 
如果使用mac,请确保将游戏安装在默认位置(~),并在主文件夹中创建“Maps”、“Replays”文件夹。使用安装程序现在让我们检查pysc2和pytorch:
 

conda create -n pysc2 python=3 -y
pip install pysc2
pip install numpy

 
现在我们需要得到sc2地图,我们将用它作为我们的AI的试验地: 在该链接上获取mini games地图
 
出于某种原因,pysc2的github上的迷你游戏zip文件在linux上不起作用。所以我在我的mac电脑上解压缩了它,然后把它移到了我的linux机器上。将mini_games文件夹放入StarcraftII安装文件夹中的Maps文件夹。小游戏地图实际上是随pysc2一起提供的,但谁知道deepmind是否会继续这样做。好了,现在我们有了所有的软件和地图,让我们编写第一个代理程序,并检查pysc2环境的详细信息。
 

实现随机AI

 
我们要做的人工智能将要玩移动到信标游戏。我们的人工智能将控制一个海军陆战队(小型作战部队),并将其移动到信标。
 
我要做一个简单的AI,可以与这个环境交互。它会在地图上随机移动:
 

import numpy as np
from pysc2.agents import base_agent
from pysc2.lib import actions
from pysc2.lib import features
from pysc2.env import sc2_env, run_loop, available_actions_printer
from pysc2 import maps
from absl import flags

# define the features the AI can seee
_AI_RELATIVE = features.SCREEN_FEATURES.player_relative.index
# define contstants for actions
_NO_OP = actions.FUNCTIONS.no_op.id
_MOVE_SCREEN = actions.FUNCTIONS.Attack_screen.id
_SELECT_ARMY = actions.FUNCTIONS.select_army.id
# define constants about AI's world
_BACKGROUND = 0
_AI_SELF = 1
_AI_ALLIES = 2
_AI_NEUTRAL = 3
_AI_HOSTILE = 4
# constants for actions
_SELECT_ALL = [0]
_NOT_QUEUED = [0]

def get_marine_location(ai_relative_view):
    '''get the indices where the world is equal to 1'''
    return (ai_relative_view == _AI_SELF).nonzero()

def get_rand_location(ai_location):
    '''gets a random location at least n away from current x,y point.'''
    return [np.random.randint(0, 64), np.random.randint(0, 64)]

class Agent1(base_agent.BaseAgent):
    # An agent for doing a simple movement form one point to another.
    def step(self, obs):
        # step function gets called automatically by pysc2 environment
        # call the parent class to have pysc2 setup rewards/etc for us
        super(Agent1, self).step(obs)
        # if we can move our army (we have something selected)
        if _MOVE_SCREEN in obs.observation['available_actions']:
            # get what the ai can see about the world
            ai_view = obs.observation['screen'][_AI_RELATIVE]
            # get the location of our marine in this world
            marine_x, marine_y = get_marine_location(ai_view)
            # it our marine is not on the screen do nothing.
            # this happens if we scroll away and look at a different
            # part of the world
            if not marine_x.any():
                return actions.FunctionCall(_NO_OP, [])
            target = get_rand_location([marine_x, marine_y])
            return actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, target])
        # if we can't move, we havent selected our army, so selecto ur army
        else:
            return actions.FunctionCall(_SELECT_ARMY, [_SELECT_ALL])

 
下面是上面代码运行的视频;AI在地图上随机移动:
 

python -m pysc2.bin.agent --map MoveToBeacon --agent agent1.Agent1

 

 

功能图如下所示:
 

 

这里没发生什么疯狂的事。你可以看到一个海军陆战队(绿色)和信标(灰蓝色)的主视图。海军陆战队只是像我们说的那样随意移动。屏幕的右边是我们的机器人可以看到的所有不同的视图。屏幕上的单位类型不同,地形高度图也不同。要查看此部分的更多代码/说明,请参阅此笔记本。
 
具有更可读文本的要素图层的另一个示例:
 

 

实现脚本化人工智能

 
现在我们想做一些比随机更好的事情。在移动到灯塔迷你游戏中,目标是移动到灯塔。我们将编写一个执行此操作的机器人脚本:
 

import numpy as np
from pysc2.agents import base_agent
from pysc2.lib import actions
from pysc2.lib import features
from pysc2.env import sc2_env, run_loop, available_actions_printer
from pysc2 import maps
from absl import flags

_AI_RELATIVE = features.SCREEN_FEATURES.player_relative.index
_NO_OP = actions.FUNCTIONS.no_op.id
_MOVE_SCREEN = actions.FUNCTIONS.Attack_screen.id
_SELECT_ARMY = actions.FUNCTIONS.select_army.id
_BACKGROUND = 0
_AI_SELF = 1
_AI_ALLIES = 2
_AI_NEUTRAL = 3
_AI_HOSTILE = 4
_SELECT_ALL = [0]
_NOT_QUEUED = [0]

def get_beacon_location(ai_relative_view):
    '''returns the location indices of the beacon on the map'''
    return (ai_relative_view == _AI_NEUTRAL).nonzero()

class Agent2(base_agent.BaseAgent):
    """An agent for doing a simple movement form one point to another."""
    def step(self, obs):
        '''Step function gets called automatically by pysc2 environment'''
        super(Agent2, self).step(obs)
        if _MOVE_SCREEN in obs.observation['available_actions']:
            ai_view = obs.observation['screen'][_AI_RELATIVE]
            # get the beacon coordinates
            beacon_xs, beacon_ys = get_beacon_location(ai_view)
            if not beacon_ys.any():
                return actions.FunctionCall(_NO_OP, [])
            # get the middle of the beacon and move there
            target = [beacon_ys.mean(), beacon_xs.mean()]
            return actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, target])
        else:
            return actions.FunctionCall(_SELECT_ARMY, [_SELECT_ALL])

 
下面是这个机器人的视频:
 

python -m pysc2.bin.agent --map MoveToBeacon --agent agent2.Agent2 --save_replay True

 
然后您可以在星际争霸2游戏客户端中查看重播:
 

 

正如你所看到的脚本人工智能玩灯塔游戏,并移动到灯塔的海军陆战队。这个脚本机器人在运行105集后平均每集获得25个奖励。这个奖励反映了我们的机器人在mingame计时器启动前(120秒)到达becaon的能力。我们开发的任何人工智能都应该至少和这个脚本机器人一样好,所以一次训练的平均分是25分。接下来,我们将使用强化学习实现一个实际的人工智能(学习如何玩)。
 

实现Q Learning人工智能

 
这种方法是一种称为“Q Learning”的方法的变体,它试图为游戏世界中的每一个状态学习一种称为“质量”的值,并将更高的质量归因于能够带来更多回报的状态。我们创建一个表(称为Qtable),其中游戏世界的所有可能状态都在y轴上,所有可能的操作都在x轴上。质量值存储在此表中,并告诉我们在任何可能的状态下应采取的操作。下面是Qtable的一个示例:

 

 
因此,当我们的AI选择了海军陆战队员,但它不在信标上时,state=(1,0),我们的AI将了解到移动到信标上的值(索引3处的动作)与处于相同状态的其他动作相比是最高的。当它没有选择海军陆战队员,并且它不在信标上时,state=(0,0),我们的AI了解到select海军陆战队员有最高的值(在索引1处的动作)。当它是一个信标,它选择了海军陆战队,state =(1,1),什么都不做是有价值的。
 
在update Q Table函数中更新Q表时,我们遵循以下公式:
 

 

它基本上是说把我们对采取行动的回报的估计值和实际采取行动的回报进行比较,然后把这个差异调整我们的Q值,以减少一点错误。我们的人工智能将获取状态信息并发出一个要采取的行动。我已经简化了这个世界状态和操作,以便更容易地学习Q表并保持代码简洁。我们给我们的代理人一个选择,而不是硬编码逻辑总是移动到信标。我们给了它6件可以做的事:
 
_NO_OP — 什么也不做。
 
_SELECT_ARMY — 选择海军陆战队。
 
__SELECT_POINT — 取消选择海军陆战队。
 
_MOVE_SCREEN — 移动到信标。
 
_MOVERAND — 移动到不是信标的随机点。
 
_MOVE_MIDDLE — 移到地图中间的一点。
 
这是我们的预培训代理Q表代码:
 

import math
import numpy as np
from pysc2.agents import base_agent
from pysc2.lib import actions
from pysc2.lib import features
from pysc2.env import sc2_env, run_loop, available_actions_printer
from pysc2 import maps
from absl import flags

_AI_RELATIVE = features.SCREEN_FEATURES.player_relative.index
_AI_SELECTED = features.SCREEN_FEATURES.selected.index
_NO_OP = actions.FUNCTIONS.no_op.id
_MOVE_SCREEN = actions.FUNCTIONS.Attack_screen.id
_SELECT_ARMY = actions.FUNCTIONS.select_army.id
_SELECT_POINT = actions.FUNCTIONS.select_point.id
_MOVE_RAND = 1000
_MOVE_MIDDLE = 2000
_BACKGROUND = 0
_AI_SELF = 1
_AI_ALLIES = 2
_AI_NEUTRAL = 3
_AI_HOSTILE = 4
_SELECT_ALL = [0]
_NOT_QUEUED = [0]
EPS_START = 0.9
EPS_END = 0.025
EPS_DECAY = 2500

possible_actions = [
    _NO_OP,
    _SELECT_ARMY,
    _SELECT_POINT,
    _MOVE_SCREEN,
    _MOVE_RAND,
    _MOVE_MIDDLE
]

def get_eps_threshold(steps_done):
    return EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)

def get_state(obs):
    ai_view = obs.observation['screen'][_AI_RELATIVE]
    beaconxs, beaconys = (ai_view == _AI_NEUTRAL).nonzero()
    marinexs, marineys = (ai_view == _AI_SELF).nonzero()
    marinex, mariney = marinexs.mean(), marineys.mean()

    marine_on_beacon = np.min(beaconxs) <= marinex <=  np.max(beaconxs) and np.min(beaconys) <= mariney <=  np.max(beaconys)

    ai_selected = obs.observation['screen'][_AI_SELECTED]
    marine_selected = int((ai_selected == 1).any())

    return (marine_selected, int(marine_on_beacon)), [beaconxs, beaconys]

class QTable(object):
    def __init__(self, actions, lr=0.01, reward_decay=0.9, load_qt=None, load_st=None):
        self.lr = lr
        self.actions = actions
        self.reward_decay = reward_decay
        self.states_list = set()
        self.load_qt = load_qt
        if load_st:
            temp = self.load_states(load_st)
            self.states_list = set([tuple(temp[i]) for i in range(len(temp))])

        if load_qt:
            self.q_table = self.load_qtable(load_qt)
        else:
            self.q_table = np.zeros((0, len(possible_actions))) # create a Q table

    def get_action(self, state):
        if not self.load_qt and np.random.rand() < get_eps_threshold(steps):
            return np.random.randint(0, len(self.actions))
        else:
            if state not in self.states_list:
                self.add_state(state)
            idx = list(self.states_list).index(state)
            q_values = self.q_table[idx]
            return int(np.argmax(q_values))

    def add_state(self, state):
        self.q_table = np.vstack([self.q_table, np.zeros((1, len(possible_actions)))])
        self.states_list.add(state)

    def update_qtable(self, state, next_state, action, reward):
        if state not in self.states_list:
            self.add_state(state)
        if next_state not in self.states_list:
            self.add_state(next_state)
        state_idx = list(self.states_list).index(state)
        next_state_idx = list(self.states_list).index(next_state)
        q_state = self.q_table[state_idx, action]
        q_next_state = self.q_table[next_state_idx].max()
        q_targets = reward + (self.reward_decay * q_next_state)
        loss = q_targets - q_state
        self.q_table[state_idx, action] += self.lr * loss
        return loss

    def get_size(self):
        print(self.q_table.shape)

    def save_qtable(self, filepath):
        np.save(filepath, self.q_table)

    def load_qtable(self, filepath):
        return np.load(filepath)

    def save_states(self, filepath):
        temp = np.array(list(self.states_list))
        np.save(filepath, temp)

    def load_states(self, filepath):
        return np.load(filepath)

class Agent3(base_agent.BaseAgent):
    def __init__(self, load_qt=None, load_st=None):
        super(Agent3, self).__init__()
        self.qtable = QTable(possible_actions, load_qt='agent3_qtable.npy', load_st='agent3_states.npy')

    def step(self, obs):
        '''Step function gets called automatically by pysc2 environment'''
        super(Agent3, self).step(obs)
        state, beacon_pos = get_state(obs)
        action = self.qtable.get_action(state)
        func = actions.FunctionCall(_NO_OP, [])

        if possible_actions[action] == _NO_OP:
            func = actions.FunctionCall(_NO_OP, [])
        elif state[0] and possible_actions[action] == _MOVE_SCREEN:
            beacon_x, beacon_y = beacon_pos[0].mean(), beacon_pos[1].mean()
            func = actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, [beacon_y, beacon_x]])
        elif possible_actions[action] == _SELECT_ARMY:
            func = actions.FunctionCall(_SELECT_ARMY, [_SELECT_ALL])
        elif state[0] and possible_actions[action] == _SELECT_POINT:
            ai_view = obs.observation['screen'][_AI_RELATIVE]
            backgroundxs, backgroundys = (ai_view == _BACKGROUND).nonzero()
            point = np.random.randint(0, len(backgroundxs))
            backgroundx, backgroundy = backgroundxs[point], backgroundys[point]
            func = actions.FunctionCall(_SELECT_POINT, [_NOT_QUEUED, [backgroundy, backgroundx]])
        elif state[0] and possible_actions[action] == _MOVE_RAND:
            beacon_x, beacon_y = beacon_pos[0].max(), beacon_pos[1].max()
            movex, movey = np.random.randint(beacon_x, 64), np.random.randint(beacon_y, 64)
            func = actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, [movey, movex]])
        elif state[0] and possible_actions[action] == _MOVE_MIDDLE:
            func = actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, [32, 32]])
        return func

 
通过下载两个文件(agent3_qtable.npy, agent3_states.npy),使用预先准备好的代码运行此代理:
 

python -m pysc2.bin.agent --map MoveToBeacon --agent agent3.Agent3

 
这个人工智能可以匹配的奖励25每集我们的脚本人工智能可以得到一次训练。它在地图周围尝试了很多不同的移动,并注意到在海洋和信标位置相互重叠的州,它会得到奖励。然后,它试图在每一个导致这种结果的州采取行动,以获得最大的回报。这是一段人工智能早期播放的视频:
 

 

这是一个视频,一旦它知道移动到灯塔提供奖励:
 

 

 

您还可以使用notebook代码培训自己的AI。
 

结论和未来打算

 
在这篇文章中,我想向你们展示三种编程人工智能行为,随机的,脚本化的,和Q Learning AI。
 
正如《悖论发展》(Paradox development)杂志的马丁·安沃德所:“复杂游戏的机器学习在这个时候主要是科幻小说。”我同意马丁的一些观点。我仍然认为在游戏中机器学习是有潜力的。剩下的部分是如何使用加权列表来制作一个好的人工智能;神经网络和加权列表是一样的,但它是学习的。最困难的是,马丁就在这里,复杂的交互对于一台计算机来说是很难拼凑和推理的。
 
这是我们的Q学习信标AI,这次以正常速度播放:
 

 

 

在这篇文章中,我坚持使用一个迷你游戏,因为我想要一些易于实验和编程的东西。pysc2足够复杂,可以工作。我们还没有训练我们的Q学习代理来识别并移动到beacon(我们只是将其作为一个选项)。这是可能的,但超出了本文介绍的范围。在未来,我们将做一个类似DQN的Deepmind的论文,也许可以解决这个更复杂的任务。
 

最后请订阅Generation Machine。这里是代码和notebook