在这篇博客里,我将总结一下在这次实验中学到的UCT算法实现原理。
首先是参考文章:
https://blog.csdn.net/u014397729/article/details/27366363 这是一篇用UCT算法实现四子棋AI的博客。这里给出了UCT的完整伪代码,而且有现成的可运行代码以供参考
https://blog.csdn.net/yw2978777543/article/details/70799799 这篇文章则用数学语言和伪代码进一步阐述了UCT算法的工作原理
https://jeffbradberry.com/posts/2015/09/intro-to-monte-carlo-tree-search/ 这篇英文文章则有一个清晰的图示,可以直观地认识UCT算法。
然后,让我唠叨一下本次黑白棋的具体实现和规则。
首先,每回合只有五秒的可用时间。这是为了防止有同学拿剪枝算法算太久。
同时,为了防止同学们写剪枝算法的时候直接抄网上的估值表,定义了如下规则:本方下棋时不可以下在上一回合落子的邻接点。
如图,黑色星是刚刚下的位置,X是不能下的位置,白方可下位置用♂标出(不要在意细节)。
那么具体的数据结构是这样设计的:
class Board { //记录了棋盘,以及上一子的颜色和位置 int chessboard[8][8], latestColor; chesspos latestStep; public: //棋盘的操控 Board(); //初始化,在四角放上棋子 bool isEnd(); //用对黑白子都算可下位置的方法计算是否终局 bool notConj(chesspos a, chesspos b); //用于判断某点是否可下 bool search(chesspos p, int color, int d); //判断某个方向上是否满足翻子条件 void rev(chesspos p, int color, int d); //在search满足后翻棋子 void oneMove(chesspos p); //确保合法的状态下给定位置,自动完成落子过程 vector<chesspos> getValidPos(int, chesspos); //提供颜色和上一子的位置,返回可下位置 //将会用于UCT算法的操作 int calScore(); //计算分数 chesspos randomMove(); //随机落子 int simulate(); //用随机落子的方法完成棋盘 //作业具体要求下的操作,可以无视之 void graphBoard(); void graphBoard(string path); void printScore(string path); };
好了,让我们开始讨论UCT算法吧。
要写一个基于UCT算法的AI,首先就要弄懂UCT算法究竟在干什么。这是很重要,只有弄懂了怎么回事,才可以基于伪代码的框架实现,否则很可能实现的东西四不像,甚至为别人下棋。
黑猩猩算法——仅仅比随机落子好一点
在接触UCT之初,我曾一知半解地构想出这样一个解决方案:
- 我们在计算正方形内切圆面积时,可以随机播撒豆子。然后,数一数圆内的豆子数量,与所有豆子的数量比较,就可以知道圆的面积了。
- 基于同样的道理,我可以随机在所有的可下位置选一点,然后通过随机落子完成棋盘。只要模拟次数够多,那么不同位置的可能胜率就会有差异。选择胜率最好的那个位置,然后就可以一路走向成功喽!
尽管可能是我的估值设计有误(在没有理解具体含义的情况下使用UCT算法的估值公式),导致算法和随机落子没太大区别,但总而言之,黑猩猩不是一个好的算法。模拟是需要时间的,五秒钟随机的结果很难让优势显现出来,就好像一个一米的正方形,只播撒一百颗豆子去算内切圆的面积,当然算不准。那么如何去改进呢?
多臂赌博机问题,以及由此引出的UCB算法
现在不考虑黑白棋了,考虑我们去玩赌博机。现在面前有几台(不妨设为4台)相同的赌博机,它们的玩法是一样的:拉下拉杆,然后有几率获得一枚硬币作为奖励。不同的赌博机有不同的出奖概率,而你拉动拉杆的次数是有限的——比如233次。如何设计策略,让你这233次的拉动能获得最高的奖励呢?
- 首先,凭我们的直觉,当然是每个赌博机都拉一次,先看看他们的表现如何。
- 然后,如果A,B,C,D四个赌博机中,只有B赌博机给了你硬币,那么你要怎么选择呢?从当前的局面来看,当然是要继续拉B赌博机了——毕竟,从统计学上说,B赌博机的出奖概率是100%呢。
- 然而,又两次的拉动,都没有出奖。即使B赌博机的出奖概率还是比A,C,D的高——33%对0%,但你有理由怀疑,A,C,D中有更好的选择,只是样本太少暂时没有显现出来。于是你就先放下了B,转而尝试其它赌博机。
这听起来的确很有道理,而也的确就是UCB算法处理博弈树的思想:
- 通过多次模拟的结果,寻找到概率最高的那一个节点。将自己的主要精力用在这一个节点上,避免不必要的浪费。这个过程叫利用(Exploitation)。
- 但是,也要照顾到那些被“冷落”的节点,避免失去机会。这个过程叫探索(Exploration)。
那么,我们如何确定选择哪一个节点呢?这就要使用UCB公式。针对像多臂赌博机这样的问题,可以设计这样的一个公式:
其中:
- Cw为节点分数。
- Cv为该节点总访问数。
- Pv为所有节点总访问数。
- C为比例系数。这个系数越大越注重探索,越小越注重利用。
这里的关键就在于如何调整比例系数了。一般需要用实验来确定,参考的文章中提供的一个可选的参数是1.38,也就是求解C*sqrt(2)==1.96得到的数值。至于为什么是1.96,我也说不清楚……
总之,在这个策略下,你可以记下拉动拉杆的总次数,以此作为Pv。针对单个赌博机,记得到的硬币为Cw,历史拉动次数为Cv,那么拉动前给每个赌博机计算UCB值,选择最大的那个去拉动就好了。当存在未拉动的赌博机,我们可以视为其UCB值无限大,这样我们总是优先地去尝试这些赌博机,毕竟它们有无限可能嘛。
将UCB算法与蒙特卡罗树结合的UCT算法——直观的解释
出于赌博机的封装机制,我们并不能看出拉动拉杆之后,赌博机内部如何进行运算。但是,黑白棋游戏中,我们是可以看到随机落子的模拟过程的。于是我们就可以对UCB算法做一些改进,使得我们的模拟过程能记录下来。这里就要用到树,树的节点代表一个棋盘的状态,同时也记录了这个状态下落子的一方以及落子方的胜率。可以推知,游戏开局的根节点是白色的,因为下一手是黑方行动,根节点是黑色的。
问题来了,UCT对模拟的记录是怎么样的呢?如果对算法理解不透彻就按伪代码搭代码,就可能对伪代码产生误解。我和舍友都曾经对其产生过误解,当时我们是这样认为的:UCT算法提到了模拟和备份的概念,那么是不是就意味着,模拟过程中,每一步落子前,我们都考察UCB值最大的点,若这个点不在树中,便生成一个胜率为0/0的节点。当我们经过约60多步的落子,完成棋盘,博弈树上就会有一条长长的枝。最后,根据模拟结果,给枝上的节点记分,得到一串胜率为0/1或是1/1的节点,以此算作所谓的备份过程。
上图是一个错误的理解,你可以看到,开局的首次模拟过程就开辟了一串节点,虚线指向的是还没有开辟的可行节点
但实际上并非如此。对于这个令人困扰的问题,我在观看了文首英文文献中的图片之后茅塞顿开。UCT的过程,实际上是这样的:
- 首先,我们从以当前棋盘状态对应的节点,作为博弈树的根节点。
- 每次UCT搜索,看的是当前所到的节点,是不是尚未完全扩展的节点。这就好比在看,是否存在没有拉动拉杆的赌博机。
- 如果这个节点是完全扩展的,那么我们就计算UCB值,选择最大的那个往下走。
- 最终可能出现两种可能:我们遇到了没有完全扩展的节点,或者遇到了终局节点。
- 终局节点当然好说,就是直接沿着我们刚才来的路径,一个一个节点备份棋局结果。
- 不然的话,我们就相当于发现了没有拉动的赌博机。这时候就选一个拉下去,即以一个可行状态出发,进行随机模拟。这个模拟过程就是随机在可行位置下不断下子,直到棋盘结束。这个随机过程中我们并不记录任何东西。模拟的结果,从刚才生成的0/0节点开始,依次向上备份结果。
抽象地说(来自第一篇参考文章的注解),我们就是在找当前UCT树的主路径,然后取得主路径新生成的尾节点,从这个尾节点出发进行模拟,备份得分的对象是新的主路径。百度百科上有一张图,很直观地展现了什么是主路径。
刚才说的是单次的UCT搜索。一次完整的UCT算法求解,是要在限定的时间内进行多次UCT搜索的。每次UCT搜索,都会改变博弈树的结构,影响下一次UCT搜索的主路径走向。而搜索得越多,结果也就越准确。
如果主路径直达终局节点,那么当然就是直接备份结果。
这张图是最常见的情况。在主路径中发现了非全扩展节点,就为从可行节点中新开辟一个0/0节点(你可以看到,虚线还连着一个节点,如果下一次有主路径通往这里,就会开辟它)。
模拟的结果,假定是黑方胜利,那么沿着主路径从这个0/0节点回溯,一直到当前棋盘的根节点,都进行胜率的更新。
由此,就解释清楚了UCT算法的过程。那么,具体到代码,应该怎么写呢?
UCT算法在代码上的具体实现
先是数据结构:
class Node { public: chesspos pos; //此状态的落子位置,如果上一回合没有落子,就是(-1,-1) int total, score; //节点的胜率信息 int color; //落子的颜色 Node* parent; vector<Node*> child; vector<chesspos> validPos; //生成每个节点的时,都保存了可下位置,这样方便判断是否完全扩展,也可以快速找到可扩展节点 Node(chesspos p, int c, Node* par, vector<chesspos> v); }; class Tree { Node *subroot, *tail; //一开始的时候想复用搜索树,所以还写了个root保存开局节点,但这实际上是不需要的,因为这个算法不复用搜索结果 int ownColor; //本方的颜色,用于记录胜率 public: //下面这些在后面细讲 Tree(int ownc); Node* expend(Board board);//expend tail void nextnode(chesspos nextp, Board board); //includes nonexist node constuction Node* bestChild(Node * tarRoot, double cof); Board getTail(Board board);//tree policy void backup(int result); //下面的这两个都不用管,是作业特殊要求的函数。 void printInfo(); void newTurn(); };
除了这些UCT树用到的算法,还会用到Board类的simulate()。
关于初始化这样的基本操作我们就跳过不提了,先来看看UCT算法的主要部分是怎么工作的:
//到自己的回合了...
//树是Tree UCTtree
//当前棋盘是Board b s = clock(); n = clock(); while ((int)(n - s)<4750) { UCTtree.backup(UCTtree.getTail(b).simulate()); n = clock(); } //根据搜索结果落子...
看起来有些简单,因为最重要的过程被抽象到了一条语句里。我们一步步地来看。
首先是getTail,由于我们不在节点中保存棋盘,所以这个函数接收一个棋盘,这个棋盘的状态等同于当前根节点代表的状态。
Board Tree::getTail(Board board) { tail = subroot; while (!board.isEnd()) { int vs = tail->validPos.size(), cs = tail->child.size(); if (vs != cs) { tail = expend(board); board.oneMove(tail->pos); break; } else { tail = bestChild(tail); board.oneMove(tail->pos); } } return board; }
你可以看到,如果主路径直达终局,那么就退出while,返回一个终局的棋盘。如果不是,也就是vs > cs的时候,就基于当前棋盘,扩展一个节点,然后根据这节点落子,最后返回棋盘。
getTail里有两个函数没有说,一个是expend,一个是bestChild。
Node* Tree::expend(Board board) { Node* newNode; vector<chesspos> possiblePos; bool matched; //以下的循环就是找出validPos中不在child的那些位置 for (auto v : tail->validPos) { matched = false; for (auto c : tail->child) { if (v == c->pos) { matched = true; break; } } if (!matched) possiblePos.push_back(v); } int index = rand() % possiblePos.size(); board.oneMove(possiblePos[index]); newNode = new Node(possiblePos[index], !(bool)tail->color, tail, board.getValidPos()); //你可以看到,节点在生成的时候就保留了可下位置。 tail->child.push_back(newNode); //把新节点放入tail的子节点行列中。事实上,getTail里的tail = expend(board)是可以合并在expend里的,这就是具体实现细节的问题了。 return newNode; }
这个代码应该很直观了,就是为搜索树扩展一个新的节点,然后棋盘相应地更新一下。
然后是bestChild。你可以看到比例系数cof是150,这个稍后会解释。
Node* Tree::bestChild(Node *tarRoot = NULL, double cof = 150) { double argmax = -99999999, ucb; Node* best = NULL; if (tarRoot == NULL) tarRoot = subroot; for (auto c : tarRoot->child) { ucb = 1.0 * c->score / c->total + cof * sqrt(log(tarRoot->total) / c->total); if (ucb > argmax) { argmax = ucb; best = c; } } return best; }
要注意的是,无论是自己还是对方,UCT公式是一样的。如果在算对方的UCB值时加负号什么的,实测中发生的事,就是显示自己的胜率为99.98%,但是瞬间归零。因为在某些接近终盘的局面下,对方的选择可能就将决定胜负归谁。那么这个负号就是假设对手下在最坏位置,并且算出自己胜率。只要人家不傻,下在有利于他的位子,那么自己就绝对会输,胜率也就归零了。
以上就是getTail部分了,小结一下,getTail结束时候,我们就获得了一个棋盘,这个棋盘是这么得到的:从游戏当前的棋盘开始,根据UCT树的主路径落子,要么下到游戏结束,要么下到出现了非全扩展节点。如果是后者,就随机选一个之前没试过的位置落子,然后相应地在树上记录这个新的节点。
getTail之后就是simulate了,这是Board类的功能,简单看看就行:
int Board::simulate() { vector<pair<int, int>> aps; int score, tmpcolor = latestColor, tmpBoard[8][8]; chesspos tmpStep = latestStep; memcpy(tmpBoard, chessboard, sizeof(chessboard)); //以上是备份当前棋盘。其实这个备份环节是出于调试的需要,实际上不会直接对本地棋盘这么调用,所以不备份或许也可以。 while (!isEnd()) { randomMove(); } score = calScore(); //保存分数 memcpy(chessboard, tmpBoard, sizeof(chessboard)); //以下是恢复棋盘。 latestColor = tmpcolor; latestStep = tmpStep; return score; //返回模拟结果 } chesspos Board::randomMove() { vector<chesspos> aps; int index; aps = getValidPos(); index = rand() % aps.size(); if (aps[index].first != -1) oneMove(aps[index]); //在oneMove里已经转换了颜色 else latestColor = !(bool)latestColor; //说明一下,getValidPos在无子可下的时候会返回一个(-1,-1)的位置。 latestStep = aps[index]; return latestStep; //其实不一定要return,这里是调试需要 }
simulate之后就是backup。
void Tree::backup(int result) { //simulate的结果通过正负号来记录黑白子的胜利信息。 int mod = result > 0 ? BLACK_WINS : WHITE_WINS; result = abs(result); while (tail != subroot) { tail->total += 64; if (!(tail->color ^ mod)) tail->score += result; tail = tail->parent; } //由于之前的规划问题,这里还要再对subroot进行处理。如果每次转移搜索树的根节点的时候,都清除subroot的parent,那么就可以用while(tail)一步到位。 tail->total += 64; if (!(tail->color ^ result)) tail->score += result; //这一行貌似可以不要,因为根节点的胜率不在计算的考虑范围内。 }
这里有必要说明一下记分规则。在我自己的实验过程中,设计了两种计算分数的规则,一种是计算胜负,一种是计算终局棋盘本方剩余子数。
- 如果是计算胜负,那么主路径的所有节点Cv+1,胜方颜色节点Cw+1,但负方不扣分。
- 如果是计算胜子,那么Cv+64,胜方Cw加棋盘上的本方子数,同样的,负方不扣分。注意,不能Cv+32,然后Cw考虑负方扣分,这会导致奇奇怪怪的情况。
两个记分规则会导致什么不同呢?
- 计算胜负,可以直观地看到胜率信息,但是最终只是能赢,不能考虑赢多。此时的比例系数c照常为1.38
- 计算胜子,就根据胜子数量细分了胜率,可以追求更多的胜子。然而,Cv+64导致增长过快,1.38的比例系数会导致极为不平衡的利用,所以必须把c调大。我尝试过从88.32到180的比例系数,但是由于时间上的限制,没办法清晰地展现出这些系数的不同。最终我采用了150,当然小一点也是没问题的。
至此,UCT算法的主要过程就结束了。之后就是一些操作上的设计了。
//...搜索结束 //要获得最佳节点,就把比例系数设为0,即完全利用,只看胜率了。 Node* best = UCTtree.bestChild(NULL, 0); UCTtree.nextnode(best->pos, b); //进行下一回合,轮到对手落子...
对了,nextnode是什么呢?
void Tree::nextnode(chesspos nextp, Board board) { for (auto c : subroot->child) { if (c->pos == nextp) { subroot = c; subroot->score = 0; subroot->total = 0; subroot->child.clear(); return; } } }
虽然nextnode很简单,就是向下转移根节点,但是注意到:
- 会不会出现我的目标节点并未被扩展出来?实际上不需要担心这个,一个局面的可下位置至多不超过30多,而5秒已经可以达到800多次的UCT搜索,所以并没有要为还没扩展的节点考虑在树上新生成节点。此外,bestChild也保证了只会在已扩展节点中选择位置。
- 注意subroot->child.clear(),也就是每次转移根节点,都不必要保存之前的搜索结果,因为这可能会妨碍最优子节点的判断。而且,实际上搜索结果的复用效率很低,即使保存了也不会有很大的能力提升。
UCT的实战效果怎么样?
你可能注意到了,我没有为动态申请的节点写清除的函数。这意味着会占用很多内存——实测一局大概15M左右。你要是想自己写清除的功能也没问题。
接下来的图片,都是与猴子(完全随机落子)对战的日志。
以上是计算胜负的情况下的结果。在第19步,就已经有1360步的搜索。在角位落子,显示出了高胜率,所以角位的搜索次数也相对较多。程序最终选择下在角位,而且胜率暂时为60%。
在计算胜负的策略中,算法在第47步确定自己将会胜利,胜率显示为了100%。
当采用计算剩子的策略时,计算的就是棋盘剩子的期望值了。47的剩子以为着极大的获胜可能,而且看AI的行为,已经逼得对方无棋可走,最终达成了完胜的局面。
甚至还出现了这样的局面:对阵的是同学的剪枝算法,UCT算法是白方。虽然UCT算法很难招架,但是由于对方的一些失误,UCT甚至在失去三个角位的情况下也达成了胜利!
关于UCT的总结
- 虽然UCT靠的是随机模拟,但是靠着模拟次数足够和UCB策略,也能有着很不错的表现。
- UCT算法是独立于游戏本身的算法,只要有接口,大部分相似的游戏都可以使用UCT,比如五子棋,象棋等。
- α-β剪枝是常用的算法,但是它需要针对游戏进行精细的估值。相比之下,虽然UCT算法可能打不过精细调参的剪枝算法,但是它只需要调一个比例系数,非常省事高效。
- 搜索次数也是限制UCT算法能力的一个因素。开局情况下只能搜索800次,只有到后期才可能上千上万。如果开局不好,UCT算法可能会无法给自己布好局,从而早早地给出低胜率。当然了,对付猴子还是绰绰有余的。
可以跑起来的代码
一个可以跑起来的代码会给我很大帮助,这在我研究UCT的时候就是这么想的。但是下面的代码是我自己本地调试,写的比较乱,很多没有使用的冗余功能,主函数也没有整理,而且整体代码不是最新版本,有兴趣的话看看就好,毕竟上面已经整理好相关代码了。

1 #include <cstdio> 2 #include <iostream> 3 #include <vector> 4 #include <ctime> 5 #include <string> 6 #include <fstream> 7 #include <omp.h> 8 //#include <cmath> 9 #define BLACK_WINS 0 10 #define WHITE_WINS 1 11 //#define TESTING 12 using namespace std; 13 typedef pair<int, int> chesspos; 14 15 class Node { 16 public: 17 chesspos pos; 18 int total, score; // long long? 19 int color; 20 Node* parent; 21 vector<Node*> child; 22 vector<pair<int, int>> validPos; 23 Node(chesspos p, int c, Node* par, vector<chesspos> v); 24 }; 25 class Board { 26 int chessboard[8][8], latestColor; 27 chesspos latestStep; 28 public: 29 Board(); 30 int calScore(); 31 bool isEnd(); 32 bool notConj(chesspos a, chesspos b); 33 bool search(chesspos p, int color, int d); 34 void rev(chesspos p, int color, int d); 35 void oneMove(chesspos p); 36 vector<chesspos> getValidPos(int, chesspos); 37 chesspos randomMove(); 38 int simulate(); 39 void graphBoard(); 40 void printScore(); 41 }; 42 43 class Tree { 44 Node *root, *subroot, *tail; 45 int ownColor; 46 public: 47 Tree(int ownc, Board board); 48 Node* expend(Board board);//expend tail 49 void nextnode(chesspos nextp, Board board); //includes nonexist node constuction 50 Node* bestChild(Node * tarRoot, double cof); 51 Board getTail(Board board);//tree policy 52 void backup(int result); 53 void printInfo(); 54 void newTurn(); 55 }; 56 int dr[8] = { 0,0,1,1,1,-1,-1,-1 }; 57 int dc[8] = { 1,-1,1,0,-1,1,0,-1 }; 58 59 int main() { 60 srand(time(NULL)); 61 int x, y; 62 time_t s, n; 63 Board b=Board(); 64 Tree UCTtree(0, b); 65 Node* best; 66 int res, searchCount ,total = 0; 67 chesspos r; 68 while (!b.isEnd()) { 69 s = clock(); 70 n = clock(); 71 searchCount = 0; 72 while ((int)(n - s)<4750) { 73 //Board t = UCTtree.getTail(b); 74 //res = t.simulate(); 75 UCTtree.backup(UCTtree.getTail(b).simulate()); 76 //printf("%d\n", i); 77 n = clock(); 78 searchCount++; 79 } 80 n = clock(); 81 printf("time use:%d\nSearch times:%d\n", (int)(n - s), searchCount); 82 UCTtree.printInfo(); 83 best = UCTtree.bestChild(NULL, 0); 84 printf("win rate:%lf\n", 1.0 * best->score / best->total * 64); 85 b.oneMove(best->pos); 86 total++; 87 printf("total:%d\n", total); 88 b.graphBoard(); 89 if (!b.isEnd()) { 90 UCTtree.nextnode(best->pos, b); 91 UCTtree.printInfo(); 92 best = UCTtree.bestChild(NULL, 0); 93 printf("win rate:%lf\n", 1.0 * best->score / best->total * 64); 94 //cin >> x >> y; 95 //b.oneMove(r); //if you want to see how monkey moves, delete this two lines and use the next line 96 r = b.randomMove(); 97 total++; 98 printf("The monkey choose to move in (%d,%d)\n", r.first, r.second); 99 printf("total:%d\n", total); 100 UCTtree.nextnode(r, b); 101 b.graphBoard(); 102 } 103 system("pause"); 104 } 105 b.printScore(); 106 system("pause"); 107 //take white as owncolor, monkey mode only 108 total = 0; 109 b = Board(); 110 UCTtree.newTurn(); 111 while (!b.isEnd()) { 112 UCTtree.printInfo(); 113 best = UCTtree.bestChild(NULL, 0); 114 printf("win rate:%lf\n", 1.0 * best->score / best->total * 64); 115 r = b.randomMove(); 116 total++; 117 printf("The monkey choose to move in (%d,%d)\n", r.first, r.second); 118 printf("total:%d\n", total); 119 UCTtree.nextnode(r, b); 120 b.graphBoard(); 121 if (!b.isEnd()) { 122 s = clock(); 123 n = clock(); 124 searchCount = 0; 125 while ((int)(n - s)<4750) { 126 res = UCTtree.getTail(b).simulate(); 127 UCTtree.backup(res); 128 //printf("%d\n", i); 129 n = clock(); 130 searchCount++; 131 } 132 n = clock(); 133 printf("time use:%d\nSearch times:%d\n", (int)(n - s), searchCount); 134 UCTtree.printInfo(); 135 best = UCTtree.bestChild(NULL, 0); 136 printf("win rate:%lf\n", 1.0 * best->score / best->total * 64); 137 b.oneMove(best->pos); 138 UCTtree.nextnode(best->pos, b); 139 total++; 140 printf("total:%d\n", total); 141 b.graphBoard(); 142 system("pause"); 143 } 144 } 145 b.printScore(); 146 system("pause"); 147 return 0; 148 } 149 150 Board::Board() 151 { 152 for (int i = 0; i < 8; i++) 153 for (int j = 0; j < 8; j++) 154 chessboard[i][j] = -1; 155 chessboard[3][3] = 0; 156 chessboard[4][4] = 0; 157 chessboard[3][4] = 1; 158 chessboard[4][3] = 1; 159 latestColor = 1;//gameStartRoot is white, next and first move is black 160 latestStep = make_pair(-1, -1); 161 } 162 163 int Board::calScore() { 164 int c[2] = { 0 }; 165 for (int i = 0; i < 8; i++) 166 for (int j = 0; j < 8; j++) 167 if (chessboard[i][j] >= 0) c[chessboard[i][j]]++; 168 #ifdef TESTING 169 return c[0] - c[1]; 170 #else 171 if (c[0] == c[1]) return 0; 172 else if (c[0] > c[1]) return c[0]; 173 else if (c[0] < c[1]) return -1 * c[1]; 174 //return c[0] > c[1] ? BLACK_WINS : WHITE_WINS; 175 #endif // TESTING 176 177 } 178 179 bool Board::isEnd() { 180 if (getValidPos(1, make_pair(-1, -1))[0].first == -1 && getValidPos(0, make_pair(-1, -1))[0].first == -1) return true; 181 return false; 182 } 183 184 bool Board::notConj(chesspos a, chesspos b) { 185 if (abs(a.first - b.first) + abs(a.second - b.second) == 1) return false; 186 else return true; 187 } 188 189 bool Board::search(chesspos p, int color, int d) 190 { 191 int r = p.first + dr[d], c = p.second + dc[d]; 192 if (chessboard[r][c] == color) return false; //diff color should be in the middle 193 while (0 <= r && r <= 7 && 0 <= c && c <= 7) { 194 if (chessboard[r][c] == -1) return false; 195 else if (chessboard[r][c] == color) return true; 196 else { 197 r += dr[d]; 198 c += dc[d]; 199 } 200 } 201 return false; 202 } 203 204 void Board::rev(chesspos p, int color, int d) 205 { 206 int r = p.first + dr[d], c = p.second + dc[d], oppcolor = !(bool)color; 207 while (0 <= r && r <= 7 && 0 <= c && c <= 7 && chessboard[r][c] == oppcolor) { 208 chessboard[r][c] = color; 209 r += dr[d]; 210 c += dc[d]; 211 } 212 } 213 214 void Board::oneMove(chesspos p) 215 { 216 latestColor = !(bool)latestColor; 217 if (p.first != -1) { 218 chessboard[p.first][p.second] = latestColor; 219 for (int d = 0; d < 8; d++) { //flip in 8 direction 220 if (search(p, latestColor, d)) { 221 rev(p, latestColor, d); 222 } 223 } 224 } 225 latestStep = p; 226 } 227 228 vector<chesspos> Board::getValidPos(int targetColor = -1, chesspos lstep = make_pair(233, 233)) 229 { 230 vector<chesspos> result; 231 chesspos pos; 232 if (targetColor == -1) targetColor = !(bool)latestColor; //next step is for the opp 233 if (lstep == make_pair(233, 233)) lstep = latestStep; 234 #pragma omp parallel for 235 for (int k = 0; k < 64; k++) { 236 int i = k / 8; 237 int j = k % 8; 238 if (chessboard[i][j] == -1) { 239 pos = make_pair(i, j); 240 for (int d = 0; d < 8; d++) { 241 if (notConj(pos, lstep) && search(pos, targetColor, d)) { 242 result.push_back(pos); 243 break; 244 } 245 } 246 } 247 } 248 if (result.size() == 0) result.push_back(make_pair(-1, -1)); 249 return result; 250 } 251 252 chesspos Board::randomMove() 253 { 254 vector<pair<int, int>> aps; 255 int index; 256 aps = getValidPos(); 257 index = rand() % aps.size(); 258 if (aps[index].first != -1) oneMove(aps[index]); //in this func color has been flipped 259 else latestColor = !(bool)latestColor; 260 latestStep = aps[index]; 261 return latestStep; 262 } 263 264 int Board::simulate() { 265 vector<pair<int, int>> aps; 266 int index, tmpcolor = latestColor, tmpBoard[8][8]; 267 chesspos tmpStep = latestStep; 268 memcpy(tmpBoard, chessboard, sizeof(chessboard)); //backup 269 while (!isEnd()) { 270 randomMove(); 271 #ifdef TESTING 272 graphBoard(); // 273 #endif // TESTING 274 } 275 index = calScore(); //for temp use 276 memcpy(chessboard, tmpBoard, sizeof(chessboard)); // reset to initial state 277 latestColor = tmpcolor; 278 latestStep = tmpStep; 279 return index; 280 } 281 282 void Board::graphBoard() { 283 int markboard[8][8]; 284 vector<chesspos> aps = getValidPos(); 285 memcpy(markboard, chessboard, sizeof(chessboard)); 286 if (latestStep.first != -1) markboard[latestStep.first][latestStep.second] = 2; 287 for (auto p : aps) markboard[p.first][p.second] = 3; 288 printf(" 0 1 2 3 4 5 6 7\n"); 289 for (int i = 0; i < 8; i++) { 290 printf(" %d", i); 291 for (int j = 0; j < 8; j++) { 292 switch (markboard[i][j]) { 293 case 0:printf("●"); break; 294 case 1:printf("○"); break; 295 case 2: { 296 if (latestColor == 0) printf("★"); 297 else printf("☆"); 298 break; 299 } 300 case 3:printf("♂"); break; 301 case -1: { 302 if (notConj(latestStep, make_pair(i, j))) printf(" "); 303 else printf("×"); 304 break; 305 } 306 } 307 } 308 printf("||\n"); 309 } 310 printf("=======================\n"); 311 } 312 313 void Board::printScore() { 314 int c[2] = { 0 }; 315 for (int i = 0; i < 8; i++) 316 for (int j = 0; j < 8; j++) 317 if (chessboard[i][j] != -1) c[chessboard[i][j]]++; 318 printf("BLACK %d : %d WHITE\n", c[0], c[1]); 319 } 320 Node::Node(chesspos p, int c, Node * par, vector<chesspos> v) 321 { 322 pos = p; 323 color = c; 324 parent = par; 325 total = 0; 326 score = 0; 327 validPos = v; 328 } 329 330 Tree::Tree(int ownc, Board board) 331 { 332 root = new Node(make_pair(-1, -1), 1, NULL, board.getValidPos()); 333 subroot = root; 334 tail = root; 335 ownColor = ownc; 336 } 337 338 Node* Tree::expend(Board board) { 339 Node* newNode; 340 vector<chesspos> possiblePos; 341 bool matched; 342 for (auto v : tail->validPos) { 343 matched = false; 344 for (auto c : tail->child) { 345 if (v == c->pos) { 346 matched = true; 347 break; 348 } 349 } 350 if (!matched) possiblePos.push_back(v); 351 } 352 int index = rand() % possiblePos.size(); 353 board.oneMove(possiblePos[index]); 354 newNode = new Node(possiblePos[index], !(bool)tail->color, tail, board.getValidPos()); 355 tail->child.push_back(newNode); 356 return newNode; 357 } 358 359 void Tree::nextnode(chesspos nextp, Board board) { 360 for (auto c : subroot->child) { 361 if (c->pos == nextp) { 362 subroot = c; 363 return; 364 } 365 } 366 //no child matched 367 board.oneMove(nextp); 368 Node *newNode = new Node(nextp, !(bool)subroot->color, subroot, board.getValidPos()); 369 subroot = newNode; 370 } 371 372 Node* Tree::bestChild(Node *tarRoot = NULL, double cof = 150) { 373 double argmax = -99999999, ucb; 374 Node* best = NULL; 375 if (tarRoot == NULL) tarRoot = subroot; 376 for (auto c : tarRoot->child) { 377 ucb = 1.0 * c->score / c->total + cof * sqrt(log(tarRoot->total) / c->total); 378 if (ucb > argmax) { 379 argmax = ucb; 380 best = c; 381 } 382 } 383 return best; 384 } 385 Board Tree::getTail(Board board) { 386 tail = subroot; 387 while (!board.isEnd()) { 388 int vs = tail->validPos.size(), cs = tail->child.size(); 389 if (vs != cs) { 390 tail = expend(board); 391 board.oneMove(tail->pos); 392 break; 393 } 394 else { 395 tail = bestChild(tail); 396 board.oneMove(tail->pos); 397 } 398 } 399 return board; 400 } 401 402 void Tree::backup(int result) { 403 //if a subroot is avoidable, then use while(tail) for root node's parent is NULL 404 int mod = result > 0 ? BLACK_WINS : WHITE_WINS; 405 result = abs(result); 406 while (tail != subroot) { 407 tail->total += 64; 408 if (!(tail->color ^ mod)) tail->score += result; 409 //tail->score += result * mod; 410 //mod *= -1; 411 tail = tail->parent; 412 } 413 tail->total += 64; 414 //tail->score += result * mod; 415 if (!(tail->color ^ result)) tail->score += result; 416 } 417 418 void Tree::printInfo() { 419 printf("subroot:(%d,%d)\n", subroot->pos.first, subroot->pos.second); 420 for (auto c : subroot->child) { 421 printf("-child:(%d,%d), score:%d, total:%d\n", c->pos.first, c->pos.second, c->score, c->total); 422 } 423 Node* n = bestChild(subroot, 0); 424 chesspos p = n == NULL ? make_pair(8, 8) : n->pos; 425 printf("bestchild:(%d,%d)\n",p.first,p.second); 426 } 427 428 void Tree::newTurn() { 429 subroot = root; 430 ownColor = !(bool)ownColor; 431 }