本节内容学习如何用sc2库和决策树实现打星际争霸。

依赖库、游戏下载与安装

deepmind之前为此写了sc2的环境。由于版本问题,我们使用BurnySc2提供的环境

GitHub - BurnySc2/python-sc2: A StarCraft II bot api client library for Python 3 · GitHub

pip install --upgrade burnysc2

请先前往暴雪官网下载游戏,并注册通行证和账号。

《星际争霸Ⅱ》国服回归

下好之后必须体验一下人族 异虫 星灵族的教程,以了解之后策略的编写。

地图下载

感谢@气泡橙汁Zz 的提供:
地图的下载地址是:https://aiarena.net/wiki/maps/#wiki-toc-sc2-ai-arena-season-23的Sc2 AI Arena Season 2/3标签下的download,但是这个网站访问比较困难,
我把自己下载好的地图的网盘链接放在这里:链接:https://pan.baidu.com/s/1G3U-lPI2yi6agz2_jSOKoA?pwd=6fep 提取码:6fep

在下载StarCraft2的文件夹下创建或打开Maps文件夹,将刚刚的地图文件都考进去

运行示例

在刚刚的仓库中可以看到例子代码,

我们先运行一下这个例子

!pip install --upgrade burnysc2
!pip install tensorflow==2.12.0 d2l==1.0.3 numpy==1.23.5 matplotlib pandas
from sc2 import maps
from sc2.player import Bot, Computer
from sc2.main import run_game
from sc2.data import Race, Difficulty
from sc2.bot_ai import BotAI
import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'
import nest_asyncio
nest_asyncio.apply()
class WorkerRushBot(BotAI):
    async def on_step(self, iteration: int):
        if iteration == 0:
            for worker in self.workers:
                worker.attack(self.enemy_start_locations[0])

run_game(maps.get("2000AtmospheresAIE"), [
    Bot(Race.Protoss, WorkerRushBot()),
    Computer(Race.Zerg, Difficulty.Medium)
], realtime=True)

run_game有三个参数,地图,和两个玩家,我们设置Agent代理星灵Protoss,电脑代理虫族Zerg,难度选择Medium,不加速。

目前Agent的操作只有一个,就是生成woker去攻击虫族,肯定会输。

关于己方 Bot 的信息

可以从这里看到sc2支持的信息

https://burnysc2.github.io/python-sc2/text_files/introduction.html

资源与人口

self.minerals: int          # 晶体矿数量
self.vespene: int           # 高能瓦斯数量
self.supply_army: int       # 军队人口(游戏开局为 0)
self.supply_workers: int    # 农民人口(开局为 12)
self.supply_cap: int        # 人口上限(虫族开局 14,人/神开局 15)
self.supply_used: int       # 已用人口(开局 12)
self.supply_left: int       # 剩余人口(虫族开局 2,人/神开局 3)

单位相关

self.warp_gate_count: Units  # 传送门数量(仅限神族)
self.idle_worker_count: int  # 空闲农民数量
self.army_count: int          # 军队单位总数
self.workers: Units           # 所有农民单位
self.larva: Units             # 所有幼虫(仅限虫族)
self.townhalls: Units         # 主基地(星核/孵化场/指挥中心等)
self.gas_buildings: Units     # 气矿建筑(精炼厂/萃取房/吸收体)
self.units: Units             # 所有己方单位(含幼虫、农民)
self.structures: Units        # 所有己方建筑(含主基地、气矿)

其他 Bot 信息

self.race: Race               # 己方种族(选随机也会显示实际分配的种族)
self.player_id: int           # 玩家 ID(双人局中为 1 或 2)
self.start_location: Point2   # 出生位置(第一个主基地位置)
self.main_base_ramp: Ramp      # 主基地斜坡信息(人族堵口专用)

关于敌方玩家的信息

self.enemy_units: Units       # 视野内的敌方单位(含隐形,不含钻地)
self.enemy_structures: Units  # 视野内的敌方建筑
self.enemy_start_locations: list[Point2]  # 敌方出生位置列表
self.blips: set[Blip]         # 感应塔探测到的敌方单位信号
self.enemy_race: Race         # 敌方种族(敌方选随机则永远显示随机)

其他地图 / 中立信息

self.mineral_field: Units     # 地图上所有矿点
self.vespene_geyser: Units    # 地图上所有气矿(包括已被占领的)
self.resources: Units         # 所有资源点(矿点 + 气矿)
self.destructables: Units     # 所有可摧毁岩石(不含主矿斜坡下的平台)
self.watchtowers: Units      # 地图上所有瞭望塔
self.all_units: Units         # 全场所有单位(己方 + 敌方 + 中立)

self.expansion_locations: dict[Point2, Units]  # 所有可扩张点位
self.game_data: GameData      # 单位、技能、升级数据
self.game_info: GameInfo      # 地图信息:寻路、建造位置、地形高度、视野、菌毯
self.state: GameState         # 每帧更新的游戏状态

额外信息

self.realtime: bool           # 是否为实时模式(是则 Bot 每帧计算时间受限)
self.time: float              # 当前游戏时间(秒)
self.time_formatted: str      # 格式化时间:分:秒

Bot可以采取的行动

https://burnysc2.github.io/python-sc2/bot_ai/index.html

请务必读完这个网页,内容很多,但是不看完很难理解后续代码。

BotAI 核心函数总结

一、资源与成本检查

函数 作用 返回值
can_afford(unit_type/ability_id/upgrade_id) 检查是否有足够资源(矿 / 气)+ 足够人口生产 / 研究 / 建造 bool
calculate_cost(item_id) 计算单位 / 建筑 / 技能 / 升级的总消耗(矿 + 气) Cost 对象
calculate_supply_cost(unit_type) 计算单位占用的人口 int

二、建造 / 训练 / 升级(核心操作层)

函数 作用 关键参数
build(building, near, ...) 自动找位 + 选农民 + 下建造命令 building: 建筑类型near: 参考位置 / 单位max_distance: 最大搜索距离placement_step: 搜索步长
expand_now(building=None, ...) 快速扩张(自动找分矿点位 + 建造主基地) building: 主基地类型(默认自动)location: 指定扩张位置
train(unit_type, amount=1) 自动训练单位(如农民 / 军队),自动扣资源与人口 单位类型、训练数量
research(upgrade_id, structure) 研究升级(需指定研究建筑) 升级 ID、研究建筑

三、单位 / 建筑状态检查

函数 作用 返回值
already_pending(unit_type) 统计正在建造 / 训练 / 研究 + 正在路上 的数量 float
already_pending_upgrade(upgrade_id) 检查升级研究进度 float (0 = 未开始,0~1 = 研究中,1 = 完成)
alert(alert_code) 检测当前帧是否触发指定警报(如建筑完成、攻击等) bool

四、经济与工人管理

函数 作用 说明
distribute_workers(resource_ratio=2) 自动分配工人采矿 / 采气 矿气比大于此值优先补气,反之优先补矿
assign_workers_to_gas(...) 手动分配工人到气矿(需自行实现) 库内无内置函数,需写逻辑

五、地图与位置相关

函数 作用
find_placement(unit_type, near, placement_step=2) 在指定位置附近寻找合法建造位置(避障 / 地形 / 菌毯)
find_expansion_locations(...) 返回地图所有可扩张点位(结合资源与地形)

六、聊天与信息

函数 作用
chat_send(message, team_only=False) 发送聊天信息(用于调试 / 嘲讽)

七、其他常用工具函数

函数 作用
calculate_unit_value(unit_type) 计算单位价值(用于经济评估)
can_cast(ability_id, unit) 检查单位是否可释放某技能(冷却 / 能量 / 状态)
can_attack(unit, target) 检查单位是否可攻击目标(距离 / 类型 / 状态)

机器人可执行的操作

游戏已经开始,现在你需要用采集到的资源建造单位或建筑。我假设你至少玩过一局《星际争霸 2》,了解基础操作,例如:虫族如何生产工蜂(Drone)(通过幼虫),人族如何生产SCV(通过指挥中心),神族如何生产探机(Probe)(通过星灵枢纽)。

我们接下来完成两个基本操作:如何建造?如何训练单位?

建造建筑

我们首先要建造大本营,那么建大本营需要用什么函数?

文档要确定
“要建造什么类型的建筑”
“是否负负担得起资源”
“有没有空闲工人来建造”
“建在哪里”


建筑类型可以写成 UnitTypeId.SPAWNINGPOOL(血池)。检查是否有足够资源可以用:if self.can_afford(UnitTypeId.SPAWNINGPOOL):

选择用来建造的工人会稍微复杂一些。你可以随机选一个农民:my_worker = self.workers.random

也可以选择距离目标建造位置最近的工人:my_worker = self.workers.closest_to(placement_position)

但这两种方式都有一个问题:选中的工人可能正在忙。通常我们会选择正在采矿或空闲的工人:my_worker = self.workers.filter(lambda worker: worker.is_collecting or worker.is_idle).random


最后是确定血池的建造位置。最简单的写法是:

map_center = self.game_info.map_center
placement_position = self.start_location.towards(map_center, distance=5)

但问题来了:你真的能把建筑放在那个位置吗?有没有菌毯?会不会被其他建筑或敌方单位挡住?

建筑布局其实非常难:你不想把建筑造在矿区里;你想留出足够空间让人族建筑能挂上附属挂件;你想在建筑之间留出 2x2 空间,防止执政官被卡住。


有一个函数可以自动检测合法位置:self.find_placement,它会在指定点附近寻找可建造区域。这个函数可能运行较慢:

map_center = self.game_info.map_center
position_towards_map_center = self.start_location.towards(map_center, distance=5)
placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1)
# 如果找不到位置会返回 None
if placement_position:

还有一个关键点:你绝对不想造多个血池。为了避免重复建造,你可以检查正在建造 + 已完成的血池数量是否为 0:

if self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0:
    # 建造血池

完整推荐写法(朝向地图中心建造血池)

if self.can_afford(UnitTypeId.SPAWNINGPOOL) and self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0:
    # 筛选:采矿/空闲 + 未分配任务的农民
    worker_candidates = self.workers.filter(lambda worker: (worker.is_collecting or worker.is_idle) and worker.tag not in self.unit_tags_received_action)
    
    # 农民列表可能为空
    if worker_candidates:
        map_center = self.game_info.map_center
        position_towards_map_center = self.start_location.towards(map_center, distance=5)
        
        # 寻找可建造位置
        placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1)
        
        if placement_position:
            # 选最近的农民建造
            build_worker = worker_candidates.closest_to(placement_position)
            build_worker.build(UnitTypeId.SPAWNINGPOOL, placement_position)

简化写法(使用自带函数 self.build)

你可以用更方便的 self.build 实现完全一样的效果,它会自动选农民、自动找位置

if self.can_afford(UnitTypeId.SPAWNINGPOOL) and self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0:
    map_center = self.game_info.map_center
    position_towards_map_center = self.start_location.towards(map_center, distance=5)
    await self.build(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1)

生产单位

假设你为机器人选择了虫族,并想要生产一只工蜂。可用的幼虫存储在 self.larva 中,你的机器人开局会拥有3 只幼虫。若想指定其中一只幼虫执行生产工蜂的指令,需要先选中一只幼虫。最简单的方式是:my_larva = self.larva.random

选中幼虫后,必须下达变异为工蜂的指令。

你可以通过 __call__ 函数下达指令:unit(ability)。使用前必须导入技能 ID:from sc2.ids.ability_id import AbilityId

此时,执行动作的代码为:my_larva(AbilityId.LARVATRAIN_DRONE)

综上,完整代码如下:

from sc2.ids.ability_id import AbilityId

my_larva = self.larva.random
my_larva(AbilityId.LARVATRAIN_DRONE)

重要注意事项:

指令会在 on_step 函数执行完毕、机器人通过 API 与星际 2 客户端通信后才会生效。这可能导致不符合预期的状态:

  • 幼虫数量仍显示为 3(self.larva.amount == 3
  • 晶体矿仍显示为 50(self.minerals == 50
  • 人口并未增加(self.supply_used == 12

预期行为应该是:幼虫数量减为 2,晶体矿变为 0,人口变为 13(因为正在生产的工蜂会占用人口)。

后两个问题可以通过另一种调用方式修复:

self.larva.random(AbilityId.LARVATRAIN_DRONE, subtract_cost=True, subtract_supply=True)

这些关键字参数是可选的。因为大部分指令是移动或攻击指令,而非训练或建造指令,所以只在特定指令中检查资源消耗,能让机器人运行效率更高。


实现相同功能的另外两种写法:

写法 2(推荐,更直观)
from sc2.ids.unit_typeid import UnitTypeId
self.larva.random.train(UnitTypeId.DRONE)

该方法会自动将单位类型 ID转换为生产该单位所需的技能 ID

写法 3(最简便,自动分配)
self.train(UnitTypeId.DRONE, amount=1)

该函数会自动寻找合适的生产单位(幼虫),并在下达训练指令后自动扣除资源与人口

如果性能对你很重要,建议直接向空闲且资源充足的建筑 / 单位下达训练指令


高效生产:尽可能多造工蜂

for loop_larva in self.larva:
    # 检查是否有足够资源与人口
    if self.can_afford(UnitTypeId.DRONE):
        loop_larva.train(UnitTypeId.DRONE)
        # 如果只想造一只,在这里加 break
    else:
        # 资源不足,停止生产
        break
  • self.can_afford:检查是否拥有足够的资源空闲人口来生产单位。
  • self.do:会自动增加人口计数并扣除资源消耗。

最终正确写法

警告:必须避免在同一帧同一只幼虫下达多次指令。

self.do 函数会自动将单位的标识加入 self.unit_tags_received_action(一个整数集合,每帧都会清空),用于标记已下达指令的单位,防止重复操作。

最终规范代码:

for loop_larva in self.larva:
    # 如果该幼虫已收到指令,跳过
    if loop_larva.tag in self.unit_tags_received_action:
        continue
    # 检查资源
    if self.can_afford(UnitTypeId.DRONE):
        loop_larva.train(UnitTypeId.DRONE)
    else:
        break

代码编写

我们用的是星灵,因此要修改代码,

首先看看是否存在主基地

import random
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId

class WorkerRushBot(BotAI):
    async def on_step(self, iteration: int):
        # 第一步:检查是否有主基地(开局就有,if 基本恒成立)
        if self.townhalls:
            # 随机选一个主基地(开局只有1个,就是出生的 Nexus)
            nexus = self.townhalls.random

            # 打印游戏状态(调试用,每帧输出资源、人口、单位数量)
            print(f"{iteration},minerals:{self.minerals},vespene:{self.vespene},supply:{self.supply_used}/{self.supply_cap},workers:{self.workers.amount},idle_workers:{self.idle_worker_count},nexus:{self.structures(UnitTypeId.NEXUS).amount},probes:{self.units(UnitTypeId.PROBE).amount},pylon:{self.structures(UnitTypeId.PYLON).amount}")
        # 🟣 极端情况:没有主基地了(比如被拆光)
        else:
            # 资源够就自动开分矿(重建主基地)
            if self.can_afford(UnitTypeId.NEXUS):
                self.expand_now()

其中,如果没有主基地,可以使用expand_now()重建:

  • 获取你的种族:通过 self.race 拿到当前 Bot 的种族(神族 / 人族 / 虫族)
  • 自动匹配对应主基地
    种族 自动匹配的主基地建筑
    神族(Protoss) UnitTypeId.NEXUS(星核 / 枢纽)
    人族(Terran) UnitTypeId.COMMANDCENTER(指挥中心)
    虫族(Zerg) UnitTypeId.HATCHERY(孵化场)
  • 自动执行扩张流程
    • 调用 self.get_next_expansion() 找到地图上最近的、未被占领的分矿点位
    • 自动筛选空闲 / 采矿的农民
    • 自动调用 self.find_placement() 找合法建造位置
    • 给农民下达建造主基地的命令
    • 自动扣除资源、处理人口

破局思路

有了主基地,就可以开始造各种单位了

游戏中星灵的战斗单位需要通过折跃建筑生产

而折跃建筑必须通过水晶塔供能,且增加单位容量也需要水晶塔

因此思路就是先向敌方出生点拓展水晶塔,再布置折跃单位,最后生产进攻单位。而这一切的前提就是有足够的矿、汽,需要这些资源就需要源源不断的Probe。需要Probe,就需要从主基地Nexus生产,生产Probe,就需要矿,需要矿,就需要布置初始的Probe采矿的任务:

资源生产与获取

Probe的生产与水晶塔的构建

思路明确开始写代码:

首先游戏初始会给一定量的Probe,我们全部投入采矿

# 自动分配农民采矿(内置函数,不用自己写分配逻辑,异步执行,所以加 await)
await self.distribute_workers()

然后,先从Nexus生产Probe,我们刚刚nexus=self.townhalls.random获取了主基地,可以判断是否负担得起can_afford(X),且主基地空闲is_idle然后生产Probe。

            # 🔴 最高优先级:持续造探机(Probe)
            if self.can_afford(UnitTypeId.PROBE) and nexus.is_idle:
                nexus.train(UnitTypeId.PROBE)  # 主基地直接训练,不用农民

然后开始建水晶塔,注意第一个水晶塔要在主基地附近建,在API中提供了build函数

其次参是修建的位置,我们填写near=nexus主基地附近。

然后要判断是否有足够的资源构建,也是can_afford()。

# 🟡 优先级2:造第一个水晶塔(Pylon)
            elif not self.structures(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) == 0:
                if self.can_afford(UnitTypeId.PYLON):
                    # 异步建造:自动选农民、找位置、下命令,必须加 await
                    await self.build(UnitTypeId.PYLON, near=nexus)

注意,要用await,
因为self.build() 是异步函数,内部要做「选农民→找位置→发建造命令」一系列耗时操作,await 确保这些操作完成后,再进入下一轮 on_step,避免重复造塔、农民冲突。

然后我们预定向敌人方向建五座塔,间隔8-15格。逻辑是找离敌方出生点最近的一座塔,然后向前扩展8-15格,调用build(),同理await。

            # 🟢 优先级3:向敌方方向持续铺水晶塔(最多5个)
            elif self.structures(UnitTypeId.PYLON).amount < 5:
                if self.can_afford(UnitTypeId.PYLON):
                    # 找离敌方出生点最近的已造水晶塔
                    target_pylon = self.structures(UnitTypeId.PYLON).closest_to(self.enemy_start_locations[0])
                    # 从这个水晶塔向敌方方向,走8-15格的随机距离,作为新水晶塔的位置
                    pos = target_pylon.position.towards(self.enemy_start_locations[0], random.randrange(8, 15))
                    # 异步建造新水晶塔,加 await
                    await self.build(UnitTypeId.PYLON, near=pos)

瓦斯的获取

瓦斯需要建造瓦斯吸收间获取。

在主基地15格内找气矿,然后判断是否负担得起且资源点上没有在建造的吸收间。

            # 修复吸收间报错:去掉 already_pending,改用 pending_builds 判断
            # elif self.structures(UnitTypeId.ASSIMILATOR) and not self.already_pending(UnitTypeId.ASSIMILATOR):
            #     for vespene in self.vespene_geyser.closer_than(15, nexus):
            #         if self.can_afford(UnitTypeId.ASSIMILATOR):
            #             await self.build(UnitTypeId.ASSIMILATOR, vespene)

            elif self.structures(UnitTypeId.ASSIMILATOR).amount < 2:
                for vespene in self.vespene_geyser.closer_than(15, nexus):
                    if self.can_afford(UnitTypeId.ASSIMILATOR):
                        worker = self.select_build_worker(vespene.position)
                        if worker is not None:
                            # 神族造气矿 官方唯一正确方法
                            worker.build_gas(vespene)
                            worker.stop(queue=True)

Protoss建吸收间【不可以】用老师的代码(注释段),会报错keyerror4444

KeyError: 4444
2026-04-11 19:10:40.101 | INFO     | sc2.sc2process:_close_connection:243 - Closing connection at 62906...
2026-04-11 19:10:40.108 | INFO     | sc2.sc2process:kill_all:36 - kill_switch: Process cleanup for 6 processes
------------------------------------------------
KeyError       Traceback (most recent call last)
Cell In[20], line 1
----> 1 run_game(maps.get("2000AtmospheresAIE"), [
      2     Bot(Race.Protoss, WorkerRushBot()),
      3     Computer(Race.Zerg, Difficulty.Hard)
      4 ], realtime=False)

必须参考官方examples:

参考来源:
https://github.com/BurnySc2/python-sc2/blob/develop/examples/protoss/threebase_voidray.py

77行

完整代码:

import random
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId

class WorkerRushBot(BotAI):
    async def on_step(self, iteration: int):
        # 1. 检查是否有主基地(开局就有,if 基本恒成立)
        if self.townhalls:
            nexus = self.townhalls.random  # 选一个主基地(开局只有1个)

            # 📊 调试打印:每帧输出资源、人口、单位/建筑数量
            print(f"{iteration},minerals:{self.minerals},vespene:{self.vespene},supply:{self.supply_used}/{self.supply_cap},workers:{self.workers.amount},idle_workers:{self.idle_worker_count},nexus:{self.structures(UnitTypeId.NEXUS).amount},probes:{self.units(UnitTypeId.PROBE).amount},pylon:{self.structures(UnitTypeId.PYLON).amount},forge:{self.structures(UnitTypeId.FORGE).amount},cannon:{self.structures(UnitTypeId.PHOTONCANNON).amount}")

            # ⚙️ 自动分配农民采矿(内置函数,异步执行,必须加await)
            await self.distribute_workers()

            # 🔴 最高优先级:持续造探机(Probe)
            if self.can_afford(UnitTypeId.PROBE) and nexus.is_idle:
                nexus.train(UnitTypeId.PROBE)  # 主基地直接训练,不用农民

            # 🟡 优先级2:造第一个水晶塔(Pylon)
            elif not self.structures(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) == 0:
                if self.can_afford(UnitTypeId.PYLON):
                    await self.build(UnitTypeId.PYLON, near=nexus)  # 造在主基地旁边

            # 🟢 优先级3:向敌方方向铺水晶塔(最多5个)
            elif self.structures(UnitTypeId.PYLON).amount < 5:
                if self.can_afford(UnitTypeId.PYLON):
                    # 找离敌方最近的已造水晶塔
                    target_pylon = self.structures(UnitTypeId.PYLON).closest_to(self.enemy_start_locations[0])
                    # 向敌方方向走8-15格,作为新水晶塔位置
                    pos = target_pylon.position.towards(self.enemy_start_locations[0], random.randrange(8, 15))
                    await self.build(UnitTypeId.PYLON, near=pos)  # 铺塔推进

            elif self.structures(UnitTypeId.ASSIMILATOR).amount < 2:
                for vespene in self.vespene_geyser.closer_than(15, nexus):
                    if self.can_afford(UnitTypeId.ASSIMILATOR):
                        worker = self.select_build_worker(vespene.position)
                        if worker is not None:
                            # 神族造气矿 官方唯一正确方法
                            worker.build_gas(vespene)
                            worker.stop(queue=True)

        # 🟣 兜底逻辑:主基地被拆光,自动开分矿重建
        else:
            if self.can_afford(UnitTypeId.NEXUS):
                await self.expand_now()  # 补了await,规范异步写法

运行一下

run_game(maps.get("2000AtmospheresAIE"), [
    Bot(Race.Protoss, WorkerRushBot()),
    Computer(Race.Zerg, Difficulty.Hard)
], realtime=False)

可以看到源源不断的Probe生产和塔被建造

防守建筑构建

一般的核心逻辑是:极限爆Probe → 铺水晶塔推进 → 造熔炉 → 堆光子炮 → 冲脸,专门用来打前期快攻。

在这个网址可以看到建筑树

https://liquipedia.net/starcraft2/Protoss_Units_(Legacy_of_the_Void)

如果我们要构建光子炮,就需要修熔炉。

依然是判断是否有建造过/被摧毁,如果没有就看看能否造的起,然后选择合适的位置

熔炉我们选择靠大本营的第一座水晶塔建

near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

            # 🔵 优先级4:造熔炉(Forge)
            elif not self.structures(UnitTypeId.FORGE):
                if self.can_afford(UnitTypeId.FORGE):
                    # 造在离主基地最近的水晶塔旁边(保证供电)
                    await self.build(UnitTypeId.FORGE, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

然后建造光子炮,要额外判断一下是否存在熔炉,然后也建在主基地附近

near=nexus

            # 🟠 优先级5:造光子炮(Photon Cannon,最多3个)
            elif self.structures(UnitTypeId.FORGE).ready and self.structures(UnitTypeId.PHOTONCANNON).amount < 3:
                if self.can_afford(UnitTypeId.PHOTONCANNON):
                    await self.build(UnitTypeId.PHOTONCANNON, near=nexus)  # 造在主基地附近防守/推进

完整代码:

import random
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId

class WorkerRushBot(BotAI):
    async def on_step(self, iteration: int):
        # 1. 检查是否有主基地(开局就有,if 基本恒成立)
        if self.townhalls:
            nexus = self.townhalls.random  # 选一个主基地(开局只有1个)

            # 📊 调试打印:每帧输出资源、人口、单位/建筑数量
            print(f"{iteration},minerals:{self.minerals},vespene:{self.vespene},supply:{self.supply_used}/{self.supply_cap},workers:{self.workers.amount},idle_workers:{self.idle_worker_count},nexus:{self.structures(UnitTypeId.NEXUS).amount},probes:{self.units(UnitTypeId.PROBE).amount},pylon:{self.structures(UnitTypeId.PYLON).amount},forge:{self.structures(UnitTypeId.FORGE).amount},cannon:{self.structures(UnitTypeId.PHOTONCANNON).amount}")

            # ⚙️ 自动分配农民采矿(内置函数,异步执行,必须加await)
            await self.distribute_workers()

            # 🔴 最高优先级:持续造探机(Probe)
            if self.can_afford(UnitTypeId.PROBE) and nexus.is_idle:
                nexus.train(UnitTypeId.PROBE)  # 主基地直接训练,不用农民

            # 🟡 优先级2:造第一个水晶塔(Pylon)
            elif not self.structures(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) == 0:
                if self.can_afford(UnitTypeId.PYLON):
                    await self.build(UnitTypeId.PYLON, near=nexus)  # 造在主基地旁边

            # 🟢 优先级3:向敌方方向铺水晶塔(最多5个)
            elif self.structures(UnitTypeId.PYLON).amount < 5:
                if self.can_afford(UnitTypeId.PYLON):
                    # 找离敌方最近的已造水晶塔
                    target_pylon = self.structures(UnitTypeId.PYLON).closest_to(self.enemy_start_locations[0])
                    # 向敌方方向走8-15格,作为新水晶塔位置
                    pos = target_pylon.position.towards(self.enemy_start_locations[0], random.randrange(8, 15))
                    await self.build(UnitTypeId.PYLON, near=pos)  # 铺塔推进

            elif self.structures(UnitTypeId.ASSIMILATOR).amount < 2:
                for vespene in self.vespene_geyser.closer_than(15, nexus):
                    if self.can_afford(UnitTypeId.ASSIMILATOR):
                        worker = self.select_build_worker(vespene.position)
                        if worker is not None:
                            # 神族造气矿 官方唯一正确方法
                            worker.build_gas(vespene)
                            worker.stop(queue=True)

            # 🔵 优先级4:造熔炉(Forge)
            elif not self.structures(UnitTypeId.FORGE):
                if self.can_afford(UnitTypeId.FORGE):
                    # 造在离主基地最近的水晶塔旁边(保证供电)
                    await self.build(UnitTypeId.FORGE, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

            # 🟠 优先级5:造光子炮(Photon Cannon,最多3个)
            elif self.structures(UnitTypeId.FORGE).ready and self.structures(UnitTypeId.PHOTONCANNON).amount < 3:
                if self.can_afford(UnitTypeId.PHOTONCANNON):
                    await self.build(UnitTypeId.PHOTONCANNON, near=nexus)  # 造在主基地附近防守/推进

        # 🟣 兜底逻辑:主基地被拆光,自动开分矿重建
        else:
            if self.can_afford(UnitTypeId.NEXUS):
                await self.expand_now()  # 补了await,规范异步写法

可以看到成功建造了三个光子炮和熔炉。

一转攻势

接下来开始训练进攻单位。

我们要训练虚空战舰VOIDRAY,就需要建造星门Stargate,建造星门需要建造控制核心CyberneticsCore,要建造控制核心,就需要建造传送门Gateway。

建造传送门

照常判断是否有建造过,是否负担得起,然后选址建造。

传送门要建造在靠近主基地的首个水晶塔附近。

# ------建设进攻设施------
# Gateway(传送门/兵营)
elif not self.structures(UnitTypeId.GATEWAY):
    if self.can_afford(UnitTypeId.GATEWAY):
        await self.build(UnitTypeId.GATEWAY, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

建造控制核心

照常判断是否有建造过,是否负担得起,然后选址建造。

控制核心要建造在靠近主基地的首个水晶塔附近。

# Cybernetic Core(控制核心)
elif not self.structures(UnitTypeId.CYBERNETICSCORE):
    if self.can_afford(UnitTypeId.CYBERNETICSCORE):
        await self.build(UnitTypeId.CYBERNETICSCORE, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

建造星门

照常判断是否有建造过,是否负担得起,然后选址建造。

星门要建造在靠近主基地的首个水晶塔附近。

# Stargate(星门)
elif not self.structures(UnitTypeId.STARGATE):
    if self.can_afford(UnitTypeId.STARGATE):
        await self.build(UnitTypeId.STARGATE, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

建造虚空舰队

这里注意一点,虚空舰队的优先级要高于Probe,否则一旦有矿就会生产Probe而不是舰队了。

我们需要把代码放在

            # 🔴 最高优先级:持续造探机(Probe)
            if self.can_afford(UnitTypeId.PROBE) and nexus.is_idle:
                nexus.train(UnitTypeId.PROBE)  # 主基地直接训练,不用农民

之前

首先确定建造数量小于10,然后检查检查星门是否就绪,如果建好了且空闲就开始建造,同时要看看是否负担得起。

            if self.units(UnitTypeId.VOIDRAY).amount < 10:
                for sg in self.structures(UnitTypeId.STARGATE).ready.idle:
                    if self.can_afford(UnitTypeId.VOIDRAY):
                        sg.train(UnitTypeId.VOIDRAY)

进攻代码

进攻的优先级是消灭敌方单位-消灭敌方建筑-消灭敌方主基地

# ------发动进攻------
# 触发条件:虚空辉光舰数量 > 3(凑够3艘就开始进攻)
if self.units(UnitTypeId.VOIDRAY).amount > 3:
    # 第一层:如果视野内有敌方单位(兵/农民)
    if self.enemy_units:
        # 遍历所有空闲的虚空舰
        for vg in self.units(UnitTypeId.VOIDRAY).idle:
            # 让虚空舰随机攻击一个敌方单位
            vg.attack(random.choice(self.enemy_units))
    # 第二层:如果没有敌方单位,但有敌方建筑
    elif self.enemy_structures:
        # 遍历所有空闲的虚空舰
        for vg in self.units(UnitTypeId.VOIDRAY).idle:
            # 让虚空舰随机攻击一个敌方建筑
            vg.attack(random.choice(self.enemy_structures))
    # 第三层:如果既没单位也没建筑(视野内没敌人)
    else:
        # 遍历所有空闲的虚空舰
        for vg in self.units(UnitTypeId.VOIDRAY).idle:
            # 让虚空舰直接冲向敌方出生点(主基地)
            vg.attack(self.enemy_start_locations[0])

完全胜利

完整代码:

下载依赖库

!pip install --upgrade burnysc2
!pip install tensorflow==2.12.0 numpy==1.23.5 matplotlib pandas 

导入依赖库

from sc2 import maps
from sc2.player import Bot, Computer
from sc2.main import run_game
from sc2.data import Race, Difficulty
from sc2.bot_ai import BotAI

import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'
import nest_asyncio
nest_asyncio.apply()

创建Agent

import random
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId

class WorkerRushBot(BotAI):
    async def on_step(self, iteration: int):
        if self.townhalls:
            nexus = self.townhalls.random

            # 状态打印(按老师要求补充完整)
            print(f"{iteration},minerals:{self.minerals},vespene:{self.vespene},supply:{self.supply_used}/{self.supply_cap},workers:{self.workers.amount},idle_workers:{self.idle_worker_count},nexus:{self.structures(UnitTypeId.NEXUS).amount},probes:{self.units(UnitTypeId.PROBE).amount},pylon:{self.structures(UnitTypeId.PYLON).amount},forge:{self.structures(UnitTypeId.FORGE).amount},cannon:{self.structures(UnitTypeId.PHOTONCANNON).amount},gateway:{self.structures(UnitTypeId.GATEWAY).amount},cybercore:{self.structures(UnitTypeId.CYBERNETICSCORE).amount},stargate:{self.structures(UnitTypeId.STARGATE).amount},voidray:{self.units(UnitTypeId.VOIDRAY).amount}")

            await self.distribute_workers()

            # ==============================================
            # 🔴 严格按老师顺序:虚空辉光舰(放在探机之前,最高优先级)
            # ==============================================
            if self.units(UnitTypeId.VOIDRAY).amount < 10:
                for sg in self.structures(UnitTypeId.STARGATE).ready.idle:
                    if self.can_afford(UnitTypeId.VOIDRAY):
                        sg.train(UnitTypeId.VOIDRAY)

            # 🟡 探机(次高优先级)
            if self.can_afford(UnitTypeId.PROBE) and nexus.is_idle:
                nexus.train(UnitTypeId.PROBE)

            # 🟢 水晶塔(第一个)
            elif not self.structures(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) == 0:
                if self.can_afford(UnitTypeId.PYLON):
                    await self.build(UnitTypeId.PYLON, near=nexus)

            # 🟢 按一定方向构建水晶塔(最多5个)
            elif self.structures(UnitTypeId.PYLON).amount < 5:
                if self.can_afford(UnitTypeId.PYLON):
                    target_pylon = self.structures(UnitTypeId.PYLON).closest_to(self.enemy_start_locations[0])
                    pos = target_pylon.position.towards(self.enemy_start_locations[0], random.randrange(8, 15))
                    await self.build(UnitTypeId.PYLON, near=pos)

            #             # 修复吸收间报错:去掉 already_pending,改用 pending_builds 判断
            # elif self.structures(UnitTypeId.ASSIMILATOR) and not self.already_pending(UnitTypeId.ASSIMILATOR):
            #     for vespene in self.vespene_geyser.closer_than(15, nexus):
            #         if self.can_afford(UnitTypeId.ASSIMILATOR):
            #             await self.build(UnitTypeId.ASSIMILATOR, vespene)

            elif self.structures(UnitTypeId.ASSIMILATOR).amount < 2:
                for vespene in self.vespene_geyser.closer_than(15, nexus):
                    if self.can_afford(UnitTypeId.ASSIMILATOR):
                        worker = self.select_build_worker(vespene.position)
                        if worker is not None:
                            # 神族造气矿 官方唯一正确方法
                            worker.build_gas(vespene)
                            worker.stop(queue=True)


            # 🟠 熔炉
            elif not self.structures(UnitTypeId.FORGE):
                if self.can_afford(UnitTypeId.FORGE):
                    await self.build(UnitTypeId.FORGE, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

            # 🟠 光子炮(最多3个)
            elif self.structures(UnitTypeId.FORGE).ready and self.structures(UnitTypeId.PHOTONCANNON).amount < 3:
                if self.can_afford(UnitTypeId.PHOTONCANNON):
                    await self.build(UnitTypeId.PHOTONCANNON, near=nexus)

            # 🟡 Gateway(传送门)
            elif not self.structures(UnitTypeId.GATEWAY):
                if self.can_afford(UnitTypeId.GATEWAY):
                    await self.build(UnitTypeId.GATEWAY, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

            # 🟡 Cybernetic Core(控制核心)
            elif not self.structures(UnitTypeId.CYBERNETICSCORE):
                if self.can_afford(UnitTypeId.CYBERNETICSCORE):
                    await self.build(UnitTypeId.CYBERNETICSCORE, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

            # 🟡 Stargate(星门)
            elif not self.structures(UnitTypeId.STARGATE):
                if self.can_afford(UnitTypeId.STARGATE):
                    await self.build(UnitTypeId.STARGATE, near=self.structures(UnitTypeId.PYLON).closest_to(nexus))

            # 🟣 兜底:主基地被拆,自动开矿
            else:
                if self.can_afford(UnitTypeId.NEXUS):
                    await self.expand_now()

            # ==============================================
            # 🎯 严格按老师代码:发动进攻(放在所有建造逻辑最后)
            # ==============================================
            if self.units(UnitTypeId.VOIDRAY).amount > 3:
                if self.enemy_units:
                    for vg in self.units(UnitTypeId.VOIDRAY).idle:
                        vg.attack(random.choice(self.enemy_units))
                elif self.enemy_structures:
                    for vg in self.units(UnitTypeId.VOIDRAY).idle:
                        vg.attack(random.choice(self.enemy_structures))
                else:
                    for vg in self.units(UnitTypeId.VOIDRAY).idle:
                        vg.attack(self.enemy_start_locations[0])

        # 兜底:无主基地时的扩张
        else:
            if self.can_afford(UnitTypeId.NEXUS):
                await self.expand_now()

运行Agent

run_game(maps.get("2000AtmospheresAIE"), [
    Bot(Race.Protoss, WorkerRushBot()),
    Computer(Race.Zerg, Difficulty.Hard)
], realtime=False)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐