2020-12-29

一,评分表设置

基本思路就是给棋局打分,棋局的分数是建立在每一条线上特定模式的分数,我的打分表是在15*15的五子棋的基础上修改的:


     //一个字符串对应一个分数
    struct Pattern {
        string pattern;
        int score;
    };

    //模式
    vector<Pattern> patterns = {
        { "1111",  50000 },
        { "01110", 4320 },
        { "01100", 720 },//添一个形成011110
        { "00110", 720 },
        { "01010", 720 },
        { "1110",  720 },
        { "0111",  720 },
        { "1011",  720 },
        { "1101",  720 },
        { "00100", 120 },//添两个形成011110,且必有一边有两个0
        { "01000", 120 },
        { "00010", 120 },
    };

这里棋盘中使用1表示当前角色的棋子,0表示无人落子。
分数分为三个层次,

  1. 若存在四子相连,就是有绝对统治力的50000分;
  2. 若三子相连,且两端都是空白,这个棋形下一步也必胜了,就是4320分;
  3. 若再添一子,可以得到情况2的棋形,就是第三层次720分;
  4. 若再添一子,可以得到情况3的棋形,就是第四层次120分。

二、模式匹配算法

给出棋盘上某一条线上的棋子分布情况,如何计算他有多少个成功匹配了上述打分表中模式的字符串?在python可以使用正则表达式,我程序中使用的是AC树算法。完整代码如下:

//ACSearch.h
#pragma once

#include <string>
#include <vector>
#include <map>
using namespace std;

//trie树节点
//以数组来存树,那就是一个很浪费的结构了,还不能随时改
struct ACNode {
    ACNode(int p, char c)
        :parent(p),
        ch(c),
        fail(-1)
    {
    }

    char ch;
    //定义一个char 到int 的一一对应的映射对象
    map<char, int> sons;
    //构建fail指针,指向查询失败之后的起始节点
    int fail;
    vector<int> output;
    int parent;
};

//AC算法类
class ACSearcher
{
public:
    ACSearcher();
    ~ACSearcher();

    void LoadPattern(const vector<string>& paterns);
    void BuildGotoTable();
    void BuildFailTable();
    vector<int> ACSearch(const string& text);           //返回匹配到的模式的索引

private:
    int maxState;                                       //最大状态数,树中结点的个数
    vector<ACNode> nodes;                               //trie树,不是以根节点指针形式存储
    vector<string> paterns;                             //需要匹配的模式,pattern就相当于单词

    void AddState(int parent, char ch);                                    //初始化新状态
};


实现代码

//ACSearch.cpp
#include "stdafx.h"
#include "ACSearcher.h"

#include <cassert>


ACSearcher::ACSearcher()
    :maxState(0)
{
    //初始化根节点,node.size为1
    AddState(-1, 'a');
    nodes[0].fail = -1;

}


ACSearcher::~ACSearcher()
{
}


void ACSearcher::LoadPattern(const vector<string>& paterns) {
    this->paterns = paterns;
}

void ACSearcher::BuildGotoTable() {
    assert(nodes.size());

    unsigned int i, j;
    //单词的个数
    for (i = 0; i < paterns.size(); i++) {
        //从根节点开始
        int currentIndex = 0;
        //pattern[i][j]是一个char
        for (j = 0; j < paterns[i].size(); j++) {
            if (nodes[currentIndex].sons.find(paterns[i][j]) == nodes[currentIndex].sons.end()) {
                //如果没有该int,就顺序加1
                nodes[currentIndex].sons[paterns[i][j]] = ++maxState;

                //生成新节点,以currentIndex为父节点,以patternij为ch
                AddState(currentIndex, paterns[i][j]);
                currentIndex = maxState;
            }
            else {
                currentIndex = nodes[currentIndex].sons[paterns[i][j]];
            }
        }

        nodes[currentIndex].output.push_back(i);
    }
}

void ACSearcher::BuildFailTable() {
    assert(nodes.size());

    //中间节点收集器
    vector<int> midNodesIndex;

    //给第一层的节点设置fail为0,并把第二层节点加入到midState里
    ACNode root = nodes[0];

    map<char, int>::iterator iter1, iter2;
    for (iter1 = root.sons.begin(); iter1 != root.sons.end(); iter1++) {
        nodes[iter1->second].fail = 0;
        ACNode &currentNode = nodes[iter1->second];

        //收集第三层节点
        for (iter2 = currentNode.sons.begin(); iter2 != currentNode.sons.end(); iter2++) {
            midNodesIndex.push_back(iter2->second);
        }
    }

    //广度优先遍历
    while (midNodesIndex.size()) {
        vector<int> newMidNodesIndex;

        unsigned int i;
        for (i = 0; i < midNodesIndex.size(); i++) {
            ACNode &currentNode = nodes[midNodesIndex[i]];

            //以下循环为寻找当前节点的fail值
            int currentFail = nodes[currentNode.parent].fail;
            while (true) {
                ACNode &currentFailNode = nodes[currentFail];

                if (currentFailNode.sons.find(currentNode.ch) != currentFailNode.sons.end()) {
                    //成功找到该节点的fail值
                    currentNode.fail = currentFailNode.sons.find(currentNode.ch)->second;

                    //后缀包含
                    if (nodes[currentNode.fail].output.size()) {
                        currentNode.output.insert(currentNode.output.end(), nodes[currentNode.fail].output.begin(), nodes[currentNode.fail].output.end());
                    }

                    break;
                }
                else {
                    currentFail = currentFailNode.fail;
                }

                //如果是根节点
                if (currentFail == -1) {
                    currentNode.fail = 0;
                    break;
                }
            }

            //收集下一层节点
            for (iter1 = currentNode.sons.begin(); iter1 != currentNode.sons.end(); iter1++) {
                //收集下一层节点
                newMidNodesIndex.push_back(iter1->second);
            }
        }
        midNodesIndex = newMidNodesIndex;
    }
}

vector<int> ACSearcher::ACSearch(const string& text) {
    vector<int> result;

    //初始化为根节点
    int currentIndex = 0;

    unsigned int i;
    map<char, int>::iterator tmpIter;
    for (i = 0; i < text.size();) {
        //顺着trie树查找
        if ((tmpIter = nodes[currentIndex].sons.find(text[i])) != nodes[currentIndex].sons.end()) {
            currentIndex = tmpIter->second;
            i++;
        }
        else {
            //失配的情况
            while (nodes[currentIndex].fail != -1 && nodes[currentIndex].sons.find(text[i]) == nodes[currentIndex].sons.end()) {
                currentIndex = nodes[currentIndex].fail;
            }

            //如果没有成功找到合适的fail
            if (nodes[currentIndex].sons.find(text[i]) == nodes[currentIndex].sons.end()) {
                i++;
            }
        }

        if (nodes[currentIndex].output.size()) {
            result.insert(result.end(), nodes[currentIndex].output.begin(), nodes[currentIndex].output.end());
        }

    }

    return result;
}
//nodes数组加了一个
void ACSearcher::AddState(int parent, char ch) {
    nodes.push_back(ACNode(parent, ch));
    assert(nodes.size() - 1 == maxState);
}

三、给棋局打分

保存一个int型全局变量allscores[2],里面存放当前棋局的分数,有两个值是因为要在双方的角度都打一次分。
为了减少内存消耗,只需要在存在棋子的坐标计算分数。如图,就只用计算8个棋子延伸出的四个方向的路线的总分。
在这里插入图片描述

四、保存棋局

使用哈希散列值保存棋局,在递归时可以避免为不同顺序的重复棋局打分。
定义一个产生64位随机值的函数:

//生成64位随机数
    long long random64() {
        return (long long)rand() | ((long long)rand() << 15) | ((long long)rand() << 30) | ((long long)rand() << 45) | ((long long)rand() << 60);
    }

再根据散列函数定义一个长度为2^^16的数组用来保存每一个棋局的棋子分布、分数、是否是alpha或beta,是否保存了棋盘。

//保存棋局的哈希表条目
    struct HashItem {
        long long checksum;			//保存当前棋局的哈希值,取值时作为检验序列。
        int depth; 					//递归深度
        int score;
        //标签,EMPTY表示这个棋盘是空的,ALPHA表示棋盘作为最大值,BETA表示棋盘作为另一方的最小值,EXACT表示棋局存在且没有被剪枝掉。
        enum Flag { ALPHA = 0, BETA = 1, EXACT = 2, EMPTY = 3 } flag;
    };
//记录计算结果在哈希表中
    void recordHashItem(int depth, int score, HashItem::Flag flag) {
        //做了与运算,取最后面的16位,还是随机值,用作哈希的键
        int index = (int)(currentZobristValue & HASH_ITEM_INDEX_MASK);
        //哈希的值,键为index
        HashItem* phashItem = &hashItems[index];
        //合法检测,万一这个随机值已经存在一个对应的散列值了,就不记录
        if (phashItem->flag != HashItem::EMPTY && phashItem->depth > depth) {
            return;
        }
        //checksum是一个随机值,拿checksum的后64位可以在哈希表中取值
        phashItem->checksum = currentZobristValue;
        phashItem->score = score;
        phashItem->flag = flag;
        phashItem->depth = depth;
    }

五、最大最小算法与alpha-beta剪枝

整体来说是一个递归过程。
在已有棋局的条件下,定义一个递归查找的深度。
在这里插入图片描述

退出条件:

  1. 查找到所需的深度,返回score1 - score2 ,其中score1是该棋盘的己方视角的局面分数allscore[0];score2是对方视角下的局面分数allscore[1].
  2. 某一棋局的哈希值对应在哈希表中不为EMPTY,也就表明已经存在其他顺序到达这一棋局,就直接返回存好的该棋局的score。
  3. 某一棋局的下一层已经全部计算完了,根据需要更新alpha的值。
  4. 某一棋局出现了50000分,这就表明这一棋局己方可以胜利,返回一个相当大的值,这样之后的alpha就要更新。
int abSearch(char board[BOARD_WIDTH][BOARD_WIDTH], int depth, int alpha, int beta, Role currentSearchRole) {
        HashItem::Flag flag = HashItem::ALPHA;
        int score = getHashItemScore(depth, alpha, beta);
        //如果这个哈希值已经存了这一种局面的棋局,就直接返回分数,不用计算
        if (score != UNKNOWN_SCORE && depth != DEPTH) {
            return score;
        }
        //记录board对两方棋局的评分
        int score1 = evaluate(board, currentSearchRole);
        int score2 = evaluate(board, currentSearchRole == HUMAN ? COMPUTOR : HUMAN);

        //如果棋局的总分大于50000,表示在这个深度有一个己方胜利的局面,则它的上一层的分数为一个较大的值
        if (score1 >= 50000) {
            return MAX_SCORE - 1000 - (DEPTH - depth);
        }
        //对手胜利
        if (score2 >= 50000) {
            return MIN_SCORE + 1000 + (DEPTH - depth);
        }

        //若这个分支递归结束,那就存入哈希表,返回当前角色的分数优势
        if (depth == 0) {
            recordHashItem(depth, score1 - score2, HashItem::EXACT);
            return score1 - score2;
        }

        //set<Position> possiblePossitions = createPossiblePosition(board);


        int count = 0;
        set<Position> possiblePositions;
        const set<Position>& tmpPossiblePositions = ppm.GetCurrentPossiblePositions();

        //对当前可能出现的位置进行粗略评分
        set<Position>::iterator iter;
        for (iter = tmpPossiblePositions.begin(); iter != tmpPossiblePositions.end(); iter++) {
            //得到的是,若下在这里,对双方产生的总分
            possiblePositions.insert(Position(iter->x, iter->y, evaluatePoint(board, *iter)));
        }

        while (!possiblePositions.empty()) {
            Position p = *possiblePositions.begin();

            possiblePositions.erase(possiblePositions.begin());

            //放置棋子
            board[p.x][p.y] = currentSearchRole;
            //这里使用此前定义的两套随机值,这里生成一个临时使用的随机值
            currentZobristValue ^= boardZobristValue[currentSearchRole - 1][p.x][p.y];
            //allscore[2]记录下若p已经下了时的两方总分
            updateScore(board, p);

            //增加可能出现的位置
            p.score = 0;
            //若是p已经下了,更新currentPossiblePositions
            ppm.AddPossiblePositions(board, p);
            //递归
            int val = -abSearch(board, depth - 1, -beta, -alpha, currentSearchRole == HUMAN ? COMPUTOR : HUMAN);
            if (depth == DEPTH)
                cout << "score(" << p.x << "," << p.y << "):" << val << endl;

            //取消上一次增加的可能出现的位置
            ppm.Rollback();

            //取消放置
            board[p.x][p.y] = 0;
            //散列值复原
            currentZobristValue ^= boardZobristValue[currentSearchRole - 1][p.x][p.y];
            //allscores[2]的值恢复到没有p时的水平
            updateScore(board, p);
            //负值,这里绝对值小于beta,所以不改变beta的值,这个val基本可以舍弃了
            if (val >= beta) {
                //存入currentZobristValue对应的棋局里面
                recordHashItem(depth, beta, HashItem::BETA);
                return beta;
            }
            //大于最大值,就更新alpha的值
            if (val > alpha) {
                flag = HashItem::EXACT;
                alpha = val;
                //如果这是第一层,且得到的val大于最大值,就暂定搜索结果为p
                if (depth == DEPTH) {
                    searchResult = p;
                }
            }

            count++;
            if (count >= 9) {
                break;
            }
        }
        //循环结束后,表示下一深度的所有棋局遍历结束,存下这个棋局的alpha和beta值,返回最大值alpha.因此第一层得到的值就是所有条件中最大的alpha
        recordHashItem(depth, alpha, flag);
        return alpha;

    }

六、AI判断胜负

在前面的递归中,可以知道AI是要往最大alpha的方向走,那么在接近己方胜利的时候,alpha是不断接近最大值的,当查询深度depth为0时,对应alpha得到最大值,此时得到胜利者。

//获得下一步的走法
Position getAGoodMove(char board[BOARD_WIDTH][BOARD_WIDTH]) {
    int score = abSearch(board, DEPTH, MIN_SCORE, MAX_SCORE, COMPUTOR);
    if (score >= MAX_SCORE - 1000 - 1) {
        winner = COMPUTOR;
    }
    else if (score <= MIN_SCORE + 1000 + 1) {
        winner = HUMAN;
    }

    return searchResult;
}

暂时就写这么多了,这个源代码是根据github上某位大佬写的五子棋程序修改的,附上原链接:
如何为五子棋设计一个还可以的AI

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值