fstqwq
Bot 是一个主体采用了 Alpha Beta 优化的最大-最小搜索和上限置信区间蒙特卡洛树搜索(Upper Confidence Boundary Applied to Trees, UCT)的 NoGo Bot。Alpha-Beta 搜索的部分采用了可行步数差来评估局面好坏,应用了 Pattern Match 等评估方法为 Alpha Beta 优化顺序、实现剪枝、作为第二关键字为搜索结果排序,并实现了自适应搜索深度以适应前后期可达深度不同导致的时间利用不充分的问题;UCT 部分使用了经典 UCT 的实现,并且通过估价值限制一定的随机性来提高模拟的有效性,并且每一步初始时利用已有知识初始化了胜率。通过前后期算法的分治,总体胜率能达到第一梯队水平,并使得即使贪心策略被克制的情况下,仍有着不低的胜率。
在下面的描述中,我们令
作为一个完全知识博弈游戏,NoGo 游戏显然存在必胜与必败策略。然而,基于我们的算力远无法遍历
我们通过多局游戏后可以发现开局局势非常简单,此时几乎靠估价函数来进行布局——多产生「眼」,也即最简单的产生可行步数差方式,并且抑制对方产生「眼」,利用 Pattern Match 以及类似的其他方法可以很方便地求得这样的点。当然,不同的估价函数——无论怎么调参——得到的答案可能会存在「循环克制」的问题,Bot 在后面会有针对这种情况进行优化。
到游戏中期时,局面已经变得比较复杂,简单的估价已经无法很好地求得答案了。此时 Alpha-Beta 搜索派上了用场。通过对后几步的预测,Alpha-Beta 可以很轻松地避免因贪心导致的短视,亦可以稳健地在对手出现失误时获得优势。事实上,Bot 的开局也综合估价函数来应用了 Alpha Beta 搜索来避免被估价克制和压制棋力非常弱的对手。
可以发现,随着深度限制
不过,因为前期通常来说只能搜索出约
到游戏偏后期但 Alpha Beta 仍搜索不到游戏终结的时候,由于单纯的评价可行步数差效果很差,搜索里可行步数差相同的不同结果可能会导致不同程度的类似「奇数步赢偶数步输」的情况,单纯应用 Alpha Beta 搜索容易盲目地将自己的优势葬送掉。
UCT 的一次 Simulation 的复杂度为
到游戏终局时,胜负已可以轻易判定。为了方便评测,Bot 用 Alpha Beta 来以非常短的时间做出每一步的决策。
这里说一下在 OJ 上的
- 在 Alpha Beta 到深度限制时采用多元化估价以替代单一的步数差的估价。这样看起来很优秀,甚至可以避免出现前述缺点,但实际上这样严重依赖于估价函数的准确性以及和其他 bot 的克制性,效果很差。
- 在 Alpha Beta 估价时使用
$O(N ^ 2)$ 的试下策略(Dot Evaluation)来优化贪心函数的短视。这样看起来很棒棒,但是牺牲的复杂度实在是太高,最终极大影响到了搜索的深度,因此最终被舍弃。 - 限制 UCT 模拟步数,到达指定深度时用
$$r = \frac {1 + e ^ {-k}} 2 \times D ^ s$$ 来预测胜率,其中$k$ 为步数差,$s$ 为任一玩家剩余可行步数,$D$ 为衰减常数,$\sqrt[81]{0.1} < D \leq 1$。 这样做效果确实不错,OJ 上上传的IG.TheShy(223)
就是这么做的。然而,为了算法的纯粹性~~(强迫症),以及为了使 UCT 更稳定,Bot 没有采用这一做法。(然后因为这个原因 Bot 最终被吊打了)~~
目前来说,与其他 Bot 对打,TheShy 仍然是最强的。
为了尝试使用 OOP 实现程序,Bot 中所有用到的结构体全部进行了封装,模块化实现功能,提高了程序的可拓展性与鲁棒性。这在 Bot 的开发中得到了体现:迭代升级的过程非常流畅,未曾需要对主体程序进行重构。当然,因为存在部分冗余函数,并且需要参数传递,程序相对冗长、常数较大是不可忽视的缺点。
局面使用了 Board
来保存;UFset
是并查集;Point
是用来替代 std::pair<int, int>
的传递坐标的位置,实现了和 pair<int, int>
相互的类型转换;策略保存在 Alpha_Beta
与 UCT
中,两个类都有一个 public 函数 Action()
。
判断一个局面是否可行的函数 Board::valid()
使用的是随机合并并查集来实现的,复杂度为 f[x] = getf(f[f[x]])
利用寻址比递归快的特性跑得比西方记者快得多。因此,并查集运行的实际效果远优秀于 Flood Fill,甚至优秀于严格 Board::valid()
的复杂度称为
一个非常常用到的操作是 Board::getValidMoves()
,也寻找一个局面下的可行操作。常规的做法是使用 Board::valid()
判断。显然,这个复杂度是不优秀的。Bot 里实现了一个 Board::getValidMoves()
,使用对联通块的气计数的方法来优化判气复杂度。当然,由于我们两部分搜索都会用到对估价值进行排序,实质上调用一次 Board::getValidMoves()
复杂度是
对于评分系统,我们主要介绍 Pattern Match。Board::EyeDetect
实现了对 Pattern 的处理,Board::EyeDetect::eval()
通过枚举每一个位置可能成为一口气的方法,来对开局进行比较有效的估价。
我们通常都不会优先去走只有自己能走的点——那样通常会降低自己占气的优势。因此,Bot 针对这种情况进行了判断,对敌人能走的点提高优先级。
以人类智慧设置的参数显然并不一定准确。为了提高评分准确性,Bot 在本地通过模拟退火算法进行了
Alpha-Beta 实现部分通过评分系统加扰动来确定搜索顺序,使得搜索有随机化,并使 Alpha-Beta 剪枝更有效,并间接实现了使用贪心值作为二关键字排序,估价相同时默认保留第一个结果,自适应地完成了前中期的交替。
在对选择进行剪枝时,我们可以发现:尽管我们自己的选择从来都是估价函数得到的结果中的前几个,但敌人很可能选择出乎意料的走法。我们知道估价函数是不准确的,所以我们搜索时可以默认敌人比我们更 「聪明」。因此,Bot 的剪枝的限制对于敌人更加宽松,而对于自己的选择则适当收紧,这样在损失极少正确性的情况下 Bot 可以使搜索获得更深的深度。
UCT 实现很常规,探索常数按论文取到了
- 预处理了所有有关
sqrt
,log
,exp
的函数; - 使用估价函数加大扰动来实现一种「总体而言,更倾向于采用估价函数大的一步下的随机方法」的模拟,提高了模拟效率;
- 每一步初始时利用已有知识初始化了胜率;
- 在
IG.TheShy
等版本中采用了前述公式来减少模拟步数,预测胜率; - 随机数发生器采用了
xor-shift-128+
,复杂度低且随机性好。
当可行选择数在
有时,搜索程序会返回必败,此时搜索会无返回结果。秉承着永不言败的精神,Bot 将会返回一个最有希望的合法点,以期望对手下错来赢得该局游戏。