游戏开发中的人工智能(十一):规则式 AI

接上文 游戏开发中的人工智能(十):模糊逻辑

本文内容:技术上而言,有限状态机和模糊逻辑都落在基于规则的方法这个大伞之下。本章将谈这些方法,以及其他变化的方法。


规则式 AI

本章我们要研讨基于规则的 AI 系统。基于规则的 AI 系统可能是真实世界和游戏软件 AI 中最为广泛使用的 AI 系统了。规则系统最简单的形式由一连串的 if-then 规则组成,用来推论或行动决策。从形式上来说,在第九章的有限状态机中,已经看过规则系统的一种形式:我们用规则处理状态的转换问题。第十章谈到模糊逻辑时,也看过另一种规则系统(模糊规则)。

规则系统基础

规则系统有两个主要的部分,一个是工作记忆,另一个是规则记忆

工作记忆储存已知的游戏世界信息,这部分是动态的。规则记忆储存设游戏设计师设计的的规则。当工作记忆符合规则记忆的某一条规则时,相应的行动就会被触发。或者,规则记忆中的规则也能修改工作记忆的内容。

为了说明规则系统,我们举个实时战略模拟游戏中科技树的例子。在实时战略模拟游戏中,玩家必须训练农民,建立设施以及收割农作物。与此同时计算机对手也会追踪玩家当前的科技状态进行评估并推论,更新自己的科技。玩家也可以以同样的方式评估计算机对手的科技状态。因此,玩家和计算机都必须排除侦察兵,收集信息,根据所收集到的信息做推论。(可以利用简单的规则系统达到这种效果)。图11-1 说明了科技树的构成。

这里写图片描述

例11-1 是实时策略游戏科技树的工作记忆内容。

//例11-1:工作记忆示例

enum TMemoryValue(Yes,No,Maybe,Unknown);

TMemoryValue peasants;    //农民
TMemoryValue Woodcutter;  //伐木工
TMemoryValue Stonemason;  //石匠
TMemoryValue Blacksmith;  //铁匠
TMemoryValue Barracks;    //兵营
TMemoryValue Fletcher;    //箭工
TMemoryValue WoodWalls;   //木栅栏
TMemoryValue StoneWalls;  //石墙
TMemoryValue Cavalry;     //骑兵
TMemoryValue FootSoldier; //步兵
TMemoryValue Spearman;    //矛兵
TMemoryValue Archer;      //弓箭手
TMemoryValue Temple;      //庙宇
TMemoryValue Priest;      //僧侣
TMemoryValue Crossbowman; //十字弓箭手
TMemoryValue Longbowman;  //长弓箭手

就此例而言,我们让工作记忆里的每个元素都以 TMemoryValue 类型声明,而且可以取下列四个值之一:Yes、No、Maybe 或 Unknown。主要目的是,让计算机对手知道当前玩家对手的科技状态。Yes 表示玩家有某种科技,No 表示没有。如果玩家满足所有获得某种科技的条件,但其状态尚未被侦察兵确认,则其值是 Maybe。如果计算机不知道玩家对某科技的能力,则取值 Unknown。

计算机可以收集玩家当前科技状态的事实,做法是派出侦察兵,并做观察。例如,如果计算机派出一名侦察兵,而侦察兵看见玩家建了庙宇,则 Temple 设为 Yes。不过在此之前,使用一组 if-then 规则,在侦测兵确认之前,计算机能根据既有事实推论玩家的科技状态。例如,看图11-1 ,如果玩家有伐木工和石匠,则有能力建庙宇,则 Temple 的值会是 Maybe。如例11-2 所示。

11-2:庙宇规则示例

if(Woodcutter==Yes && Stonemason==Yes && Temple==Unknown)
    Temple=Maybe;

推论也可以以反推的方式得到。例如,如果玩家被观察到有僧侣,则计算机可以推论,玩家一定有庙宇,因此,也一定有兵营、伐木工以及石匠。如例11-3 所示。

//例11-3:僧侣规则示例

if(Priest==Yes)
{
    Temple=Yes;
    Barracks=Yes;
    Woodcutter=Yes;
    Stonemason=Yes;
}

根据图11-1 的科技树还可以写出许多规则,例11-4 是可以写出的其他规则。

//例11-4:其他规则示例

if(Peasants==Yes && Woodcutter==Unknown)
    Woodcutter=Maybe;
if(Peasants==Yes && Stonemason==Unknown)
    Stonemason=Maybe;
…

如前所述,就此例而言能写的规则不止这些,你可以开发更多规则,包含如图11-1 所示的所有可能科技。思路是:你可以写这类规则,并在游戏中不断执行(GameCycle 时),以保持计算机对手看待玩家科技能力的最新图像,以决定如何部署攻防兵力。

此例让你大致了解规则系统的运作方式,实际上就是一组 if-then 规则。但是,注意,开发人员经常不用本节所用的 if 语句建构规则系统,因为直接把 if 语句写在程序里,会让某种推论难以达到。开发人员时常使用描述语言或 shell 语言,使他们能建立规则并予以修改,而不用修改源代码再重新编译。

对战游戏攻击预测

此例中,我们的目标是,在对战游戏中,预测人类对手的下一个招式。我们想让计算机对手,能够利用玩家最近出的招式以及玩家过去所出招式的某些模式,预测玩家下次要出什么招。如果计算机可以预测下一招,就能采取适当的反击、阻挡或闪躲动作,比如往侧边跳或往后退。这会让战斗模拟游戏有更强烈的真实感,给玩家新的挑战。

为了达到这种效果,我们要实现一个有学习能力的规则系统。让每条规则加权,强化某些规则,压抑另外一些规则,借此达到学习的效果。

为了让范例能在讨论的掌控范围内,我们做一些简化工作。假定玩家的招式可以分成挥拳、下踢、上踢。

工作记忆

例11-6 是工作记忆的操作方式。

//例11-6:工作记忆

enum TStrikes(Punch,LowKick,HighKick,Unknown);

struct TWorkingMemory
{
    TStrikes strikeA; //前前次攻击
    TStrikes strikeB; //前次攻击
    TStrikes strikeC; /预测的下次攻击
    //可以在这里加上其他元素,比如要怎么反击等
};

TWorkingMemory WorkingMemory; //全局工作记忆变量

规则

例11-7 是此例的规则类。这里我们没有直接写出 if-then 规则,我们以 TRule 对象数组表示规则记忆。

//例11-7:规则类

class TRule
{
    public:
        TRule();
        void SetRule(TStrikes A,TStrikes B,TStrikes C);

        TStrikes antecedentA; //前前次攻击
        TStrikes antecedentB; //前次攻击
        TStrikes consequentC; //预测的下次攻击

        bool matched; //工作记忆是否与规则记忆相匹配
        int weight;   //权值因子
};

TRule 规则类只有两个方法:SetRule( ) 和构造方法。构造方法是把 matched 赋初值 false,weight 赋为 0。我们以 SetRule( ) 设定其他成员:antecedentA、antecedentB、consequentC,由此就可以定义出一条规则。SetRule( ) 方法如例11-8 所示。

//例11-8:SetRule()方法

void TRule::SetRule(TStrikes A,TStrikes B,TStrikes C)
{
    antecedentA=A;
    antecedentB=B;
    consequentC=C;
}

此例需要几个全局变量,第一个是 WorkingMemory,如例11-6 所示。例11-9 是其他的全局变量。

//例11-9:全局变量

TRule Rules[NUM_RULES]; //存储规则记忆 TRule对象的数组,此例指定为27
int PreviousRuleFired;  //存储上一次游戏循环中启动的规则索引值

TStrikes Prediction;    //规则系统中所作的招式预测,技术上而言并不需要,因为预测招式都会存储在工作记忆中
TStrikes RandomPrediction; //存储随机产生的预测招式,用以比较随机和我们预测的成功率

int N;                  //存储预测次数
int NSuccess;           //成功预测次数
int NRandomSuccess;     //随机猜测成的次数

初始化

游戏开始时,我们必须对所有规则和工作记忆做初始化。例11-10 的 Initialize( ) 函数会完成此任务。

//例11-10:Initialize()函数

void TFom1::Initialize()
{
    Rules[0].SetRule(Punch,Punch,Punch);
    …
    Rules[26].SetRule(HighKick,HighKick,HighKick);

    WorkingMemory.strikeA=sUnknown;
    WorkingMemory.strikeB=sUnknown;
    WorkingMemory.strikeC=sUnknown;
    PreviousRuleFired= -1;

    N=0;
    NSuccess=0;
    NRandomSuccess=0;
    UpdateForm();
}

这里我们一共有27条规则,对应出拳、下踢、上踢这三招的所有可能组合模式。例如,第一条规则 Rules[0] 可以理解成这样:

if ( WorkingMemory.strikeA == Punch && WorkingMemory.strikeB == Punch)
then 
    WorkingMemory.strikeC = Punch

检视这些规则可以发现,任何时刻都有一条以上的规则可以吻合工作记忆中的事实。例如,如果招式A 和 B 都是出拳,则前三条规则都吻合,预测的招式可以是出拳、下踢或者上踢。此时我们用加权因子,协助我们找出要启动哪条规则。我们只用权重最高的规则。如果有两条或两条以上的规则有相同的权重,那就用最前面那一条。

预测招式

当游戏开始运行,每次玩家出招之后,我们都必须做招式预测。我们用函数 ProcessMove( ) 处理玩家出的每一招,并预测其下一招。如例11-11 所示。

//例11-11:ProcessMove()

TStrikes TForm1::ProcessMove(TStrikes move)
{
    int i;
    int RuleToFire= -1;

    //第一块:
    if(WorkingMemory.strikeA == sUnknown)
    {
        WorkingMemory.strikeA=move;
        return sUnknown;
    }
    if(WorkingMemory.strikeB == sUnknown)
    {
        WorkingMemory.strikeB=move;
        return sUnknown;
    }

    //第二块:
    //先处理前次预测,记录并调整权重
    N++;
    if(move==Prediction)
    {
        NSuccess++;
        if(PreviousRuleFired != -1)
            Rules[PreviousRuleFired].weight++;
    }
    else
    {
        if(PreviousRuleFired != -1)
            Rules[PreviousRuleFired].weight--;

        //增加应该启动规则的权重
        for(i=0;i<NUM_RULES;i++)
        {
            if (Rules[i].matched && (Rules[i].consequentC == move) )
            {
                Rules[i].weight++;
                break;
            }
        }
    }
    if(move == RandomPrediction)
        NRandomSuccess++;
    //删除旧值
    WorkingMemory.strikeA=WorkingMemory.strikeB;
    WorkingMemory.strikeB=move;

    //第三块:
    //开始做新预测
    for(i=0;i<NUM_RULES;i++)
    {
        if(Rules[i].antecedentA == WorikingMemory.strikeA && Rules[i].antecedentB == WorikingMemory.strikeB)
            Rules[i].matched=true;
        else
            Rules[i].matched=false;
    }
    //选出权重最高的规则
    RuleToFire= -1;
    for(i=0;i<NUM_RULES;i++)
    {
        if(Rules[i].matched)
        {
            if(RuleToFire == -1)
                RuleToFire=i;
            else if(Rules[i].weight > Rules[RuleToFire].weight)
                RuleToFire=i;
        }
    }
    //启动规则
    if(RuleToFire != -1)
    {
        WorikingMemory.strikeC=Rules[i].antecedentC;
        PreviousRuleFired=RuleToFire;
    }
    else
    {
        WorkingMemory.strikeC=sUnknown;
        PreviousRuleFired= -1;
    }
    return WorikingMemory.strikeC;
}

第一块
第一块是填写工作记忆。游戏开始时,工作记忆初始化之后,任何招式出击之前,工作记忆中只有 Unknown 值,这样使无法预测的,所以我们要在玩家开始出招后,从玩家那里搜集资料。

第一招存储在 WorkingMemory.strikeA 中,而 ProcessMove( ) 返回 Unknown。第二招打出后,ProcessMove( ) 再次被调用,第二招存储在 WorkingMemory.strikeB 中,ProcessMove( ) 依旧返回 Unknown。

第二块
ProcessMove( ) 的第一块是处理前次预测,也就是上一次调用 ProcessMove( ) 后所返回的预测招式。

第二块首先要确认前次预测时候有效。ProcessMove( ) 以 move 为参数。move 是玩家最近一次出的招。如果 move 等于存储在 Predicition 的前次预测招式,那么我们的预测就是成功的。我们递增 NSuccess,以更新成功率。然后我们我们强化上次启动的规则即增加该规则的权重。如果前次预测是错的,我们则要递减前次启动的规则权重。

接下来我们查看前次随机预测是否正确,正确就递增 NRandomSuccess。最后,我们更新工作记忆中的招式,以便做新预测,即 WoringMemory.strikeB 变成
WoringMemory.strikeA,而 move 变成 WoringMemory.strikeB 。

第三块
首先我们要找出符合工作记忆中事实的规则(第一个 for 循环)。配对步骤完成后,我们要从那些吻合的规则中挑选一条出来,即冲突解决(第二个 for 循环),循环工作完成之后,选定的规则的索引值会存储在 RuleToFire 中。要实际启动规则,只需要把 Rules[RuleToFire] 的 consequentC 赋值给 WorkingMemory.strikeC 即可。

ProcessMove( ) 把要启动的规则索引值 RuleToFire 存储在 PreviousRuleFired中,下次 ProcessMove( )被调用时,会在第二块使用。最后,ProcessMove( ) 返回预测的招式。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录 目录 1 Graph 图论 3 | DAG 的深度优先搜索标记 3 | 无向图找桥 3 | 无向图连通度(割) 3 | 最大团问题 DP + DFS 3 | 欧拉路径 O(E) 3 | DIJKSTRA 数组实现 O(N^2) 3 | DIJKSTRA O(E * LOG E) 4 | BELLMANFORD 单源最短路 O(VE) 4 | SPFA(SHORTEST PATH FASTER ALGORITHM) 4 | 第 K 短路(DIJKSTRA) 5 | 第 K 短路(A*) 5 | PRIM 求 MST 6 | 次小生成树 O(V^2) 6 | 最小生成森林问题(K 颗树)O(MLOGM). 6 | 有向图最小树形图 6 | MINIMAL STEINER TREE 6 | TARJAN 强连通分量 7 | 弦图判断 7 | 弦图的 PERFECT ELIMINATION 点排列 7 | 稳定婚姻问题 O(N^2) 7 | 拓扑排序 8 | 无向图连通分支(DFS/BFS 邻接阵) 8 | 有向图强连通分支(DFS/BFS 邻接阵)O(N^2) 8 | 有向图最小点基(邻接阵)O(N^2) 9 | FLOYD 求最小环 9 | 2-SAT 问题 9 Network 网络流 11 | 二分图匹配(匈牙利算法 DFS 实现) 11 | 二分图匹配(匈牙利算法 BFS 实现) 11 | 二分图匹配(HOPCROFT-CARP 的算法) 11 | 二分图最佳匹配(KUHN MUNKRAS 算法 O(M*M*N)) 11 | 无向图最小割 O(N^3) 12 | 有上下界的最小(最大)流 12 | DINIC 最大流 O(V^2 * E) 12 | HLPP 最大流 O(V^3) 13 | 最小费用流 O(V * E * F) 13 | 最小费用流 O(V^2 * F) 14 | 最佳边割集 15 | 最佳点割集 15 | 最小边割集 15 | 最小点割集(点连通度) 16 | 最小路径覆盖 O(N^3) 16 | 最小点集覆盖 16 Structure 数据结构 17 | 求某天是星期几 17 | 左偏树 合并复杂度 O(LOG N) 17 | 树状数组 17 | 二维树状数组 17 | TRIE 树(K 叉) 17 | TRIE 树(左儿子又兄弟) 18 | 后缀数组 O(N * LOG N) 18 | 后缀数组 O(N) 18 | RMQ 离线算法 O(N*LOGN)+O(1) 19 | RMQ(RANGE MINIMUM/MAXIMUM QUERY)-ST 算法 (O(NLOGN + Q)) 19 | RMQ 离线算法 O(N*LOGN)+O(1)求解 LCA 19 | LCA 离线算法 O(E)+O(1) 20 | 带权值的并查集 20 | 快速排序 20 | 2 台机器工作调度 20 | 比较高效的大数 20 | 普通的大数运算 21 | 最长公共递增子序列 O(N^2) 22 | 0-1 分数规划 22 | 最长有序子序列(递增/递减/非递增/非递减) 22 | 最长公共子序列 23 | 最少找硬币问题(贪心策略-深搜实现) 23 | 棋盘分割 23 | 汉诺塔 23 | STL 的 PRIORITY_QUEUE 24 | 堆栈 24 | 区间最大频率 24 | 取第 K 个元素 25 | 归并排序求逆序数 25 | 逆序数推排列数 25 | 二分查找 25 | 二分查找(大于等于 V 的第一个值) 25 | 所有数位相加 25 Number 数论 26 1 |递推求欧拉函数 PHI(I) 26 |单独求欧拉函数 PHI(X) 26 | GCD 最大公约数 26 | 快速 GCD 26 | 扩展 GCD 26 | 模线性方程 A * X = B (% N) 26 | 模线性方程组 26 | 筛素数 [1..N] 26 | 高效求小范围素数 [1..N] 26 | 随机素数测试(伪素数原理) 26 | 组合数学相关 26 | POLYA 计数 27 | 组合数 C(N, R) 27 | 最大 1 矩阵 27 | 约瑟夫环问题(数学方法) 27 | 约瑟夫环问题(数组模拟) 27 | 取石子游戏 1 27 | 集合划分问题 27 | 大数平方根(字符串数组表示) 28 | 大数取模的二进制方法 28 | 线性方程组 A[][]X[]=B[] 28 | 追赶法解周期性方程 28 | 阶乘最后非零位,复杂度 O(NLOGN) 29 递归方法求解排列组合问题 30 | 类循环排列 30 | 全排列 30 | 不重复排列 30 | 全组合 31 | 不重复组合 31 | 应用 31 模串匹配问题总结 32 | 字符串 HASH 32 | KMP 匹配算法 O(M+N) 32 | KARP-RABIN 字符串匹配 32 | 基于 KARP-RABIN 的字符块匹配 32 | 函数名: STRSTR 32 | BM 算法的改进的算法 SUNDAY ALGORITHM 32 | 最短公共祖先(两个长字符串) 33 | 最短公共祖先(多个短字符串) 33 Geometry 计算几何 34 | GRAHAM 求凸包 O(N * LOGN) 34 | 判断线段相交 34 | 求多边形重心 34 | 三角形几个重要的点 34 | 平面最近点对 O(N * LOGN) 34 | LIUCTIC 的计算几何库 35 | 求平面上两点之间的距离 35 | (P1-P0)*(P2-P0)的叉积 35 | 确定两条线段是否相交 35 | 判断点 P 是否在线段 L 上 35 | 判断两个点是否相等 35 | 线段相交判断函数 35 | 判断点 Q 是否在多边形内 35 | 计算多边形的面积 35 | 解二次方程 AX^2+BX+C=0 36 | 计算直线的一般 AX+BY+C=0 36 | 点到直线距离 36 | 直线与圆的交点,已知直线与圆相交 36 | 点是否在射线的正向 36 | 射线与圆的第一个交点 36 | 求点 P1 关于直线 LN 的对称点 P2 36 | 两直线夹角(弧度) 36 ACM/ICPC 竞赛之 STL 37 ACM/ICPC 竞赛之 STL 简介 37 ACM/ICPC 竞赛之 STL--PAIR 37 ACM/ICPC 竞赛之 STL--VECTOR 37 ACM/ICPC 竞赛之 STL--ITERATOR 简介 38 ACM/ICPC 竞赛之 STL--STRING 38 ACM/ICPC 竞赛之 STL--STACK/QUEUE 38 ACM/ICPC 竞赛之 STL--MAP 40 ACM/ICPC 竞赛之 STL--ALGORITHM 40 STL IN ACM 41 头文件 42 线段树 43 求矩形并的面积(线段树+离散化+扫描线) 43 求矩形并的周长(线段树+离散化+扫描线) 44
产生推理系统是一种基于人工智能的方法,通过使用产生规则来进行推理和决策。它可以在特定的领域内模拟人类的推理过程,帮助解决复杂的问题。 使用Python进行开发,我们可以利用Python的强大的数据处理和算法库来实现产生推理系统。通过使用MySQL作为数据库,我们可以存储和检索大量的数据,提高系统的效率和灵活性。 在开发过程,我们可以使用Tkinter作为Python的图形用户界面库,使系统具有更好的交互性和可视化效果。通过Tkinter,我们可以设计用户界面,与用户进行交互并展示系统的推理结果。 产生推理系统的主要工作流程如下: 1. 设计和编写产生规则:根据具体的问题领域,我们需要定义一系列的产生规则,规定问题的条件和推理的过程。 2. 数据管理:通过MySQL数据库来管理相关的数据,包括事实、规则和推理结果。 3. 用户交互:使用Tkinter编写用户界面,与用户进行交互,输入问题条件,执行推理操作。 4. 推理过程:根据用户输入的问题条件,系统会根据产生规则进行推理,逐步得出推理结果。 5. 结果展示:将推理结果以可视化的方展示给用户,提供准确的信息和解决方案。 通过产生推理系统,我们可以快速有效地解决特定领域的问题,提供准确的推理结果和解决方案。它可以应用于各个领域,如医疗诊断、智能推荐系统等,帮助人们更好地理解和解决现实生活的复杂问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值