这是一份从零开始的 Cosmos 攻略,在这里面我们将从头开始搭建运行比赛的环境,并且设计一个拥有基础策略的游戏AI。
要运行比赛,我们首先需要配置游戏的运行环境。
如果你本地已经配置好 Python 环境,你可以直接从 GitHub 上下载最新版本的代码。
如果本地并没有 Python 环境且是 Windows 系统,也可以直接从 Releases 页面下载一个包含 Python 3.12 的可执行文件的压缩包。
下载(并解压)后的文件夹如下所示:
要想运行一场比赛,我们需要配置这场比赛的详细信息,也就是修改位于根目录下的 config.json 文件。此文件默认内容如下:
{
"rounds": 1000,
"map": "maptestsmall",
"players": [
"example",
"example"
],
"debug": true
}
这将会在名为 maptestsmall 的地图上运行一次一共 1000 回合的比赛,其中双方都使用名为 example 的代码策略。
debug 参数被设为 true 后,我们的代码如果有任何语法错误或者运行时错误,将会打印完整信息并且中断比赛。与此同时,游戏的随机种子将会固定,这意味着对于相同的代码,无论何时运行都会用相同的输出。
这些功能对于调试开发非常有用,因此我们不作修改。
命令行运行 python main.py 即可开始比赛。如果你在配置时从 Releases 中下载了压缩包,可以直接双击 run.cmd 或者运行 python\python.exe main.py 运行。
执行结果:
> python main.py
胜者:example
原因:队伍所有实体的总能量更高。
========
[Team 0] example 剩余实体:
planet: 1 平均能量:332.00
destroyer: 5 平均能量:1283.00
miner: 6 平均能量:1314.33
scout: 69 平均能量:982.84
========
[Team 1] example 剩余实体:
planet: 1 平均能量:139.00
destroyer: 7 平均能量:470.71
miner: 7 平均能量:620.57
scout: 70 平均能量:764.24
========
中立实体:0
回放已保存至:./replays/replays-debug.rpl
此时,在 /replays/ 文件夹下已经出现了名为 replays-debug.rpl 的录像文件。
要想播放录像文件,可以访问在线回放工具。打开网页后会先黑屏一段时间加载资源,等出现星空背景后将录像文件拖入浏览器窗口即可开始播放。
另外一种方法是在这个工程中按步骤下载并配置客户端。本教程不会深入探讨这种办法,并且默认使用网页端工具。
播放刚刚的回放,我们可以看到两个 example的实例都只会进行随机的行为。在游戏末尾,它们仍然处于均势:
要增加一个可以运行的“玩家”代码,我们需要在 /src 文件夹下新建一个文件夹,文件夹名字只能含有字母和数字(不能含有空格),并且开头不能是数字。我们在 src 下创建一个名为 baseline1 的文件夹,这也将是我们要填写进配置文件中的 player 名字。
接下来,我们的文件夹里面必须有一个 main.py,并且里面必须定义一个 Player 类。我们使用以下模版,复制进 /src/baseline1/main.py 里:
from src import template
from core.api import *
class Player(template.Player):
def __init__(self):
super().__init__()
def run_planet(self):
pass
def run_destroyer(self):
pass
def run_miner(self):
pass
def run_scout(self):
pass
接下来,我们修改 config.json 中的值。为了方便起见,我们暂且将总轮数修改为 500 轮,这样我们开发时可以快速验证我们的代码:
{
"rounds": 500,
"map": "maptestsmall",
"players": [
"baseline1",
"example"
],
"debug": true
}
运行游戏,我们得到以下结果:
胜者:example
原因:最终兵器有更多的能量点。
[0, 500]
========
[Team 0] baseline1 剩余实体:
planet: 1 平均能量:1900.00
destroyer: 0 平均能量:0.00
miner: 0 平均能量:0.00
scout: 0 平均能量:0.00
========
[Team 1] example 剩余实体:
planet: 1 平均能量:711.00
destroyer: 45 平均能量:200.49
miner: 23 平均能量:477.48
scout: 28 平均能量:317.46
========
中立实体:0
回放已保存至:./replays/replays-debug.rpl
我们目前的 baseline1 能够成功作为一个玩家代码运行比赛,不过它的策略是什么也不做。我们需要让它变成一个“真正意义上”的策略。
在游戏一开始,我们只有初始的星球。要想控制其他实体,我们需要先用星球制造出它们。
观察地图,我们可以发现两个基地之间的距离较远,这意味着前期我们的基地相对安全,不会和其他玩家接触。我们应该采用探索控图和优先发育的策略。
同时,由于充能机制的存在,我们也需要考虑对于充能的投入。前期资源相对匮乏,我们应该优先发育。
也就是说,前期我们优先制造开采舰和侦查舰,后期当我们资源充足的时候,我们可以再大量制造战列舰和大量充能。
我们采用以下策略:
import random
def try_build(self, type, energy, direction=None):
# 先定义一个helper function,用以帮助建造实体
# 尝试建造一个单位,如果没有指定方向,则顺时针依次尝试每一个方向。
if self.controller.get_energy() < energy: # 如果能量不足,直接返回False
return False
if direction is None: # 如果没有指定方向,则尝试所有方向
direction = Direction.all_directions()
else:
direction = [direction]
for d in direction: # 遍历所有方向
if self.controller.can_build(type, d, energy):
self.controller.build(type, d, energy)
return True
return False
def run_planet(self):
# 所有的操作都通过self.controller执行,包括获取信息和行动
current_round = self.controller.get_round_num() # 获取当前回合数
current_energy = self.controller.get_energy() # 获取当前能量
if self.controller.is_ready(): # 如果现在冷却值足够行动,则开始决定建造什么
if current_round <= 100: # 前100回合(前期)
if current_energy > 100:
# 如果能量大于100,则有30%概率建造一个destroyer,70%概率建造一个miner
if random.random() > 0.7:
self.try_build(EntityType("destroyer"), 30)
else:
self.try_build(EntityType("miner"), 75)
elif current_energy > 30:
# 如果能量值在30到100之间,则建造一个miner
self.try_build(EntityType("miner"), 30)
else:
self.try_build(EntityType("scout"), 1) # 如果能量小于30,则建造一个最便宜的scout
else: # 后期
if current_energy > 200:
# 如果能量大于200,则有80%概率建造一个destroyer,20%概率建造一个miner
if random.random() > 0.2:
self.try_build(EntityType("destroyer"), 60)
else:
self.try_build(EntityType("miner"), 100)
elif current_energy > 75: # 如果能量值在75到200之间,制造小型destroyer
# 我们需要保证星球有一定能量储备,防止出现意外
self.try_build(EntityType("destroyer"), 30)
else:
self.try_build(EntityType("scout"), 1)
# 充能不占冷却,因此每轮都进行充能判定
if self.controller.get_charge_point() > 500:
return # 如果已经有大于500点的充能,那我们就不需要再充能了
if current_round < 100:
pts = 1 # 前100回合,充能1点,如果对面不充能的话就可以获得一点
elif current_round < 500: # 中期充能20%
pts = self.controller.get_energy() // 5
else: # 后期充能1/3
pts = self.controller.get_energy() // 3
if self.controller.can_charge(pts):
self.controller.charge(pts)
加上这些代码后运行比赛,我们发现我们的星球已经开始建造实体并充能了,不过现在建造出来的实体围在星球周围不移动,导致星球无法建造新的实体。
我们将在下一节开始编写其他实体的策略。
我们的实体需要探索地图。为了能够遍历整张地图,我们采用左手法则作为寻路策略:即遇到障碍物尝试左转:
def __init__(self):
super().__init__()
self.explore_dir = None # 当前探索方向
self.home_loc = None # 母星位置,初始为None
def explore(self): # 探索逻辑
if self.explore_dir is None:
# 如果探索方向没有被初始化,说明此时这个实体刚被创建出来。这意味着制造它的星球就在旁边。
# 我们将初始探索方向设置为这个实体面向的方向。
for entity in self.controller.sense_nearby_entities(radius=2, teams=[self.controller.get_team()]):
# 遍历四周所有的同队实体
if entity.type == EntityType("planet"):
# 找到母星后,将探索方向设置为母星指向当前实体的方向
self.explore_dir = entity.location.direction_to(self.controller.get_location())
self.home_loc = entity.location # 记录母星位置
break
if self.controller.is_ready(): # 如果实体可以行动
for _ in range(8): # 尝试8次探索,均失败说明四周全被堵死
if self.controller.can_move(self.explore_dir):
self.controller.move(self.explore_dir)
return
self.explore_dir = self.explore_dir.rotate_left() # 左手法则
这样的话,我们就写好了一个通用的探索方法。接下来,我们开始实现侦察舰的算法。
考虑到侦察舰的主要目标是消灭对方的开采舰,我们优先判定周围是否能找到敌对开采舰,然后再探索:
def run_scout(self):
# 检查四周是否有敌人的miner
for entity in self.controller.sense_nearby_entities(teams=self.controller.get_opponent()):
if entity.type == EntityType("miner"):
# 如果发现敌人的miner,则攻击它
if self.controller.can_analyze(entity.location):
self.controller.analyze(entity.location)
return # 如果直接分析成功,则进入冷却,可以直接跳过回合
# 如果不能分析,那么说明要么自身在冷却中或者距离不够
# 我们直接向敌人的方向接近
d = self.controller.get_location().direction_to(entity.location)
if self.controller.can_move(d):
self.controller.move(d)
return # 如果移动成功,同理,可以直接跳过回合
# 如果没有发现敌人的miner,则继续探索
self.explore()
运行比赛,我们可以看到侦察舰已经会自发开始探索并且消灭 example 的开采舰。
对于战列舰,我们采用一个很简单的算法:优先攻击敌方星球,其次探索地图。代码很大程度上和侦察舰一样:
def run_destroyer(self):
# 检查四周是否有敌方planet
for entity in self.controller.sense_nearby_entities(teams=self.controller.get_opponent()):
if entity.type == EntityType("planet"):
# 如果发现敌人的planet,为了最大化伤害,我们优先接近
d = self.controller.get_location().direction_to(entity.location)
if self.controller.can_move(d):
self.controller.move(d)
return
# 如果无法更接近了,我们就直接攻击
dis = self.controller.get_location().distance_to(entity.location)
if self.controller.can_overdrive(dis):
self.controller.overdrive(dis)
return
# 如果没有发现敌人的planet,则继续探索
self.explore()
此时运行比赛,我们发现第一波建造的开采舰在300回合后进化成的战列舰已经能够把 example 的基地攻占下来了!
对于开采舰,我们同样使用一种很基本的策略:如果检测到敌方侦察舰,无论如何直接反方向逃跑。否则,在距离母星一定距离内使用 explore 方法移动。以下是代码实现:
def run_miner(self):
if self.explore_dir is None:
self.explore()
return # 如果探索方向没有被初始化,则直接开始explore
if not self.controller.is_ready():
return # 如果实体不能行动,则直接返回
# 检查四周是否有敌人的scout
for entity in self.controller.sense_nearby_entities(teams=self.controller.get_opponent()):
if entity.type == EntityType("scout"):
# 如果发现敌人的scout,则向反方向逃跑
d = entity.location.direction_to(self.controller.get_location()) # 对方指向自己的方向
if self.controller.can_move(d):
self.controller.move(d)
return # 直接逃跑
# 如果没有发现敌人的scout,则继续探索
dis = self.controller.get_location().add(self.explore_dir).distance_to(self.home_loc) # 探索后距离母星的距离
for _ in range(8): # 尝试8次移动,均失败说明四周全被堵死
if dis > 20: # 如果距离母星大于20,则旋转方向
self.explore_dir = self.explore_dir.rotate_left() # 左手法则
else:
self.explore() # 如果距离母星小于20,则继续探索
dis = self.controller.get_location().add(self.explore_dir).distance_to(self.home_loc)
此时,运行游戏,我们已经能够发现 baseline1 能够稳定胜过 example 了。
此时,我们将 config.json 修改为标准游戏的配置:
{
"rounds": 1000,
"map": "maptestsmall",
"players": [
"baseline1",
"example"
],
"debug": true
}
并且确保 baseline1 的实现与以下相符:
import random
from src import template
from core.api import *
class Player(template.Player):
def __init__(self):
super().__init__()
self.explore_dir = None # 当前探索方向
self.home_loc = None # 母星位置,初始为None
def try_build(self, type, energy, direction=None):
# 先定义一个helper function,用以帮助建造实体
# 尝试建造一个单位,如果没有指定方向,则顺时针依次尝试每一个方向。
if self.controller.get_energy() < energy: # 如果能量不足,直接返回False
return False
if direction is None: # 如果没有指定方向,则尝试所有方向
direction = Direction.all_directions()
else:
direction = [direction]
for d in direction: # 遍历所有方向
if self.controller.can_build(type, d, energy):
self.controller.build(type, d, energy)
return True
return False
def run_planet(self):
# 所有的操作都通过self.controller执行,包括获取信息和行动
current_round = self.controller.get_round_num() # 获取当前回合数
current_energy = self.controller.get_energy() # 获取当前能量
if self.controller.is_ready(): # 如果现在冷却值足够行动,则开始决定建造什么
if current_round <= 100: # 前100回合(前期)
if current_energy > 100:
# 如果能量大于100,则有30%概率建造一个destroyer,70%概率建造一个miner
if random.random() > 0.7:
self.try_build(EntityType("destroyer"), 30)
else:
self.try_build(EntityType("miner"), 75)
elif current_energy > 30:
# 如果能量值在30到100之间,则建造一个miner
self.try_build(EntityType("miner"), 30)
else:
self.try_build(EntityType("scout"), 1) # 如果能量小于30,则建造一个最便宜的scout
else: # 后期
if current_energy > 200:
# 如果能量大于200,则有80%概率建造一个destroyer,20%概率建造一个miner
if random.random() > 0.2:
self.try_build(EntityType("destroyer"), 60)
else:
self.try_build(EntityType("miner"), 100)
elif current_energy > 75: # 如果能量值在75到200之间,制造小型destroyer
# 我们需要保证星球有一定能量储备,防止出现意外
self.try_build(EntityType("destroyer"), 30)
else:
self.try_build(EntityType("scout"), 1)
# 充能不占冷却,因此每轮都进行充能判定
if self.controller.get_charge_point() > 500:
return # 如果已经有大于500点的充能,那我们就不需要再充能了
if current_round < 100:
pts = 1 # 前100回合,充能1点,如果对面不充能的话就可以获得一点
elif current_round < 500: # 中期充能20%
pts = self.controller.get_energy() // 5
else: # 后期充能1/3
pts = self.controller.get_energy() // 3
if self.controller.can_charge(pts):
self.controller.charge(pts)
def explore(self): # 探索逻辑
if self.explore_dir is None:
# 如果探索方向没有被初始化,说明此时这个实体刚被创建出来。这意味着制造它的星球就在旁边。
# 我们将初始探索方向设置为这个实体面向的方向。
for entity in self.controller.sense_nearby_entities(radius=2, teams=[self.controller.get_team()]):
# 遍历四周所有的同队实体
if entity.type == EntityType("planet"):
# 找到母星后,将探索方向设置为母星指向当前实体的方向
self.explore_dir = entity.location.direction_to(self.controller.get_location())
self.home_loc = entity.location # 记录母星位置
break
if self.controller.is_ready(): # 如果实体可以行动
for _ in range(8): # 尝试8次探索,均失败说明四周全被堵死
if self.controller.can_move(self.explore_dir):
self.controller.move(self.explore_dir)
return
self.explore_dir = self.explore_dir.rotate_left() # 左手法则
def run_destroyer(self):
# 检查四周是否有敌方planet
for entity in self.controller.sense_nearby_entities(teams=self.controller.get_opponent()):
if entity.type == EntityType("planet"):
# 如果发现敌人的planet,为了最大化伤害,我们优先接近
d = self.controller.get_location().direction_to(entity.location)
if self.controller.can_move(d):
self.controller.move(d)
return
# 如果无法更接近了,我们就直接攻击
dis = self.controller.get_location().distance_to(entity.location)
if self.controller.can_overdrive(dis):
self.controller.overdrive(dis)
return
# 如果没有发现敌人的planet,则继续探索
self.explore()
def run_miner(self):
if self.explore_dir is None:
self.explore()
return # 如果探索方向没有被初始化,则直接开始explore
if not self.controller.is_ready():
return # 如果实体不能行动,则直接返回
# 检查四周是否有敌人的scout
for entity in self.controller.sense_nearby_entities(teams=self.controller.get_opponent()):
if entity.type == EntityType("scout"):
# 如果发现敌人的scout,则向反方向逃跑
d = entity.location.direction_to(self.controller.get_location()) # 对方指向自己的方向
if self.controller.can_move(d):
self.controller.move(d)
return # 直接逃跑
# 如果没有发现敌人的scout,则继续探索
dis = self.controller.get_location().add(self.explore_dir).distance_to(self.home_loc) # 探索后距离母星的距离
for _ in range(8): # 尝试8次移动,均失败说明四周全被堵死
if dis > 20: # 如果距离母星大于20,则旋转方向
self.explore_dir = self.explore_dir.rotate_left() # 左手法则
else:
self.explore() # 如果距离母星小于20,则继续探索
dis = self.controller.get_location().add(self.explore_dir).distance_to(self.home_loc)
def run_scout(self):
# 检查四周是否有敌人的miner
for entity in self.controller.sense_nearby_entities(teams=self.controller.get_opponent()):
if entity.type == EntityType("miner"):
# 如果发现敌人的miner,则攻击它
if self.controller.can_analyze(entity.location):
self.controller.analyze(entity.location)
return # 如果直接分析成功,则进入冷却,可以直接跳过回合
# 如果不能分析,那么说明要么自身在冷却中或者距离不够
# 我们直接向敌人的方向接近
d = self.controller.get_location().direction_to(entity.location)
if self.controller.can_move(d):
self.controller.move(d)
return # 如果移动成功,同理,可以直接跳过回合
# 如果没有发现敌人的miner,则继续探索
self.explore()
运行比赛,我们可以得到以下结果:
胜者:baseline1
原因:最终兵器有更多的能量点。
[501, 0]
========
[Team 0] baseline1 剩余实体:
planet: 2 平均能量:65.50
destroyer: 122 平均能量:30.37
miner: 0 平均能量:0.00
scout: 340 平均能量:1.00
========
[Team 1] example 剩余实体:
planet: 0 平均能量:0.00
destroyer: 0 平均能量:0.00
miner: 0 平均能量:0.00
scout: 15 平均能量:22.47
========
中立实体:0
回放已保存至:./replays/replays-debug.rpl
看起来,这时候我们对上 example 的全随机策略已经可以以绝对优势拿下胜利了。
到这里,我们的《从零开始的 Cosmos 生活》也就告一段落了。在本教程中,我们从零开始设计了一个有完备策略的 AI,它制造了每一种实体,并且充分利用了每一种实体的所有技能。
当然,我们的 baseline1 还有很大的提升空间,例如设计驱逐舰对于其他单位的攻击逻辑,这能让你更快地结束比赛;又比如利用上每一个实体都具有的无线电功能,让你的实体之间高效传递信息;还可以进一步调整实体的制造逻辑,甚至完成更加高效的寻路系统……
如何呢?这些可能性是否能让你也感到心潮澎湃呢?
希望你在看完这篇文章后能够有所收获,并有信心设计出更强大的策略。
Good luck, and have fun!