帝国时代2决定版AI(人工智能)编写系列教程(入门篇)

什么是AI?

人工智能(Artificial Intelligence, AI)亦称机器智能,是指由人工制造出来的系统所表现出来的智能。通常人工智能是指通过普通电脑实现的智能。帝国时代2决定版的AI保存于*:\Steam\steamapps\common\AoE2DE\resources\_common\ai文件夹,一个帝国时代能识别的AI由一个.ai文件和一个同名的.per文件构成;
其中.ai永远为空白文件,.per文件包含了我们要编写的全部代码。

AI能够做什么?

单位的每一次进攻,多少人,什么兵种,单位带了多少资源,距离敌人多远,地图信号的位置,战略的判断,敌人有多少,敌人是什么策略,收到了多少进贡,外交策略等等等等,甚至现在可以检测单位血量等等。
只要是你在帝国中能判断的决定的,AI几乎都可以做到。
最典型的如不动的AI,让你的所有单位不动而不是乱跑,Boom2,强大的对战AI,能力超过很多中手。
AI的每一个参数基本相当于一个触发条件或效果,AI有多少参数?上千个,拥有AI,就像触发编辑器多出了很多很多功能。

可能需要用到的编辑工具

对人工智能没有基础的纯新人推荐直接使用Windows记事本编辑.per脚本;
有一定编程知识的推荐使用notepad++。需要查询更为详细的语法请查看帝国时代2决定版AI脚本教程

基本语法

接下来要了解一些AI的规格。 看这个句法:

(defrule   
(difficulty == hard)
=>
(chat-to-all "一二三四五六")   
(disable-self) )

当你用一个拥有上面句法的AI选择难度为难进入游戏,一开始就可以看到聊天显示:一二三四五六 。但选择难度为标准或别的, 都不能看到“一二三四五六”。

分析:
(defrule
(内容)
=>
(内容))

这是编写AI的基本格式,所有的句法都是如此,每一个这样子的句法 就是一个动作。   
(difficulty == hard) 这一句是说选择难度为“难”。   
(chat-to-all “一二三四五六”) 这一句是发送聊天信息:一二三四五六 。
(disable-self) 这一句是本条内容只执行一次。

我们可以将这AI语法格式理解为
            

(defrule         
(difficulty == hard)   ;(条件)
=>      
(chat-to-all "一二三四五六")  ;(效果)   
(disable-self)                ;(效果) )    

现在比较明确了,这句语法就是说:当选择的难度为 “难” 的时候 ,就发送聊天信息为:一二三四五六。所以当我们选择难度为别的时候便不会再发送聊天信息:一二三四五六。

另外,如果希望去掉条件,就像触发中只写效果,就需要将这个效果写成:

(defrule
(true)
=>
(chat-to-all "一二三四五六") 
(disable-self))

下面,我们来简单练习一下,可能需要一些英语基础,我找了一些比较简单的单词组。

事实(即条件):

can-build 现在能够建造

can-build-gate 现在能够建造城门

building-count 计算当前建筑数

cheats-enabled 能够作弊

map-size 检测地图的大小

wood-amount 检测木材量

动作(即效果):

train do-nothing什么也不做
attack-now开始进攻
build-gate建造城门
cc-add-resource增加自己的资源
chat-local自言自语
delete-unit删除单位
build-wall建造墙
train训练

以上都是语句的基本,并不是完整的语句。

下面,我们再来看一看不动的AI究竟写了什么。

(defrule
    (true)
=>
    (set-strategic-number sn-maximum-food-drop-distance 0);设置策略值:最大采集食物距离为0  
    (set-strategic-number sn-maximum-wood-drop-distance 0);设置策略值:最大采集木材距离为0
    (set-strategic-number sn-maximum-gold-drop-distance 0);设置策略值:最大采集金子距离为0
    (set-strategic-number sn-maximum-stone-drop-distance 0);设置策略值:最大采集石头距离为0
    (disable-self)
)
(defrule
    (true)
=>
    (set-strategic-number sn-percent-civilian-gatherers 0);设置策略值:采集者比例0
    (set-strategic-number sn-percent-civilian-builders 0);设置策略值:建造者比例0
    (set-strategic-number sn-percent-civilian-explorers 0);设置策略值:侦查者比例0
    (disable-self)
)
(defrule
    (true)
=>
    (set-strategic-number sn-task-ungrouped-soldiers 0);设置电脑玩家的未编组部队是否分散开并保卫城镇地区,0为不散开1为散开
    (disable-self)
)

又比如:

(defrule   
(unit-type-count-total knight-line < 12);检测所有的骑兵系数量总和是否少于12个 
(can-train knight-line);检测是否能够生产骑兵
=>  
(train knight-line));满足上述条件后就生产骑兵

(defrule   
(unit-type-count-total archer-line < 8)  ;检测先有弓箭手总数是否少于8个 
(can-train archer-line);检测是否能够生产弓箭手
=>  
(train archer-line));满足上述条件后就生产弓箭手

是不是很简单?当然,除了defrule,AI当中还有其他重要语句,如defconstload,分别是定义常数读取的作用。对于初学者来说,这两种暂时不需要掌握。如有兴趣自己研究一下。

基础资料类型

在人工智慧脚本编辑里,储存的资料有几种:用户自定义常数、策略值、目标及系统定义常数。

目标:目标就像一个数字的储存器,你可以将一个目标设为 -32767 到 32768 之间的数值,而将目标相加起来的话更能得到大于 32768 的数值,而目标编号范围则是从 1(非 0)到 512。
策略值:策略值与目标一样是储存了一个数字,不过这些储存在策略值里的数字会被游戏程式读取作为影响人工智慧游戏者表现的参数。在 Userpatch 里,策略值的基本范围是 -32767 到 32768,当然其中也有一些例外,详情可见相关参考文件(https://userpatch.aiscripters.net/reference.html)内对每个策略值的说明,此外有个别无特定作用的策略值其实可以用作目标。
用户自定义常数:用户自己透过 defconst 指令来进行定义的任何东西都用户自定义是常数(参看“常数”部分)。
  系统定义常数:游戏程式已经自动定义好给脚本去读取的常数,它们包括 militiaman 、 archer-line 以及 my-player-number 等,另请注意事实、动作并非常数(意思就是相关的关键词,像 set-goal 或 train 等)。

常数

常数透过 defconst 指令进行定义,其格式为:

        (defconst 常数名称 数值)

它的数值可以是一个数字或一组字串。

数字包括单位及建筑物编号、单位类别编号( 900 以上的编号)、纯数字、常数、策略值及其他各种东西,字串则是任何用引号括起来的文字,一些有效的例子包括有: castle-age 、 my-player-number 、 30 、 archery-class 、 militiaman-line 、 knight 、 sn-number-attack-groups 、 “A” 、 “Training knight” 。

你可以给一个常数取任何的名称,祇要该名称不与其他系统定义常数或你的用户自定义常数冲突就行了,系统定义常数包括 my-player-number 、 any-ally 、 scout-cavalry 及其他类似的资料,而事实与动作的名称都可以用作常数名称,因此 unit-type-count 将会是一个有效的常数名称,但将 militiaman 作为常数名称就可能让人工智慧游戏者无法顺利用它来训练民兵了。

定义一个与系统定义常数同名的常数固然会让调用它的指令出现问题,但另外还要注意人工智慧脚本编辑是区分大小写的,亦即 Castle 并不等同 castle ,因此你可以在命名一个叫 Castle 的常数同时却又不干涉其他與建筑城堡相關的指令。

至于字串常数用途其实比较有限,原因就是你无法利用它们进行运算或修改数值,并且祇有 chat-to-player 或 up-change-name 一类动作会用到它们,然而配合 load-randoms 你仍然可以做出随机交谈讯息或玩家名称的效果。

有同样数值的常数是通用的,因为游戏程式其实是将它们视为普通数字而已,它们的名称没有真正影响力,譬如你有一个叫 g-enable-training-knights 的常数以及一个叫 g-disable-military-training 的常数,两者数值一致如下:

        (defconst g-enable-training-knights 103)
        (defconst g-disable-military-training 103)

这样,你以后将 g-enable-training-knights 设为 1 就会将第 103 号目标设为 1 ,而你要调用 g-disable-military-training 时它的数值也会是 1 ,显然易见这会造成混乱,因此你得确定每个目标编号常数都拥有不同的数值。

一些定义常数的例子有:

        (defconst House 5) ;因为“House”不同于“house”,所以它仍然会生效
        (defconst TownUnderAttack "别碰我!") ;定义常数 TownUnderAttack 为一组字串
        (defconst platypus my-player-number) ; my-player-number 是系统定义常数
        (defconst g-kill-percent-chance 13) ;定义出会被目标事实解读为第 13 号目标的 g-kill-percent-chance

运算子与类型识别符

除了普通的运算子(>=, <=, !=, ==, <, >)之外,Userpatch 还提供了更多可用的运算子,它们有数学运算子、比较运算子以及类型识别符。
数学运算子可以用来做出差不多任何你能够想到的数学运算;比较运算子可以比较任何类型的两组资料,譬如常数对常数、目标对策略值、策略值对常数、目标对常数等;类型识别符用来告诉处理器下一个数值属于什么类型,三种类型包括常数、目标编号或策略值编号。
只要你懂得运用运算子,想做到这些都是可能的。

类型识别符:用来告诉处理器下一个数值属于什么类型,三种类型识别符有:

c: :识别为常数(包括系统定义常数)

g: :识别为目标编号

s: :识别为策略值编号

当中有一个可能让人感到混乱的地方,那就是任何数值都能被识别成为任何一种资料类型。你可以说 g: militiaman ,然后它会将民兵的单位编号(74)转化作目标编号 74 ,同一道理也适用于策略值及目标。这些名称其实也就是某个数值的标签罢了,故此根本可以用作任何类型都行,但请在混用它们前确定你真的知道自己在什么!

比较运算子:比较运算子与 1.0c 内的六种运算子相同。唯一例外的是,你现在可以配合类型识别符来让运算子进行更多不同的比较运算。举例来说,你可以说 6 g:>= 5 而非 6 >= 5 ,前者比较的是目标 5 内的数值,后者比较对象却祇是简单的数值 5 。

数学运算子:主要用于 up-modify- 系列动作,其格式为 (up-modify-[goal, sn, or const] a 数学运算子 b),当中 a 与 b 分别代表两个数值,各种数学运算子包括有:

*:将 a 的数值乘以 b 的数值。

/:将 a 的数值除以 b 的数值,并舍入为整数,例如 1.5 = 2 。

z/:将 a 的数值除以 b 的数值,并且向下舍入,例如 1.5 = 1 。

+:将 a 的数值加以 b 的数值。

-:将 a 的数值减以 b 的数值。

mod:将 a 的数值除以 b 的数值,并且回报余数。

%*:以 b 的数值作为百分比值,并将 a 乘以它再回报其百分比值,亦即 (a * b) / 100 * 100。

%/:以 b 的数值作为百分比值,并将 a 除以它再回报其百分比值,亦即 (a * 100) / b * 100。

=:将 a 的数值设为 b 的数值。(不要与“==”混淆了)

min:回报两者间数值较低的一方。

max:回报两者间数值较高的一方。

示例:

比较运算子的示例如下:

        (up-compare-goal g-temp c:> 5) ;检查 g-temp 的数值是否大于 5
        (up-compare-goal g-temp g:> 5) ;检查 g-temp 的数值是否大于第 5 号目标的数值
        (up-compare-goal g-temp s:> 5) ;检查 g-temp 的数值是否大于第 5 号策略值的数值
        (up-compare-goal g-temp c:> militiaman) ;检查 g-temp 的数值是否大于民兵单位编号(74)
        (up-compare-goal g-temp c:== militiaman) ;检查 g-temp 的数值是否等于民兵单位编号(74)
        (up-compare-sn 5 g:== militiaman) ;检查策略值 5 的数值是否等于民兵单位编号(74)

数学运算子的示例如下:首先分别定义 g-temp 及 g-number 的常数值为 3 和 5 。

    (defconst g-temp 3)
    (defconst g-number 5)

然后,假设每组数学运算开始之前,当时 g-temp 的数值已经改变为 8 而 g-number 的数值已经改变为 3 :

    (up-modify-goal g-temp c:+ g-number) ;由于使用了常数识别符 c: ,所以加到 g-temp 的数值应该是 5 ,而当时 g-temp 的数值为 8 ,因此 8 + 5 = 13 ,於是 g-temp 為 13 而 g-number 為 3 。
    (up-modify-goal g-temp g:+ g-number) ;将当时 g-temp 的数值 8 加以当时 g-number 的数值 3 ,因此 8 + 3 = 11 ,於是 g-temp 為 11 而 g-number 為 3 。
    (up-modify-goal g-temp g:- g-number) ;将当时 g-temp 的数值 8 减以当时 g-number 的数值 3 ,因此 8 - 3 = 5 ,於是 g-temp 為 5 而 g-number 為 3 。
    (up-modify-goal g-temp g:* g-number) ;将当时 g-temp 的数值 8 乘以当时 g-number 的数值 3 ,因此 8 * 3 = 24 ,於是 g-temp 為 24 而 g-number 為 3 。
    (up-modify-goal g-temp g:/ g-number) ;将当时 g-temp 的数值 8 除以当时 g-number 的数值 3 ,并舍入为整数,因此 8 / 3 = 2.666 = 3(舍入) ,於是 g-temp 為 3 而 g-number 為 3 。
    (up-modify-goal g-temp g:z/ g-number) ;将当时 g-temp 的数值 8 除以当时 g-number 的数值 3 ,并且向下舍入,因此 8 / 3 = 2.666 =  2(下舍),於是 g-temp 為 2 而 g-number 為 3 。
    (up-modify-goal g-temp g:mod g-number) ;将当时 g-temp 的数值 8 除以当时 g-number 的数值 3 ,并且回报余数,因此 8/3 = 2 余 2 ,於是 g-temp 為 2 而 g-number 為 3 。
    (up-modify-goal g-temp g:%* g-number) ;以当时 g-number 的数值 3 作为百分比值,并将当时 g-temp 的数值 8 乘以它,因此 (8 * 3) / 100 = 24 / 100 = 0.24 = 24%,於是 g-temp 為 24 而 g-number 為 3 。
    (up-modify-goal g-temp g:%/ g-number) ;以当时 g-number 的数值 3 作为百分比值,并将当时 g-temp 的数值 8 除以它,因此 (8 * 100) / 3 = 800 / 3 = 266.7 = 267%(舍入),於是 g-temp 為 267 而 g-number 為 3 。
    (up-modify-goal g-temp g:= g-number) ;将 g-temp 的数值设为当时 g-temp 的数值 3 ,於是 g-temp 為 3 而 g-number 為 3 。
    (up-modify-goal g-temp g:min g-number) ;向 g-temp 回报两者间数值较小的一方,因为当时 g-number 的数值 3 小于当时 g-temp 的数值 8 而会回报 3 ,於是 g-temp 為 3 而 g-number 為 3 。
    (up-modify-goal g-temp g:max g-number) ;向 g-temp 回报两者间数值较大的一方,因为当时 g-temp 的数值 8 大于当时 g-number 的数值 3 而会回报 8 ,於是 g-temp 為 8 而 g-number 為 3 。

目标

在 Userpatch 里,目标有很大的作用,它们可能用来做各式各样的事情,唯一限制大概就是某些指令未必接受它们。目标就跟常数一样可以接受任何数字,但是它们无法储存字串(文字),你可以利用它们来计算想要的部队数量、纪录军事优势、指定要训练的单位或安排策略值的数值,而要设定其数值则有两个不同的指令:

        (up-modify-goal 目标编号 类型识别符 运算子 数值)
        (set-goal 目标编号 数值)


up-modify-goal 是由几个部分组成:一是要修改的目标,其编号由 1 到最后的 512 ;二是 Userpatch 提供的运算式(参看“运算子”部分);三是要将该个目标修改成的数值,这也是最为有趣的部分,修改数值范围基本没有什么限制,可以是非字串常数,也可以是其他目标及策略值,相关用例如下:

        (up-modify-goal 1 g:* 12)


如果第 1 号目标被设为 32 而第 12 号目标被设为 5,这句指令就会将第 1 号目标与第 12 号目标的数值相乘,亦即 32 乘以 5 。表面看来,它好像是会将 1 乘以 12 ,但这两个数值其实都是目标编号而已,当然我们亦可以靠下列语句定义出代替两个目标编号的常数:

        (defconst goal 1)
        (defconst goal-modifier 12)


这样前面的指令就可以改写成为:

        (up-modify-goal goal g:* goal-modifier)


如此表述应该也会更容易理解了,但对处理器来说两组指令写法其实并没有任何分别,代码背后 goal 就是 1 而 goal-modifier 就是 12 ,所以上句其实还是 (up-modify-goal 1 g:* 12) 的意思。此外,这里不妨重申一次:常数是另一种数字资料,而 g: 部分的目标就是要告诉处理器接着的数字是一个目标而非常数。现在,完成上述操作之后,结果将是:第 1 号目标的数值为 160 而第 12 号目标的数值仍为 5,这是因为 up-modify-goal 祇会修改首个目标,第二个目标并不会受到影响。

至于另一个修改目标的指令是 set-goal ,它的作用方式如下:

        (set-goal 1 160)


它祇是将第 1 号目标设成 160 ,这其实没什么大的用处,唯一例外可能是填入一个随机读取数值的常数,例如一个叫 strategy (战略)的常数可能有不同的数值,那么 (set-goal 1 strategy) 就能让你透过 (goal 1 某数值) 或 (up-compare-goal 1 c:== 某数值) 来判断人工智慧游戏者采取的战略。

策略值概述

策略值与目标十分相似,它们也会储存一个数值,而该数值也可以用类似的指令作设定(set-strategic-number 及 up-modify-sn)。不同的是,策略值亦会改变人工智慧的游戏方式,它们就好比人工智慧的设定选项。多数策略值未必有太深入的影响,然而若能好好运用它们就能将人工智慧游戏者的水平提升到一个新的高度。就算你无意接触太多关于策略值的事,但要设计出一个完整的人工智慧的话,当中仍然有一些内容是不得不知的。

策略值主要透过两组指令进行设定:

        (set-strategic-number 策略值编号 数值)
        (up-modify-sn 策略值编号 运算子 数值)


以下是一些较重要的策略值:

sn-maximum-town-size

基本用途:这个策略值是用来设定能在距离城镇多远的地点建造建筑物,然而有些建筑物则不在此限,像是高塔、伐木厂、采矿营地、磨坊等。
进阶应用:这个策略值还可以用来攻击敌人,详情参看“进攻方法”部分。

sn-enable-new-building-system

基本用途:启动 Userpatch 的新式建造系统,并容许玩家同时建造多楝同种类的建筑物。
进阶应用:有什么吗?

sn-food-gatherer-percentage

sn-wood-gatherer-percentage

sn-stone-gatherer-percentage

sn-gold-gatherer-percentage

基本用途:这些都是相当重要的策略值,它们控制了采集每种资源的村民数量。为了保证经济增长效率,你很可能有需要持续地改变它们。
进阶应用:较高阶的用法包括透过复杂算式来设定其数值,以及各资源之间相互重叠的百分比值(即总数高于 100)。

sn-number-attack-groups

基本用途:设定会参与进攻的士兵队伍数量,而队伍大小则由 sn-minimum-attack-group-size 、 sn-maximum-attack-group-size 及 sn-attack-group-size-randomness 来设定。
进阶应用:参看“进攻方法”部分。

sn-maximum-food-drop-distance

sn-maximum-wood-drop-distance

sn-maximum-gold-drop-distance

sn-maximum-stone-drop-distance

基本用途:设定村民会在距离资源存放点(伐木厂、采矿营地、磨坊、城镇中心)多远的地点采集资源。
进阶应用:其实也没有吧。

sn-mill-max-distance

sn-camp-max-distance

基本用途:设定磨坊(mill)或伐木厂及采矿营地(camp)会在距离城镇多远的地点建造。
进阶应用:其实同样没有,除非是用一些设定得复杂点儿的方法

© 版权声明
THE END
喜欢就支持一下吧
点赞8赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

Captcha Code

取消
昵称表情代码图片

    请登录后查看评论内容