一字棋指的是:在一个九宫格内率先连成三个字的取胜
首先,基于前面决策树的讲解 博弈的棋类游戏等等 只要找到合适的估值函数都可以使用博弈树来实现 下面我们来使用博弈树完成一字棋的算法。
根据前面的算法思想我们算法大致分为几步:
1.对棋局落子有正确的估值
2.通过遍历建立博弈树
3.对博弈树进行α-β剪枝增快查找速度(这里由于数据量较小 放在最后一起讲解)
4.根据极大值 极小值搜索获取博弈树产生的结果
首先在我们假设电脑先走 这时通过
在博弈树中通过这段代码在open表中将所有棋局储存 并且打分
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(closed[closedtop]->all[i][j]==N) //在firstox中已经将棋局传入
{
opentop++;//新节点于open表入队
open[opentop]=new OXchess;//开辟空间
open[opentop]->should(closed[closedtop],closedtop,i,j,step);//对新节点进行合适的操作
}
在经历完一轮循环后 将所有的open的元素 入closed表 (此时closed中的所有元素 flag = 1)
也就是记录了计算机所有的落子位置 下面进行第二次循环 同理完成人类落子 。。。(可以一直模拟下去 但是随着模拟的回合增加 由于估值函数不可能完全拟合 会产生过拟合的现象)
并且如果完全模拟完所有的情况有9! = 362 880种情况
此时编译器也会报错 这里我们将博弈树深度设为5
会发现 AI出现了问题 由于拟合层数过多 但是估值函数较为粗糙 导致随着拟合次数的增加 会使估值的误差也快速上涨 导致出现过拟合的问题
我们可以通过调整估值代码的参数来完成更多层数的拟合 通过观察可以发现这个问题是 对于我马上要形成三个棋子并且下一步是我下棋的估值过低 导致在不断的迭代过程中 自己形成二子的得分甚至高于防止对手形成三子 对于这种情况就要提高估值 在这个游戏中往后看两步足以取胜 我们选择迭代次数为2 也就是flagmax = 2
void toscore()//得到当前局面score值的函数
{
score=0;//score复位为0
int i,j,o,x,n;//i.j循环用,o,x,n分别代表某一路(连续三子为一路)上oxn棋子的数目
for(i=0;i<3;i++)//横向
{
o=0;x=0;n=0;//每一路之前要复位置零
for(j=0;j<3;j++)
{
if(all[i][j]==O)//o计数
o++;
if(all[i][j]==X)//x计数
x++;
if(all[i][j]==N)//n计数
n++;
if(o+n==3)//当这一路上只有O棋子与空棋子时
{
if(o==3)//O有3子
score=score+999999;//这种棋面已赢,评估值无限大
if(o==2)//O有2子
{
if(flag%2==1)//如果这种棋面的层数是奇数,说明下一步是计算机下O棋,当某一路上已有2个O子时,已经必胜
//评估值很大,但要小于对方已赢棋面评估值的绝对值,否则会产生不去围堵对方的胜招,而自顾自做2连子的棋面
score=score+20000;
else//如果下一步是人类下棋,这种局面的评估值不是很大
score=score+1000;
}
if(o==1)//O有1子
score=score+100;//加一点评估值
}
if(x+n==3)//当这一路上只有X棋子与空棋子时
{
if(x==3)//X有3子
score=score-99999;//人类已经赢的棋面,评估值无限小
//但绝对值要小于计算机已赢棋面的绝对值,否则会产生明明自己走在某处就可以直接胜利还偏偏去围堵对方的胜招的情况
if(x==2)//X有2子
{
if(flag%2==0)//如果下一步是人类下棋,评估值很小
score=score-10000;
else//如果下一步是计算机下棋,评估值不是很小
score=score-1000;
}
if(x==1)//X有1子
score=score-100;//减一点评估值
}
//此处没有写oxn都有的情况 因为这种情况 这一条路 谁也赢不了
}
}
for(i=0;i<3;i++)//竖向,下面同上
{
o=0;x=0;n=0;
for(j=0;j<3;j++)
{
if(all[j][i]==O)
o++;
if(all[j][i]==X)
x++;
if(all[j][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//左上——右下,下面同上
{
if(all[i][i]==O)
o++;
if(all[i][i]==X)
x++;
if(all[i][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//右上——左下,下面同上
{
if(all[i][2-i]==O)
o++;
if(all[i][2-i]==X)
x++;
if(all[i][2-i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
};
下面完成极大值极小值搜索 但是这里的树由于使用线性表实现 因此是一棵线性的树
上图是对比图注意 不同的颜色代表不同的深度 于是下面使用极大极小值搜索 一个回合(人一次 计算机一次)完成一次替换
for(int lag=closedtop;lag>0;lag--)//从closed表栈顶处向下扫描,通过叶子节点的score值给整个博弈树各节点的score赋值
{
if(closed[lag]->flag%2==0)//或节点 人类下棋时
{//或节点的父节点是与节点,与节点的score值取其子节点中最小的score值
if(closed[lag]->score<=closed[closed[lag]->parent]->score)//如果该节点的score值比其父节点的score值低
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把该节点的score值赋给父节点
}
}
else//与节点 电脑下棋时
{//与节点的父节点是或节点,或节点的score值取其子节点中最大的score值
if(closed[lag]->score>=closed[closed[lag]->parent]->score)//如果该节点的score值比其父节点的score值高
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把该节点的score值赋给父节点
if(closed[lag]->flag==1)//如果该节点是第一层节点,说明此时博弈树选择的第一层节点就是该节点
tag=lag;//记录下该节点在closed表中的数组下标
}
}
}
最后附上所有代码以供参考:
#include
using namespace std;
enum Chess{O,X,N};//棋子的类型:O代表计算机,X代表人类,N代表空
enum Node{and,or};//棋盘状态节点的类型:and代表与节点 我方下棋,or代表或节点 对手下棋
#define maxsize 9999//open和closed表的最大容量
#define flagmax 2//分析最高层数(也就是最多分析的回合),设为2时AI出错率为0,设为更高值时,会出现过拟合现象,导致拟合效果下降
class OXchess//棋盘状态节点类,用于博弈树搜索
{
public:
Chess all[3][3];//当前棋盘状态
int play[2];//当前落子位置 play[0]为x play[1]为y
int parent;//当前父节点指针
int score;//当前棋盘状态分数
int flag;//当前扩展层数
Node node;//当前节点类型
OXchess()//构造函数,用来初始化棋盘状态节点
{
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
all[i][j]=N;//棋盘置空
play[0]=-1;play[1]=-1;//落子为空
parent=-2;//父节点指针为空
score=9999;//score初始值
flag=0;//flag初始值
node=or;//节点类型初始值
}
~OXchess(){}//析构函数
void should(OXchess *a,int closedtop,int x,int y,int step)//对每个新产生的节点应该做的操作的函数(落子、棋局打分)
{
for(int i=0;i<3;i++)//首先复制父节点的棋盘
for(int j=0;j<3;j++)
{
all[i][j]=a->all[i][j];
}
play[0]=x;play[1]=y;//然后根据参数存储即将的落子
parent=closedtop;//确定父节点指针
flag=a->flag+1;//当前层数为父节点+1
if(flag%2==0)//当该节点层数为偶数时
{
score=-999999999;//score设为无限小,方便与其子节点中score较大的值比较,并获取其值
node=or;//节点类型为或节点
playchess(x,y,X);//在落子坐标放上人类的棋子
}
else//当该节点层数为奇数时
{
score=999999999;//score设为无限大,方便与其子节点中score较小的值比较,并获取其值
node=and;//节点类型为与节点
playchess(x,y,O);//在落子处坐标放上计算机的棋子
}
if(flag==flagmax || step==8)//当该节点为无法再扩展的节点时,扫描棋盘通过局面得到score值
{
toscore();//得到当前局面score值
}
}
void playchess(int x,int y,Chess z)//落子的函数
{
all[x][y]=z;//在坐标x,y处落子z
}
void copyOX(OXchess *a)//复制同类对象的函数
{
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
all[i][j]=a->all[i][j];//复制棋盘
play[0]=a->play[0];play[1]=a->play[1];//复制落子位置
parent=a->parent;//复制父节点
score=a->score;//复制score
flag=a->flag;//复制层数
node=a->node;//复制节点类型
}
void toscore()//得到当前局面score值的函数
{
score=0;//score复位为0
int i,j,o,x,n;//i.j循环用,o,x,n分别代表某一路(连续三子为一路)上oxn棋子的数目
for(i=0;i<3;i++)//横向
{
o=0;x=0;n=0;//每一路之前要复位置零
for(j=0;j<3;j++)
{
if(all[i][j]==O)//o计数
o++;
if(all[i][j]==X)//x计数
x++;
if(all[i][j]==N)//n计数
n++;
if(o+n==3)//当这一路上只有O棋子与空棋子时
{
if(o==3)//O有3子
score=score+999999;//这种棋面已赢,评估值无限大
if(o==2)//O有2子
{
if(flag%2==1)//如果这种棋面的层数是奇数,说明下一步是计算机下O棋,当某一路上已有2个O子时,已经必胜
//评估值很大,但要小于对方已赢棋面评估值的绝对值,否则会产生不去围堵对方的胜招,而自顾自做2连子的棋面
score=score+20000;
else//如果下一步是人类下棋,这种局面的评估值不是很大
score=score+1000;
}
if(o==1)//O有1子
score=score+100;//加一点评估值
}
if(x+n==3)//当这一路上只有X棋子与空棋子时
{
if(x==3)//X有3子
score=score-99999;//人类已经赢的棋面,评估值无限小
//但绝对值要小于计算机已赢棋面的绝对值,否则会产生明明自己走在某处就可以直接胜利还偏偏去围堵对方的胜招的情况
if(x==2)//X有2子
{
if(flag%2==0)//如果下一步是人类下棋,评估值很小
score=score-10000;
else//如果下一步是计算机下棋,评估值不是很小
score=score-1000;
}
if(x==1)//X有1子
score=score-100;//减一点评估值
}
//此处没有写oxn都有的情况 因为这种情况 这一条路 谁也赢不了
}
}
for(i=0;i<3;i++)//竖向,下面同上
{
o=0;x=0;n=0;
for(j=0;j<3;j++)
{
if(all[j][i]==O)
o++;
if(all[j][i]==X)
x++;
if(all[j][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//左上——右下,下面同上
{
if(all[i][i]==O)
o++;
if(all[i][i]==X)
x++;
if(all[i][i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
o=0;x=0;n=0;
for(i=0;i<3;i++)//右上——左下,下面同上
{
if(all[i][2-i]==O)
o++;
if(all[i][2-i]==X)
x++;
if(all[i][2-i]==N)
n++;
if(o+n==3)
{
if(o==3)
score=score+999999;
if(o==2)
{
if(flag%2==1)
score=score+20000;
else
score=score+1000;
}
if(o==1)
score=score+100;
}
if(x+n==3)
{
if(x==3)
score=score-99999;
if(x==2)
{
if(flag%2==0)
score=score-10000;
else
score=score-1000;
}
if(x==1)
score=score-100;
}
}
}
};
class OX//游戏类实体
{
public:
Chess chess[3][3];//记录当前棋盘状态
int step;//记录当前已走步数
Chess wholast;//记录上一步走棋的是谁
bool ifend;//记录游戏是否结束
bool winner;//记录游戏是否有胜者产生
bool humanfirst;//记录是人先走还是计算机先走
OX()//构造函数,初始化游戏
{
char command,del;//command用来接收指令 a.人先走棋 b.电脑先走 。 del用来吸收换行符。
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
chess[i][j]=N;//初始化棋盘
wholast=N;//初始化wholast
step=0;//初始化step
ifend=false;//初始化ifend
winner=false;//初始化winner
cout<<"一字棋启动ing......"<";
command=getchar();//获取指令
del=getchar();//吸收换行符
while(command!='a' && command!='b')//指令输错的时候要求玩家重新输
{
cout<<"小老弟走点心,按着我说的来,请重新选择(a.人先走棋 b.电脑先走):"<";
command=getchar();
del=getchar();
}
if(command=='a')//初始化humanfirst
humanfirst=true;
else
humanfirst=false;
};
~OX()//析构函数,在游戏结束时 输出棋盘 然后 输出游戏结果
{
display();//输出棋盘
if(winner)//存在胜者时输出谁赢了
{
if(wholast==O)
cout<<"我赢了!人类被我打败了 啊哈哈哈"<";
cin>>y>>x;//输入指令
while(!(y>0 && x>0 && y<4 && x<4 && chess[x-1][y-1]==N) )//指令输错的时候要求玩家重新输
{
cout<<"你怎么不按规则和我下棋啊!:(例:若要下在棋盘中横坐标为1,纵坐标为3的位置,请输入:1+空格+3即:1 3)"<";
cin>>y>>x;
}
x--;y--;//棋盘坐标与数组坐标的变换
play(x,y,X);//人类下棋
wholast=X;//wholast置X,代表最近一步棋是人类下的
step++;//已走步数+1
}
void computerplay()//计算机下棋的函数
{
int x[2]={-1,-1};//保存棋招
minmax(x);//极大极小法获取棋招 传入的是数组首地址
play(x[0],x[1],O);//按棋招下棋
cout<copyOX(open[openrear]);//把元素复制过去
free(open[openrear]);//再回收旧空间
//只执行一次将所有可以落子的位置 遍历打分
if(closed[closedtop]->flagall[i][j]==N) //在firstox中已经将棋局传入
{
opentop++;//新节点于open表入队
open[opentop]=new OXchess;//开辟空间
open[opentop]->should(closed[closedtop],closedtop,i,j,step);//对新节点进行合适的操作
}
}
}
//极大极小搜索
for(int lag=closedtop;lag>0;lag--)//从closed表栈顶处向下扫描,通过叶子节点的score值给整个博弈树各节点的score赋值
{
if(closed[lag]->flag%2==0)//或节点 人类下棋时
{//或节点的父节点是与节点,与节点的score值取其子节点中最小的score值
if(closed[lag]->score<=closed[closed[lag]->parent]->score)//如果该节点的score值比其父节点的score值低
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把该节点的score值赋给父节点
}
}
else//与节点 电脑下棋时
{//与节点的父节点是或节点,或节点的score值取其子节点中最大的score值
if(closed[lag]->score>=closed[closed[lag]->parent]->score)//如果该节点的score值比其父节点的score值高
{
closed[closed[lag]->parent]->score=closed[lag]->score;//就把该节点的score值赋给父节点
if(closed[lag]->flag==1)//如果该节点是第一层节点,说明此时博弈树选择的第一层节点就是该节点
tag=lag;//记录下该节点在closed表中的数组下标
}
}
}
a[0]=closed[tag]->play[0];//获取最终博弈树选择的那个节点所存储的棋招纵坐标
a[1]=closed[tag]->play[1];//获取最终博弈树选择的那个节点所存储的棋招横坐标
}
void firstOX(OXchess *a)//open表生成第一个节点的函数
{
a->parent=-1;//parent置-1
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
a->all[i][j]=chess[i][j];// 从游戏实例获取棋盘局面
a->score=-999999999;//score置无限小
}
};
int main()//主函数
{
OX OX1;//定义游戏实体,自动调用构造函数OX();初始化游戏
if(OX1.humanfirst)//如果人先走
OX1.humanplay();//那就人走
else//如果计算机先走
OX1.computerplay();//那就计算机走
OX1.getend();//判断局势,判断ifend和winner的值是否需要改变
while(!OX1.ifend)//如果游戏没结束
{
if(OX1.wholast==O)//如果上一步走棋的是计算机
OX1.humanplay();//那么轮到人类走棋
else//如果上一步走棋的是人类
OX1.computerplay();//那么轮到计算机走棋
OX1.getend();//判断局势,判断ifend和winner的值是否需要改变
}//如果游戏结束,退出程序时自动调用OX类的析构函数~OX();,输出游戏结果。
return 0;
}