Python AI в StarCraft II. Часть IV: создание AI-армии

Предыдущая статья — Python AI в StarCraft II. Часть III: гейзеры и экспансия.

Добро пожаловать в четвертую часть серии статей про использование искусственного интеллекта в игре Starcraft II. В ней мы займемся созданием армии.

До этого момента мы подходили к делу довольно методично. Нам нужны рабочие и нужно распределить их по трем участкам с полезными ископаемыми. Нам нужен газ, и поэтому мы должны расширяться, по крайней мере, до некоторой степени, чтобы создать приличное количество запасов для ведения боя. Но теперь нам нужно начать обдумывать стратегию.

Какие юниты мы будет создавать? Сколько? Юниты истощают наши запасы полезных ископаемых и топлива. Также может истощаться и сама популяция юнитов. И наконец, пожалуй, самый главный элемент игры — это время.

Мы считаем, что для простоты нам лучше начать с создания одного боевого подразделения. Со временем вы можете сделать свою армию немного более динамичной, и вам, вероятно, даже придется это сделать, но пока давайте просто создадим один отряд.

Мы, конечно, не эксперты, но немного поискав, мы обнаружили пару юритов, пригодных для ведения массивных военных операций. Это Сталкер (Stalker) и Луч Бездны (Void Ray).

Сталкер — наземный юнит, но также может принимать участие и в воздушных операциях. А Луч Смерти — воздушный, но по аналогии тоже может участвовать в наземных столкновениях. Луч Бездны несколько сложней в производстве, и, соответственно, оно займет больше времени. На данный момент проще создать Сталкера, поэтому мы им и займемся.

С этого момента игра, как и всякая стратегия, становится весьма сложной, так что держитесь! Зайдя еще раз на страницу, где описаны все юниты Протоссов, мы обнаружили, что путь к Сталкеру следующий: Нексус (Nexus) > Врата (Gateway) > Кибернетическое ядро (Cybernetics Core) > Сталкер (Stalker). Что же, вполне понятно. Обратите внимание, что Кибернетическое Ядро больше связано с исследованиями. Свои боевые единицы вы по-прежнему будете производить из Врат. Кибернетическое Ядро вам понадобится только одно (и надеемся, что оно не будет уничтожено), а вот Врат, вероятно, потребуется несколько — для ускорения процесса создания юнитов.

Наш код на настоящий момент имеет вот такой вид:

import sc2
from sc2 import run_game, maps, Race, Difficulty
from sc2.player import Bot, Computer
from sc2.constants import NEXUS, PROBE, PYLON, ASSIMILATOR


class SentdeBot(sc2.BotAI):
    async def on_step(self, iteration):
        await self.distribute_workers()  # in sc2/bot_ai.py
        await self.build_workers()  # workers bc obviously
        await self.build_pylons()  # pylons are protoss supply buildings
        await self.expand()  # expand to a new resource area.
        await self.build_assimilator()  # getting gas

    async def build_workers(self):
        # nexus = command center
        for nexus in self.units(NEXUS).ready.noqueue:
            # we want at least 20 workers, otherwise let's allocate 70% of our supply to workers.
            # later we should use some sort of regression algo maybe for this?

            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 nexuses.exists:
                if self.can_afford(PYLON):
                    await self.build(PYLON, near=nexuses.first)

    async def expand(self):
        if self.units(NEXUS).amount < 2 and self.can_afford(NEXUS):
            await self.expand_now()

    async def build_assimilator(self):
        for nexus in self.units(NEXUS).ready:
            vaspenes = self.state.vespene_geyser.closer_than(25.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))


run_game(maps.get("AbyssalReefLE"), [
    Bot(Race.Protoss, SentdeBot()),
    Computer(Race.Terran, Difficulty.Easy)
], realtime=False)

Теперь, чтобы начать, нам нужно создать необходимые строения. Любые новые здания нужно строить в пределах псионической матрицы, которая создана нашими пилонами. Поэтому все будем строить рядом с пилонами. Итак, возьмем любого пилона. Если у нас уже есть Врата, но нет Кибернетического Ядра, то его надо создать. Если нет Врат, то создадим сначала их.

[machinelearning_ad_block]

Добавим следующий код в наш метод on_step:

class SentdeBot(sc2.BotAI):
    async def on_step(self, iteration):
        await self.distribute_workers()
        await self.build_workers()
        await self.build_pylons()
        await self.build_assimilators()
        await self.expand()
        await self.offensive_force_buildings()

Теперь нам необходимо создать метод offensive_force_buildings. Сначала поищем ближайший пилон для строительства:

    async def offensive_force_buildings(self):
        if self.units(PYLON).ready.exists:
            pylon = self.units(PYLON).ready.random

Теперь, если есть Врата, давайте создадим Кибернетическое Ядро:

    async def offensive_force_buildings(self):
        if self.units(PYLON).ready.exists:
            pylon = self.units(PYLON).ready.random
            if self.units(GATEWAY).ready.exists:
                if not self.units(CYBERNETICSCORE):
                    if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE):
                        await self.build(CYBERNETICSCORE, near=pylon)

Готово. А если нам все же нужно было построить Врата, то вот код для них:

    async def offensive_force_buildings(self):
        if self.units(PYLON).ready.exists:
            pylon = self.units(PYLON).ready.random
            if self.units(GATEWAY).ready.exists:
                if not self.units(CYBERNETICSCORE):
                    if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE):
                        await self.build(CYBERNETICSCORE, near=pylon)
            else:
                if self.can_afford(GATEWAY) and not self.already_pending(GATEWAY):
                    await self.build(GATEWAY, near=pylon)

Теперь мы можем начать строить нашу армию. Давайте добавим еще один метод в наш метод on_step:

class SentdeBot(sc2.BotAI):
    async def on_step(self, iteration):
        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()

Далее, если у нас есть ресурсы и мы можем себе позволить такие затраты, приступим к строительству юнитов для нашей армии:

    async def build_offensive_force(self):
        for gw in self.units(GATEWAY).ready.noqueue:
            if self.can_afford(STALKER) and self.supply_left > 0:
                await self.do(gw.train(STALKER))

Полный код сейчас имеет следующий вид:

import sc2
from sc2 import run_game, maps, Race, Difficulty
from sc2.player import Bot, Computer
from sc2.constants import NEXUS, PROBE, PYLON, ASSIMILATOR, GATEWAY, \
 CYBERNETICSCORE, STALKER


class SentdeBot(sc2.BotAI):
    async def on_step(self, iteration):
        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()

    async def build_workers(self):
        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 nexuses.exists:
                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:
                if not self.units(CYBERNETICSCORE):
                    if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE):
                        await self.build(CYBERNETICSCORE, near=pylon)
            else:
                if self.can_afford(GATEWAY) and not self.already_pending(GATEWAY):
                    await self.build(GATEWAY, near=pylon)

    async def build_offensive_force(self):
        for gw in self.units(GATEWAY).ready.noqueue:
            if self.can_afford(STALKER) and self.supply_left > 0:
                await self.do(gw.train(STALKER))


run_game(maps.get("AbyssalReefLE"), [
    Bot(Race.Protoss, SentdeBot()),
    Computer(Race.Terran, Difficulty.Easy)
    ], realtime=False)

Теперь наша армия начинает формироваться:

army being built

На данный момент мы готовы приступить к командованию армией, чем мы и займемся в следующей статье.

Следующая статья — Python AI в StarCraft II. Часть V: командование армией.