一,评分表设置
基本思路就是给棋局打分,棋局的分数是建立在每一条线上特定模式的分数,我的打分表是在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表示无人落子。
分数分为三个层次,
- 若存在四子相连,就是有绝对统治力的50000分;
- 若三子相连,且两端都是空白,这个棋形下一步也必胜了,就是4320分;
- 若再添一子,可以得到情况2的棋形,就是第三层次720分;
- 若再添一子,可以得到情况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 ¤tNode = 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 ¤tNode = nodes[midNodesIndex[i]];
//以下循环为寻找当前节点的fail值
int currentFail = nodes[currentNode.parent].fail;
while (true) {
ACNode ¤tFailNode = 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剪枝
整体来说是一个递归过程。
在已有棋局的条件下,定义一个递归查找的深度。
退出条件:
- 查找到所需的深度,返回score1 - score2 ,其中score1是该棋盘的己方视角的局面分数allscore[0];score2是对方视角下的局面分数allscore[1].
- 某一棋局的哈希值对应在哈希表中不为EMPTY,也就表明已经存在其他顺序到达这一棋局,就直接返回存好的该棋局的score。
- 某一棋局的下一层已经全部计算完了,根据需要更新alpha的值。
- 某一棋局出现了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