一字棋游戏c语言_如何用AI设计一款棋类游戏?

0fd90fcd72c9bc8561cfdb7914feec57.gif

极大极小树作为传统AI领域的一个传统又经典的算法,有着广泛的应用,尤其是棋类AI。记得刚学C语言的时候,用控制台写了一个五子棋的程序。后来突发奇想,给它增加可以人机对战的AI,设计了一个简单的根据当前局面判断最优落子的AI。但是只能想到两手棋。为了提高AI的智商,学习了博弈树这个数据结构,现在给大家分享一下。

本文拿棋类游戏的AI设计作为背景,详细介绍AI的设计过程,引入一种叫做博弈树的数据结构,以及α-β剪枝在博弈树上面的应用。

博弈树主要应用在涉及双方互相博弈、对抗的游戏中,换言之就是『你死我活』的游戏中。既然在游戏AI里面应用广泛,接下来我们结合具体游戏来介绍。

47ad4f89fa9040c16d08a5e46f902e26.gif

1

极大极小树(Min-Max Tree)

首先介绍一个基本的数据结构,叫做极大极小树(Min-Max Tree)。

在设计对抗AI的游戏过程中,通常假设双方都是智商顶级的高手。因此,默认敌我双方都能够根据当前的局面,做出对自己一方最有利的决策。

然而,博弈通常是双方对抗,甚至是零和的博弈。也就是说,对对方最有利的决策,反过来就是对我方最不利的局面。

在轮到我们做出决策的时候,我们通常希望最大化我们的收益,叫做极大层,此时树的节点叫做极大层节点;在对手做决策的时候,对手希望最小化我们的收益,叫做极小层,此时树的节点叫做极小层节点。由于双方是交替做出决策,因此极大层、极小层通常是交替出现,这样的数据结构就叫做极大极小树(Min-Max Tree)。

假设我们已经有一个评价每种决策的收益的估值函数。对于极大层节点,如果我们知道了它的每一步决策的收益值,那么它总是会选择收益最大的那个决策,作为它的节点的收益值;反过来,对于极小层节点,它总是会选择收益最小的(对我方收益最小,就是对方收益最大)那个决策。

举个最简单的例子,分硬币游戏。

游戏规则是:一堆硬币,双方轮流将它分成大小不能相等的两堆,然后下一个人挑选任意一堆继续分下去,双方交替游戏,直到其中一个人无法继续分下去,则对方获得胜利。

假设我们刚开始有一堆7枚硬币,轮到我方先分。我需要找到当前应该怎么样分这堆硬币。能使我方利益最大化。这就需要构造出一棵极大极小树了。

首先,我们穷举所有的可能的状态。用矩形代表轮到我方做决策的极大层节点,用圆形代表轮到对方做决策的极小层节点,列举出所有的状态:

bd59a8e4c6809553e4817b5d3c0c609a.png

注意,到这里我们还没有进行估值 ,因此这还不算是极大极小树。下面我们来设计这个游戏的估值函数,很简单,如果当前局面,我们已经赢了,那么收益为+1,如果我们输掉了,那么收益为-1,这个游戏没有平局,所以只可能有这两种收益值。

刚开始估值的时候,我们还位于根节点,我们对于整棵树是一无所知的,就像下面这样:

cc25e215fc63a591cbfadacd38821557.png

我们想要获得根节点的估值,需要对根节点的子节点进行遍历,首先对第一个子节点进行遍历,发现还是不能判断它的值,继续遍历(深度优先)它的子节点,直到到达叶子节点:

afcb91dbd8aa59786e0ef634d62c153f.png

到了叶子节点,我们可以对它进行估值了,因为此时是极小层,需要对手进行决策,但是对手已经无法再继续分下去了。所以,它的收益值为+1:

636c8ba51d8126c40cf91acd1ba84ae0.png

这样获得了第一个叶子节点的估值,由于它的父节点只有唯一一个子节点,因此父节点的收益值也只能是+1,然后可以一直回溯上去,直到到达红色的节点:

806f0559840c5c5ed72d961b6e7188dc.png

此时,我们不能直接给红色节点赋值了,因为它还有别的子节点,我们继续遍历它的子节点:

af7cf76f041c11db2be6fae8813da88b.png

遍历到又一个叶子节点,发现它的收益是-1(我方失败),再回溯回去:

36935ef261dffb358bc1d17b87af9397.png

现在我们又回到了红色节点,现在它的所有子节点的收益值都已经获得了。注意,这个节点是一个极大层节点。我们说过,极大层节点总是会选择收益最大的子节点,它的子节点一个是+1一个是-1,因此,它的值应该是最大的那个+1。我们继续按照深度优先的顺序遍历:

f96be09b7106cd8b51409d765ffc760b.png

b787cf41e46a264332983be1d82694a7.png

到这里,当前红色的节点是一个极小层节点,它总是选择收益最小的决策,因此它的收益值是-1。 接下来,我们继续按照这个思路进行遍历,中间的过程我就省略了,最后的结果如下所示:

cf6d8c513d4bdd9e9e312df09683074b.png

9cc933a416488cb6a4b7b242e03fa360.png

到这里,我们的极大极小树已经构建完成了。

我们发现,当前的根节点的收益值居然是-1,也就是说,只要对方够聪明,我们无论如何都无法取胜。

这就很绝望了,但是仔细想想,我们假设的前提是,对方是聪明绝顶,不犯错误的高手。

我们知道无论如何都会失败,那可不可以赌对方会犯错呢。

这样一想,其实3种必败的决策还是有一定的优劣性的。比如,最右边那个子节点,它的所有子树跟子树的子树收益都是-1,也就是说,对方就算乱下,我们都必输;而中间跟左边那个子节点,如果对手下错了,还有一一定几率能够通往+1的叶子节点的。

因此,左边两个决策要比最右边的决策要相对好那么一点儿。在发现我们已经必败的时候,依然能够在决策中做一个取舍,选择败得不明显的那种决策。

对于分硬币这样相对简单的游戏,极大树极小树还是能够用得上的。但是它必须得穷举出所有的状态。再从终结状态开始,计算每个节点的估值,最后才能获得当前最优的决策。

2

评价函数(Evaluation function)

绝大部分的游戏,决策空间都相当庞大。即使是最简单的三子棋(又叫做“井”字棋,一字棋)。它的第一步有9种决策,然后对面有9*8=72种决策,....,最后一层的决策个数达到了 9! = 362880 种。

如此简单的游戏,在不做特殊处理的时候,都有几十万种决策(当然这个量级计算机还是能够hold住的)。它的棋盘大小仅仅是3 X 3,五子棋是15 X 15,围棋是19 X 19,想要穷举出所有决策,几乎是不可能的。

因此,我们不能每次都穷举出所有的结果,再去慢慢找最优决策。随着树的深度增加,我们的节点个数是指数级上升的。

我们不得不搜索到一定程度,就停止继续往下搜索。当我们停下来以后,这个时候,由于我们游戏还没有结束,我们如何判断当前的结果的好坏?

我们需要设计一个评价函数(Evaluation function)对于当前局面进行评分。这个评价函数如何设计?主要是根据不同的游戏,还有人类的日常经验来判断。

我当时设计五子棋AI的时候,就人为设计了一个评价当前局面的分数的函数。比如已经有5个子连成一线了,它就是最高分;如果有4个子连成一线,它就是次高分;还有双3......

这样就能根据局面,获得一个得分。当然,当对面调用这个评价函数的时候,获得的分数前面要取一个负号。因为对手的最高分,就是我们的最低分。

3

博弈树 与 α-β剪枝

有了评价函数,我们就可以随时终止我们的搜索了。因为对于任何局面,我们都能够给出一个收益得分,可以限定我们搜索的深度,随时结束搜索。

但是我们的搜索空间仍然非常庞大。因为最开始的几层,可做的决策是相当多的。

比如五子棋,第一步就有225种下法。而对手对应就有225*224=50,400种决策;再往下一层,就有225*224*223=11,239,200种。这才第三层,就已经快爆炸了。

一般五子棋的高手都能想到后面五六步,甚至十几步。想要与之对抗,我们必须得想办法减少我们的搜索数量,增加我们的搜索深度,这样我们的AI才能看得更远的未来,想得更多,这样棋力才会变强。

这里,我们用到了强大的α-β剪枝技术。它的思路就是,减少所有没有必要的搜索,及时终止,从而节省算力,同时又不能漏过所有可能的最优解。下面来详细介绍一下:

我们先来理解一下,怎么样的搜索是没有必要的,假设我们限定了搜索深度为3,我们从头开始搜索,如下:

9cc933a416488cb6a4b7b242e03fa360.png

我们从根节点往下搜,直到第一个叶子节点:

06d3225777e0f06edd0c5da902ab6994.png

此时,到达了第一个深度为3的节点,此时我们调用估值函数,假设我们获得它的收益为3,现在我们回头来看它的父节点:

898f0e21960f93616faba6d0e6d2fe58.png

由于,这个父亲节点是MIN节点,我们知道,它总是会选择子节点中最小值。现在,子节点已经出现了一个值为3。

现在仔细想想,如果我们继续获得它的子节点的收益,为一个比3要大的值,假设为12好了。那么当前的父节点,必然不会选择这个12,而会去选择3。因此,这个父亲节点的收益,无论如何,都不会超过3,那么它的取值范围,我们可以认为是:(-∞,3]。也就是说,我们的子节点,其实更新了它的父节点的收益的一个上界值,如图:

1998ecdf781a96cbb5568c12b4b7b17e.png

到这里,我们其实并没有进行剪枝,只是找到了一个父节点的上界值(β值),我们还是得继续搜索它的子节点:

598e1cb6f955776955fd3ee45651fa66.png

假如我们搜索到了12,我们依然试图更新父节点的上界值(β值),但是因为比3要大,更新失败了,继续搜索下一个,直到搜索完所有的当然父节点的所有子节点:

b9355d96ea470a6e27d512e4e7736d95.png

当它所有的子节点都被搜索完以后,我们其实可以知道当前节点的收益就是3了。这个时候我们可以修改它的下界为3,收益为3。

注意这个时候,其实跟之前的极大极小树的搜索过程没有区别,我们并没有进行任何的剪枝。接下来继续搜索:

054a47fcba05366643a1a0e461e45dad.png

我们确定了当前节点收益为3,再去看它的父节点,即根节点。根节点原本的收益值范围是(-∞,+∞)。现在我们找到了一个子节点收益是3。

根节点是一个MAX节点,跟之前相反,子节点的收益值3,可以用来更新的是根节点的下界(α值)。至于为什么,可以类比一下之前的。我们现在已经有搜索到一个3,如果我们以后搜索到比3小的值,那么根节点在取最大值的时候,肯定会选择更大的3,而不是其他值。因此最优解的下界就是3,不会再更小了。

我们带着根节点的取值区间[3, +∞)继续往下搜索,把这个区间赋给下一个子节点:

3d31cf3f79ad37d64d3ab7bc8d8bf36c.png

往下继续深度优先遍历,访问它的第一个子节点。此时到达设定的深度3,我们调用评价函数,假设评价函数返回值为2:

f4f38e94f1c2144dc89ea7b6c3023a23.png

注意,重点来了。

我们知道,当前子节点是一个收益为2的MAX节点。MAX节点可以更新父节点的上界。因此,父亲节点的上界,被修改成了2。 这里就出现了一个矛盾的区间[3,2],如下图:

caf6dbe77b01f3d0191403b621f40745.png

观察当前的节点,它的收益值的取值区间是[3,2]。这明显是不合理的,收益不可能下界是3,同时上界又是2。我们可以做出判断,这个节点无论如何都不可能是最优解。

由于这个区间已经产生了矛盾,我们可以直接给当前节点判死刑,跳过剩下所有的子节点了:

587fa264db41d77510be3443b9bbe85c.png

上面的操作叫做α-β剪枝。

你可以仔细想一想。总之,收益值的可行区间一旦变成矛盾的,说明当前节点必然不会是获得最优的决策,那么我们可以直接跳过这个节点,不管它还有多少个子节点没有被搜索。

由于及时的剪枝操作,我们大大地减少了需要搜索的节点数量,节省下来的算力就能进行更多更深层次的搜索。

这就是传说中的博弈树跟α-β剪枝的原理了。

下面给出实现的伪代码:

e5fb64462d1e9eb5f9431fdda23cfa7c.png

互动留言

可以跟我们分享一个博弈论在生活中的应用吗?

ba0fad147ab7ec7076b8a81914eab8ee.gif

本文作者折射,知乎专栏:机器学习与数据挖掘

https://zhuanlan.zhihu.com/c_1024970634371186688

6ea5f79e26ce391971dedfba2c1b475f.png

听说你正【在看】

82012bc47b111e604520fa80a8a1103d.png
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值