井字棋程序的设计与算法

井字棋程序的设计与算法

作者姓名:周翔
电子邮箱:seafrog@163.com

摘要:
本文就作者编写的井字棋程序进行了简要的介绍,并重点介绍了该程序采用的算法、程序设计方案、对算法的改进等内容。

关键字:井字棋,评估函数,极大极小值算法,α-β剪枝算法

1.程序说明

本程序旨在完成一个具有人机博弈功能的井字棋程序,具有良好的用户界面、支持人机对弈和双人对弈两种模式,并具有悔棋、选择难易级别等功能。该程序是中科院研究生院2005年秋季学期《人工智能原理》课程的项目一。

2. 设计方案

2.1 设计步骤

本程序最主要的任务是完成图形界面的井字棋的人机对弈模块的设计。在人机对弈过程中,计算机方所采用的算法,也就是博弈树的搜索技术是最重要的。所以,设计时,作者按照以下步骤进行:

(1)选定博弈算法;
(2)建立一个简单的应用程序(如字符界面程序)来测试算法;
(3)选定图形界面中要实现的其他功能(如双人对弈、悔棋、难易级别选定、联机对战等);
(4)实现图形界面的井字棋程序。

所采用的核心算法将在第3节介绍。本程序要实现两个程序,一个是字符界面的算法测试程序,另一个是要正式提交的图形界面程序。下面是对这两个程序的设计方案的介绍。

2.2 字符界面的算法测试程序

该测试程序由标准C++编写,作者采用了极大极小值算法。除了主程序外,它还包括具有以下功能的函数:
(1)棋盘初始化函数:void Init();
(2)打印棋盘函数:void PrintQP();
(3)用户输入落子位置函数void UserInput();
(4)判断当前棋局是否有一方获胜,并判断哪一方获胜的函数:int IsWin(State s);
(5)评估函数值计算函数:int e_fun(State s);
(6)极大极小值算法主函数:int AutoDone();

其中前三个函数是对程序中当前的棋局进行读写操作的函数,第4、5个函数是对当前的棋局进行判断的函数,最后一个函数中包含了计算机决定在哪个位置落子所采用的核心算法,并且可以判断计算机落子前后棋局的状态,如果在搜索树的深度范围内能判断哪一方必胜,则可提前打印输赢信息,并结束本棋局。字符界面的测试程序的运行过程如图1所示。

2.3 图形界面的井字棋程序

图形界面的井字棋程序使用MFC(微软基础类库)来构建图形界面,同时将字符界面程序中的void Init()、int IsWin(State s)、int e_fun(State s)、int AutoDone()四个函数拿出来放在一个独立的头文件中。棋盘绘制和显示对弈状态信息通过视图类CJzqView类来实现,而且所有的井字棋对弈功能的实现全部包含在CJzqView类中。作者定义的一些菜单消息如下所示,菜单消息包含了该程序实现的所有功能:

ID_NEW_QP:新开一局
ID_QP_HUI:悔棋
ID_AUTO_GAME:人机对弈
ID_TWO_MAN:双人对弈
ID_MAN_FIRST:设定人先下
ID_COMPUTER_FIRST:设定计算机先下
ID_EASY、ID_MID、ID_HARD:设定难易级别(容易、中等、困难)

此外,我还定义了ON_WM_LBUTTONUP消息的响应函数,以便于用户落子。此外,对弈结束后,用鼠标左键点一下棋盘就可以开始新的对弈,而以前的设定保持不变。

http://blog.csdn.net/images/blog_csdn_net/hifrog/157225/r_jzq_1.GIF

图1:字符界面的井字棋程序的运行过程

需要说明的是,在处理双人对战时,虽然程序不会调用AutoDone和e_fun函数,但是程序把参与对弈的第二个人当做“电脑”,即,用来标志电脑的变量被用来标识第二个人,这样可以减少变量的个数。比如ComputerFirst=1时,则表示第二个人先下,MansTurn=0时,则表示当前该第二个人落子。

3. 核心算法及源代码

3.1 定义的数据结构和变量

由于本程序采用的核心算法是极大极小值算法,所以,在实现算法之前,必须定义一些数据结构来保存生成的状态节点。因此,我定义了State结构:

struct State//该结构表示棋盘的某个状态,也可看做搜索树中的一个节点
{
int QP[3][3];//棋盘格局
int e_fun;//当前状态的评估函数值
int child[9];//儿女节点的下标
int parent;//双亲节点的下标
int bestChild;//最优节点(评估函数值最大或最小)的儿女节点下标
}States[MAX_NUM];//用来保存搜索树中状态节点的数组

我使用了States[MAX_NUM]数组来保存生成的状态节点,通过State结构中的parent、child域构成了一个搜索树,并通过bestChild域保存了一条从根节点到叶节点的最优解路径。特别的,States[0]作为根节点保存了当前的棋局状态。为了保存当前对弈过程的状态信息,我定义了以下常量:

//以下常量表示棋局当前的状态
const int DRAW_GAME=1100;//棋局为平局
const int COMPUTER_WIN=1101;//计算机赢了
const int MAN_WIN=1102;//人赢了
const int PLAYING=1103;//对弈正在进行
const int WAIT_4_PLAY=1104;//正在等待开始

const int MAX_NUM=1000;//扩展生成状态节点的最大数目
const int NO_BLANK=-1001;//表示没有空格:棋盘上没有空余的位置来落子
const int NIL=1001;//表示空

并定义下列全局变量保存临时信息:
static bool MansTurn;//是否该人下了
static bool ComputerFirst;//是否计算机先下
static int Level;//当前难易级别
static int Mode;//模式:是人机对战(1)还是双人对战(0)
static intNow;//表示棋局现在的状态
static intpos_x,pos_y;//表示计算机落子的位置的x,y坐标
static intTREE_DEPTH=2;//搜索树的最大深度,如果增加此值可以提高计算机的“智力”,
//但同时也需要增加MAX_NUM的值。
static int s_count;//用来表示当前分析的节点的下标

同时,我还定义了两个3×3的二维数组,AutoDone函数中要用到其中的一个来保存临时的棋盘格局,另一个则是用来保存上一步的棋盘格局,悔棋时可将此棋局覆盖当前的棋局States[0]。

3.2 算法及源代码

极大极小值算法的详细介绍请参看参考文献[2](第53页至55页)。在程序实现时,我使用了和[2]中相同的评估函数。评估函数的源代码如下所示:

int e_fun(State s)//评估函数
{
bool flag=true;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)flag=false;
if(flag)return NO_BLANK;

if(IsWin(s)==-1)return -MAX_NUM;
if(IsWin(s)==1)return MAX_NUM;
int count=0;//count变量用来保存评估函数的值

//将棋盘中的空格填满自己的棋子,既将棋盘数组中的0变为1
for(i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)tmpQP[i][j]=1;
else tmpQP[i][j]=s.QP[i][j];

//电脑一方
//计算每一行中有多少行的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
//计算每一列中有多少列的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
//斜行有没有连成3个的?
count+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;
count+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;

//将棋盘中的空格填满对方的棋子,既将棋盘数组中的0变为-1
for(i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)tmpQP[i][j]=-1;
else tmpQP[i][j]=s.QP[i][j];

//对方
//计算每一行中有多少行的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
//计算每一列中有多少列的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
//斜行有没有连成3个的?
count+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;
count+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;

return count;
}

整个算法在AutoDone函数中实现,其实现过程分为以下几步:

(1)为了获得最优的落子位置,在算法中首先要生成搜索树。其中,我把States[0]作为树根节点,根节点所在的层是极大层(MAX层),然后通过向棋盘中没有落子的空格添一个对方的棋子生成下一层(极小层,MIN层)的树节点,如果当前树的高度大于等于TREE_DEPTH(>=1)全局变量,则停止生成节点,否则则继续生成下一层节点(如果当前节点层为MIN层,则下一层为MAX层,否则,则下一层为MIN层)。生成每一层时可为每一层的属性(MAX或MIN)做标记,生成每个节点时,应计算这个节点的评估函数值,并将其保存在状态节点的e_fun域中。这一步的源代码如下所示:

for(int t=0;t<TREE_DEPTH;t++)//依次生成各层节点
{
count=s_count;//保存上一层节点生成的最大下标
for(int k=parent+1;k<=count;k++)//生成一层节点
{
int n_child=0;//该层节点的孩子节点数初始化为0
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(States[k].QP[i][j]==0)//如果在位置(i,j)可以放置一个棋子
{//则
s_count++;//生成一个节点,节点数(最大下标)数加1
for(int i1=0;i1<3;i1++)//该3×3循环将当前棋局复制到新节点对应的棋局结构中
for(int j1=0;j1<3;j1++)
States[s_count].QP[i1][j1]=States[k].QP[i1][j1];
States[s_count].QP[i][j]=t%2==0?1:-1;//根据是人下还是计算机下,在空位上落子
States[s_count].parent=k;//将父母节点的下标k赋给新生成的节点
States[k].child[n_child++]=s_count;//下标为k的父母节点有多了个子女

//如果下一步有一步棋就能让电脑赢,则停止扩展节点,转向结局打印语句
if(t==0&&e_fun(States[s_count])==MAX_NUM)
{
States[k].e_fun=MAX_NUM;
States[k].bestChild=s_count;//最好的下一步棋所在的节点的下标为s_count
goto L2;
}
}
}
parent=count;//将双亲节点设置为当前双亲节点的下一层节点
//cout<<s_count<<endl;//打印生成节点的最大下标
}

(2)因为层次遍历会修改非叶节点的极大极小值,而且非叶节点原来的极大极小值会对其来自其子女节点的极大极小值产生影响(比如,如果一个非叶节点的极大极小值大于或小于其子女节点中的最大者或小于其中的最小者,则导致其评估函数值无法更新)。所以非叶节点没有必要也不能保存。这一步的源代码如下所示:

tag=States[s_count].parent;//设置最底层标志,以区分叶节点和非叶节点。
for(i=0;i<=s_count;i++)
{
if(i>tag)//保留叶节点的评估函数值
{
States[i].e_fun=e_fun(States[i]);
}
else//抹去非叶节点的评估函数值
States[i].e_fun=NIL;
}

(3)然后,通过层次遍历获得每个非叶节点的评估函数值,同时将非叶节点的bestChild域指向最佳子女,从而形成一条从根节点到叶节点的最佳解路径。这一步的源代码如下所示:

while(!IsOK)//寻找最佳落子的循环
{
for(int i=s_count;i>tag;i--)
{
if(max_min)//取子女节点的最大值
{
if(States[States[i].parent].e_fun<States[i].e_fun||States[States[i].parent].e_fun==NIL)
{
States[States[i].parent].e_fun=States[i].e_fun;//设置父母节点的最大最小值
States[States[i].parent].bestChild=i;//设置父母节点的最佳子女的下标
}
}
else//取子女节点的最小值
{
if(States[States[i].parent].e_fun>States[i].e_fun||States[States[i].parent].e_fun==NIL)
{
States[States[i].parent].e_fun=States[i].e_fun;//设置父母节点的最大最小值
States[States[i].parent].bestChild=i;//设置父母节点的最佳子女的下标
}
}
}
s_count=tag;//将遍历的节点上移一层
max_min=!max_min;//如果该层都是MAX节点,则它的上一层都是MIN节点,反之亦然。
if(States[s_count].parent!=NIL)//如果当前遍历的层中不包含根节点,
//则tag标志设为上一层的最后一个节点的下标
tag=States[s_count].parent;
else
IsOK=true;//否则结束搜索
}

(4) 最后,将当前的棋局更新为其最优子女节点的棋局,并获得落子的位置:

//取落子的位置,将x,y坐标保存在变量pos_x和pos_y中,
//并将根(当前)节点中的棋局设为最佳子女节点的棋局
for(int x=0;x<3;x++)
{
for(int y=0;y<3;y++)
{
if(States[States[0].bestChild].QP[x][y]!=States[0].QP[x][y])
{
pos_x=x;
pos_y=y;
}
States[0].QP[x][y]=States[States[0].bestChild].QP[x][y];
}
}

此外,AutoDone函数中还有一些后续操作(比如判断当前棋局是否有一方获胜、打印状态信息等),请参看附件中的源代码文件zx_jzq.cpp。

3.3 “智商”分析

在我采用的算法中,可以通过增加生成树的层数,即增加TREE_DEPTH的值来提高计算机的智商。这相当于增加了计算机向前预测的步数。对井字棋来说,因为井字棋有9个格,所以TREE_DEPTH的最大值可以设为9,但是实际上,经过试验,当TREE_DEPTH=3时,计算机对井字棋的落子的处理就能达到比较好的效果。在已经实现的程序中,我将游戏难度分为3个等级:容易(TREE_DEPTH=1),中等(TREE_DEPTH=2),困难(TREE_DEPTH=3)。特别的,当TREE_DEPTH=1时,计算机只预测下一步人的走法,而TREE_DEPTH=3时,计算机会综合考虑以后3步的走法。当TREE_DEPTH=3时,程序生成的最大状态节点数为:1+9+9×8+9×8×7=585个,这对现在的计算机来说,是可以在可接受的时间范围内处理完这些状态的。但对于状态个数较多的情况,如中国象棋或围棋,搜索树层数的增加会导致计算机在空间和时间上耗费呈指数级的增长。

当TREE_DEPTH大于等于1时,可以对棋局进行预测,因此就可以提前判断输赢。比如在图1所示的对弈过程中,当棋局矩阵为(1表示一方落下的棋子,-1表示另一方落下的棋子,0表示在该位置上没有棋子):
-1-11
010
001
时,就可以判定执“1”的一方赢了,因为执“1”的一方在(1,3)点的棋子落下以后产生了两个先手,输赢关系可以在3步之内判定,如果把搜索树层数设置为2,则计算机在对手下一步棋之后才会知道自己赢。所以,在这个角度上看,搜索树层数的增加可以提高计算机的智商。

4. 总结

在本程序中的井字棋程序使用了极大极小值算法,这种算法的思想是“考虑双方对弈若干步之后,从可能的走法中选一步相对较好的走法来走”,并且“在有限的搜索深度范围内进行求解”。

最大最小值算法的核心是将搜索树的层分为MAX层和MIN层,MAX层和MIN层交替相邻(即,一个节点如果在MAX层,则其子女节点在MIN层;如果在MIN层,则其子女节点在MAX层),在MAX层的节点的评估函数值取其子女节点中的最大者,在MIN层的节点的评估函数值取其子女节点中的最小者。

此外,需要定义一个评估函数来计算叶节点的评估函数值,要注意将某方获胜的状态节点的评估函数值设为计算机能表示的最大数(无穷大)或最小数(无穷小)以表明在该状态下有一方获胜。

最后,还要“在有限的搜索深度范围内进行求解”,如果搜索深度太大,则在状态数较多的情况下会使时间耗费或空间耗费达到无法忍受的程度。

5. 改进建议

本程序中的程序的博弈算法采用的是极大极小值算法,如果采用α-β剪枝算法,则可以在一定程度上减少博弈树的节点数。假设一棵树的深度为d,且每个非叶节点的分支系数为b,则在最佳情况下,α-β剪枝算法生成深度为d的叶节点数大约相当于极大极小值算法所生成的深度为d/2的博弈树的节点数。也就是说,为了得到最佳的一步,α-β剪枝算法只需要检测O(b^d/2)个节点,而不是极大极小值算法的O(b^d )。从另一个角度看,在相同的代价下,α-β剪枝算法向前看走的步数是极大极小值算法向前看走的步数的两倍(见参考文献[2]第57页)。

参考文献:
[1]林尧瑞等,《人工智能导论》,清华大学出版社,北京,2001。
[2]王文杰等,《人工智能原理与应用》,人民邮电出版社,北京,2004。

附:字符界面的井字棋源代码
/*
代码作者:周翔
代码功能:基于字符界面的井字棋人机交互程序。
说明:对状态空间采用最大最小值搜索技术。计算机在生成的子节点中选择评估函数值最大的节点;
而计算机在选择着数时使“人”选择评估函数值最小的节点,也就是对计算机一方最不利的节点。
*/
#include<iostream>
using namespace std;

int tmpQP[3][3];//表示棋盘数据的临时数组,其中的元素0表示该格为空,
//1表示计算机放下的子,-1表示人放下的子。

const int MAX_NUM=1000;//扩展生成状态节点的最大数目
const int NO_BLANK=-1001;//表示没有空格
const int TREE_DEPTH=3;//搜索树的最大深度,如果增加此值可以提高计算机的“智力”,
//但同时也需要增加MAX_NUM的值。
const int NIL=1001;//表示空
static int s_count;//用来表示当前分析的节点的下标

struct State//该结构表示棋盘的某个状态,也可看做搜索树中的一个节点
{
int QP[3][3];//棋盘格局
int e_fun;//当前状态的评估函数值
int child[9];//儿女节点的下标
int parent;//双亲节点的下标
int bestChild;//最优节点(评估函数值最大)的儿女节点下标
}States[MAX_NUM];//用来保存搜索树中状态节点的数组

void Init()//初始化函数,当前的棋盘格局总是保存在States[0]中
{
s_count=0;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
States[0].QP[i][j]=0;//将棋盘清空
States[0].parent=NIL;//初始节点没有双亲节点
}

void PrintQP()//打印当棋盘格局的函数
{
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
cout<<States[0].QP[i][j]<<'\t';
cout<<endl;
}
}

int IsWin(State s)//有人赢了吗?返回0表示没有人赢,返回-1表示人赢了,返回1表示计算机赢了
{
for(int i=0;i<3;i++)
{
if(s.QP[i][0]==1&&s.QP[i][1]==1&&s.QP[i][2]==1)return 1;
if(s.QP[i][0]==-1&&s.QP[i][1]==-1&&s.QP[i][2]==-1)return -1;
}
for(i=0;i<3;i++)
{
if(s.QP[0][i]==1&&s.QP[1][i]==1&&s.QP[2][i]==1)return 1;
if(s.QP[0][i]==-1&&s.QP[1][i]==-1&&s.QP[2][i]==-1)return -1;
}
if((s.QP[0][0]==1&&s.QP[1][1]==1&&s.QP[2][2]==1)||(s.QP[2][0]==1&&s.QP[1][1]==1&&s.QP[0][2]==1))return 1;
if((s.QP[0][0]==-1&&s.QP[1][1]==-1&&s.QP[2][2]==-1)||(s.QP[2][0]==-1&&s.QP[1][1]==-1&&s.QP[0][2]==-1))return -1;
return 0;
}

int e_fun(State s)//评估函数
{
bool flag=true;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)flag=false;
if(flag)return NO_BLANK;

if(IsWin(s)==-1)return -MAX_NUM;//如果计算机输了,返回最小值
if(IsWin(s)==1)return MAX_NUM;//如果计算机赢了,返回最大值
int count=0;//该变量用来表示评估函数的值

//将棋盘中的空格填满自己的棋子,既将棋盘数组中的0变为1
for(i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)tmpQP[i][j]=1;
else tmpQP[i][j]=s.QP[i][j];

//电脑一方
//计算每一行中有多少行的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
//计算每一列中有多少列的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
//斜行有没有连成3个的?
count+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;
count+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;

//将棋盘中的空格填满对方的棋子,既将棋盘数组中的0变为-1
for(i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)tmpQP[i][j]=-1;
else tmpQP[i][j]=s.QP[i][j];

//对方
//计算每一行中有多少行的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
//计算每一列中有多少列的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
//斜行有没有连成3个的?
count+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;
count+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;

return count;
}

//计算机通过该函数决定走哪一步,并对当前的棋局做出判断。
bool AutoDone()
{
cout<<"The QP now is:"<<endl;
PrintQP();

int
MAX_F=NO_BLANK,//保存对自己最有利的棋局(最大)的评估函数值
parent=-1,//以当前棋局为根生成搜索树,所以当前棋局节点无双亲节点
count,//用来计算当前生成的最后一个扩展节点的下标
i,//备用
tag;//标示每一层搜索树中最后一个节点的下标
bool
max_min=TREE_DEPTH%2,//标识取下一层评估函数的最大值还是最小值?
//max_min=1取下一层中的最大值,max_min=0取最小值
IsOK=false;//有没有找到下一步落子的位置?
s_count=0;//扩展生成的节点数初始值为0

if(IsWin(States[0])==-1)//如果人赢了
{
cout<<"Conguatulations! You Win! GAME OVER."<<endl;
return true;
}

for(int t=0;t<TREE_DEPTH;t++)//依次生成各层节点
{
count=s_count;//保存上一层节点生成的最大下标
for(int k=parent+1;k<=count;k++)//生成一层节点
{
int n_child=0;//该层节点的孩子节点数初始化为0
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(States[k].QP[i][j]==0)//如果在位置(i,j)可以放置一个棋子
{//则
s_count++;//生成一个节点,节点数(最大下标)数加1
for(int i1=0;i1<3;i1++)//该3×3循环将当前棋局复制到新节点对应的棋局结构中
for(int j1=0;j1<3;j1++)
States[s_count].QP[i1][j1]=States[k].QP[i1][j1];
States[s_count].QP[i][j]=t%2==0?1:-1;//根据是人下还是计算机下,在空位上落子
States[s_count].parent=k;//将父母节点的下标k赋给新生成的节点
States[k].child[n_child++]=s_count;//下标为k的父母节点有多了个子女

//如果下一步有一步期能让电脑赢,则停止扩展节点,转向结局打印语句
if(t==0&&e_fun(States[s_count])==MAX_NUM)
{
States[k].e_fun=MAX_NUM;
States[k].bestChild=s_count;//最好的下一步棋所在的节点的下标为s_count
goto L2;
}
}
}
parent=count;//将双亲节点设置为当前双亲节点的下一层节点
cout<<s_count<<endl;//打印生成节点的最大下标
}

tag=States[s_count].parent;//设置最底层标志,以便从下到上计算最大最小值以寻找最佳解路径。
int pos_x,pos_y;//保存计算机落子的位置
for(i=0;i<=s_count;i++)
{
if(i>tag)//保留叶节点的评估函数值
{
States[i].e_fun=e_fun(States[i]);
}
else//抹去非叶节点的评估函数值
States[i].e_fun=NIL;
}
while(!IsOK)//寻找最佳落子的循环
{
for(int i=s_count;i>tag;i--)
{
if(max_min)//取子女节点的最大值
{
if(States[States[i].parent].e_fun<States[i].e_fun||States[States[i].parent].e_fun==NIL)
{
States[States[i].parent].e_fun=States[i].e_fun;//设置父母节点的最大最小值
States[States[i].parent].bestChild=i;//设置父母节点的最佳子女的下标
}
}
else//取子女节点的最小值
{
if(States[States[i].parent].e_fun>States[i].e_fun||States[States[i].parent].e_fun==NIL)
{
States[States[i].parent].e_fun=States[i].e_fun;//设置父母节点的最大最小值
States[States[i].parent].bestChild=i;//设置父母节点的最佳子女的下标
}
}
}
s_count=tag;//将遍历的节点上移一层
max_min=!max_min;//如果该层都是MAX节点,则它的上一层都是MIN节点,反之亦然。
if(States[s_count].parent!=NIL)//如果当前遍历的层中不包含根节点,则tag标志设为上一层的最后一个节点的下标
tag=States[s_count].parent;
else
IsOK=true;//否则结束搜索
}

L2://取落子的位置,将x,y坐标保存在变量pos_x和pos_y中,并将根(当前)节点中的棋局设为最佳儿子节点的棋局
for(int x=0;x<3;x++)
{
for(int y=0;y<3;y++)
{
if(States[States[0].bestChild].QP[x][y]!=States[0].QP[x][y])
{
pos_x=x;
pos_y=y;
}
States[0].QP[x][y]=States[States[0].bestChild].QP[x][y];
}
}

MAX_F=States[0].e_fun;
//cout<<MAX_F<<endl;

cout<<"The computer put a qizi at: "<<pos_x+1<<','<<pos_y+1<<'\n'<<"The QP now is:"<<endl;
PrintQP();
if(MAX_F==MAX_NUM)//如果当前节点的评估函数为最大值,则计算机赢了
{
cout<<"The computer WIN! You LOSE! GAME OVER."<<endl;
return true;
}
if(MAX_F==NO_BLANK)//否则,如果棋盘时候没空可放了,则平局。
{
cout<<"DRAW GAME!"<<endl;
return true;
}
return false;
}

//用户通过此函数来输入落子的位置,
//比如,用户输入31,则表示用户在第3行第1列落子。
void UserInput()
{
int pos,x,y;
L1:cout<<"Please Input The Position where you put your qizi (xy): ";
cin>>pos;
x=pos/10,y=pos%10;
if(x>0&&x<4&&y>0&&y<4&&States[0].QP[x-1][y-1]==0)
States[0].QP[x-1][y-1]=-1;
else
{
cout<<"Input Error!";
goto L1;
}
}

//主程序
int main()
{
Init();
cout<<"The QiPan (QP) is: "<<endl;
PrintQP();
char IsFirst;
bool IsFinish;
cout<<"Do you want do first?(y/n)";
cin>>IsFirst;
do{
if(IsFirst=='y')
{
UserInput();
IsFinish=AutoDone();
}else{
IsFinish=AutoDone();
if(!IsFinish)UserInput();
}
}while(!IsFinish);

return 0;
}

(全文完)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值