C++程设实验项目三:黑白棋与基于UCT算法的AI-程序员宅基地

在这篇博客里,我将总结一下在这次实验中学到的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次的拉动能获得最高的奖励呢?

 

  1. 首先,凭我们的直觉,当然是每个赌博机都拉一次,先看看他们的表现如何。
  2. 然后,如果A,B,C,D四个赌博机中,只有B赌博机给了你硬币,那么你要怎么选择呢?从当前的局面来看,当然是要继续拉B赌博机了——毕竟,从统计学上说,B赌博机的出奖概率是100%呢。
  3. 然而,又两次的拉动,都没有出奖。即使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 }
View Code

 

转载于:https://www.cnblogs.com/KakagouLT/p/9201845.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_34014277/article/details/94523207

智能推荐

什么是内部类?成员内部类、静态内部类、局部内部类和匿名内部类的区别及作用?_成员内部类和局部内部类的区别-程序员宅基地

文章浏览阅读3.4k次,点赞8次,收藏42次。一、什么是内部类?or 内部类的概念内部类是定义在另一个类中的类;下面类TestB是类TestA的内部类。即内部类对象引用了实例化该内部对象的外围类对象。public class TestA{ class TestB {}}二、 为什么需要内部类?or 内部类有什么作用?1、 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。2、内部类可以对同一个包中的其他类隐藏起来。3、 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。三、 内部类的分类成员内部_成员内部类和局部内部类的区别

分布式系统_分布式系统运维工具-程序员宅基地

文章浏览阅读118次。分布式系统要求拆分分布式思想的实质搭配要求分布式系统要求按照某些特定的规则将项目进行拆分。如果将一个项目的所有模板功能都写到一起,当某个模块出现问题时将直接导致整个服务器出现问题。拆分按照业务拆分为不同的服务器,有效的降低系统架构的耦合性在业务拆分的基础上可按照代码层级进行拆分(view、controller、service、pojo)分布式思想的实质分布式思想的实质是为了系统的..._分布式系统运维工具

用Exce分析l数据极简入门_exce l趋势分析数据量-程序员宅基地

文章浏览阅读174次。1.数据源准备2.数据处理step1:数据表处理应用函数:①VLOOKUP函数; ② CONCATENATE函数终表:step2:数据透视表统计分析(1) 透视表汇总不同渠道用户数, 金额(2)透视表汇总不同日期购买用户数,金额(3)透视表汇总不同用户购买订单数,金额step3:讲第二步结果可视化, 比如, 柱形图(1)不同渠道用户数, 金额(2)不同日期..._exce l趋势分析数据量

宁盾堡垒机双因素认证方案_horizon宁盾双因素配置-程序员宅基地

文章浏览阅读3.3k次。堡垒机可以为企业实现服务器、网络设备、数据库、安全设备等的集中管控和安全可靠运行,帮助IT运维人员提高工作效率。通俗来说,就是用来控制哪些人可以登录哪些资产(事先防范和事中控制),以及录像记录登录资产后做了什么事情(事后溯源)。由于堡垒机内部保存着企业所有的设备资产和权限关系,是企业内部信息安全的重要一环。但目前出现的以下问题产生了很大安全隐患:密码设置过于简单,容易被暴力破解;为方便记忆,设置统一的密码,一旦单点被破,极易引发全面危机。在单一的静态密码验证机制下,登录密码是堡垒机安全的唯一_horizon宁盾双因素配置

谷歌浏览器安装(Win、Linux、离线安装)_chrome linux debian离线安装依赖-程序员宅基地

文章浏览阅读7.7k次,点赞4次,收藏16次。Chrome作为一款挺不错的浏览器,其有着诸多的优良特性,并且支持跨平台。其支持(Windows、Linux、Mac OS X、BSD、Android),在绝大多数情况下,其的安装都很简单,但有时会由于网络原因,无法安装,所以在这里总结下Chrome的安装。Windows下的安装:在线安装:离线安装:Linux下的安装:在线安装:离线安装:..._chrome linux debian离线安装依赖

烤仔TVの尚书房 | 逃离北上广?不如押宝越南“北上广”-程序员宅基地

文章浏览阅读153次。中国发达城市榜单每天都在刷新,但无非是北上广轮流坐庄。北京拥有最顶尖的文化资源,上海是“摩登”的国际化大都市,广州是活力四射的千年商都。GDP和发展潜力是衡量城市的数字指...

随便推点

java spark的使用和配置_使用java调用spark注册进去的程序-程序员宅基地

文章浏览阅读3.3k次。前言spark在java使用比较少,多是scala的用法,我这里介绍一下我在项目中使用的代码配置详细算法的使用请点击我主页列表查看版本jar版本说明spark3.0.1scala2.12这个版本注意和spark版本对应,只是为了引jar包springboot版本2.3.2.RELEASEmaven<!-- spark --> <dependency> <gro_使用java调用spark注册进去的程序

汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用_uds协议栈 源代码-程序员宅基地

文章浏览阅读4.8k次。汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用,代码精简高效,大厂出品有量产保证。:139800617636213023darcy169_uds协议栈 源代码

AUTOSAR基础篇之OS(下)_autosar 定义了 5 种多核支持类型-程序员宅基地

文章浏览阅读4.6k次,点赞20次,收藏148次。AUTOSAR基础篇之OS(下)前言首先,请问大家几个小小的问题,你清楚:你知道多核OS在什么场景下使用吗?多核系统OS又是如何协同启动或者关闭的呢?AUTOSAR OS存在哪些功能安全等方面的要求呢?多核OS之间的启动关闭与单核相比又存在哪些异同呢?。。。。。。今天,我们来一起探索并回答这些问题。为了便于大家理解,以下是本文的主题大纲:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCXrdI0k-1636287756923)(https://gite_autosar 定义了 5 种多核支持类型

VS报错无法打开自己写的头文件_vs2013打不开自己定义的头文件-程序员宅基地

文章浏览阅读2.2k次,点赞6次,收藏14次。原因:自己写的头文件没有被加入到方案的包含目录中去,无法被检索到,也就无法打开。将自己写的头文件都放入header files。然后在VS界面上,右键方案名,点击属性。将自己头文件夹的目录添加进去。_vs2013打不开自己定义的头文件

【Redis】Redis基础命令集详解_redis命令-程序员宅基地

文章浏览阅读3.3w次,点赞80次,收藏342次。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。当数据量很大时,count 的数量的指定可能会不起作用,Redis 会自动调整每次的遍历数目。_redis命令

URP渲染管线简介-程序员宅基地

文章浏览阅读449次,点赞3次,收藏3次。URP的设计目标是在保持高性能的同时,提供更多的渲染功能和自定义选项。与普通项目相比,会多出Presets文件夹,里面包含着一些设置,包括本色,声音,法线,贴图等设置。全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,主光源和附加光源在一次Pass中可以一起着色。URP:全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,一次Pass可以计算多个光源。可编程渲染管线:渲染策略是可以供程序员定制的,可以定制的有:光照计算和光源,深度测试,摄像机光照烘焙,后期处理策略等等。_urp渲染管线