自动扫雷算法的具体实现

标签:
自动扫雷 |
自动扫雷程序主要由三个模块构成:欢迎模块WELCOME()、智能模块ANALYZE()和 控制模块GAME_CONTRO()。三个模块通过一些全局变量交互一些信息。下面是一些重要的全局变量:
int
Opened_block;
int
Height,Width;
int
Mine_num;
char **mine_board; //这个数组记录实际的布雷情况,这个数组中元素为* 的地
//方表示该位置是雷,为数字的地方指示该位置周围有多少雷,这个数组并不显示给用户
int
**score_board;
char
**view_board;
下面,就每一个模块的功能和其中的主要函数进行说明
一、WELCOME()模块
函数原型:void WELCOME();
这个模块的主要功能是显示欢迎界面,由用户输入棋盘大小,完成布雷等初始化工作。
主要的函数是随机设置地雷位置的函数void set_mine(char**&board)
void set_mine(char**&board)的功能是在mine_board 上随机设置N个地雷(N=Mine_num),将mine_board有雷的相应位置标识为‘*’,然后计算非雷的格子周围有多少雷,并用数字标识在mine_board的相应位置。
(1).设置地雷
产生两个个随机数,映射到一个横坐标和一个纵坐标,如果该位置已经被标记为雷,则重复上述操作,否则,将该位置标记为‘*’。以上步骤循环多遍,直到设置的雷数达到Mine_num;
函数具体实现如下:
for(count=0;count<Mine_num;count++)
{
do{
i=rand()%Height;
j=rand()%Width;
}
while(board[i][j]=='*');
board[i][j]='*';
}
(2)计算非雷格子相邻的雷的数目
遍历mine_board中所有格子,当遇到‘*’的格子时(即遇到标记为雷的格子时),将其左上、上方、右上、左边、右边、左下方、下方、右下方所有不是雷的格子的数值都+1
二、GAME_CONTROL()模块
函数原型:
void GAME_CONTROL(int &lin_current,int& col_current,int lin_des,int col_des,char cmd)
这个模块负责对ANALYZE()模块给出的命令予以实现。这个模块主要的
工作是实现光标的移动控制,以及根据点击的不同的位置做出反应。
(1)光标的移动控制
为了达到比较友好的程序界面,本程序用‘?’来表示当前光标的位置。
参数lin_current 和 col_current 是当前光标的行号和列号,参数 lin_des 和 col_des是目标位置的行号和列号。
程序首先比较当前光标位置和目标光标位置是否相同,如果相同,则说明光标到达点击位置,直接向 命令处理模块 给出 点击命令‘n’,否则比较光标现在位置和目标位置的差距,分别向命令处理模块给出向上移动的‘w’命令,向下的‘s’, 向左移动的 ‘a’命令,向右的‘d’命令。然后更新光标当前 ,不断循环直至当前光标位置和目标位置重合。
具体实现是:
if(lin_current!=lin_des)
{
if(lin_current<lin_des)
{
lin_current++;
}
else
{
lin_current--;
}
}
else
{
if(col_current!=col_des)
{
if(col_current<col_des)
{
col_current++; temp_cmd='d';
}
else
{
col_current--;
}
}
else
{
temp_cmd=cmd;
}
}
(2)命令处理模块
命令处理模块对命令进行处理并将结果反映到view_board上显示出来。
1如果遇到方向命令,如w,a,s,d,程序进行下面的操作(以遇到w命令为例):
W命令使光标向上移动,则程序因完成这些工作:
0.判断光标是否能够向上移动,如果已经到达顶端,则下面的步骤跳过不做
1.光标向上移动后,恢复光标所在位置的内容
2.由于光标向上移动会覆盖棋盘上面一格的内容,因此要将上面一格的内容用一个变量保存起来
3.将上面一格的内容赋值为‘?’
4.将屏幕清空
5.输出改变之后view_board 的内容
具体实现是这样的:
case 'w':
{
if(0==i0)
{}
else
{
view_board[i0][j0]=temp;
temp=view_board[i0-1][j0];
view_board[--i0][j0]='?';
}
system("cls");
print_board(view_board);
}
break;
2如果遇到点击命令 ‘n’,程序进行下面的操作:
首先根据mine_board判断点击位置的内容,如果点击位置为非0数字,则在view_board的相应位置显示该数字,将Open_block的值减1;如果点击位置为0,则将改格子进行展开;如果点击位置为‘*’,说明触雷,则显示触雷信息。
具体实现是:
其中,将为0的格子展开比较重要。当点击到的点为0时,应该将与其相邻的所有0也展开,然后对其相邻的0也执行展开操作,以此递归,直到展开的点非0为止。本程序中,运用的是深度优先搜索的方法,具体实现就是下面这样:
void merge(int i0,int
j0)
{
char value;
view_board[i0][j0]=mine_board[i0][j0];
Opened_block--;
if(view_board[i0][j0]>='1'&&view_board[i0][j0]<='8')
return;
if(get_value(view_board,i0-1,j0,value)&&'.'==value)//展开上方格子
merge(i0-1,j0);
if(get_value(view_board,i0+1,j0,value)&&'.'==value)//展开下方格子
merge(i0+1,j0);
if(get_value(view_board,i0,j0-1,value)&&'.'==value)//展开左边格子
merge(i0,j0-1);
if(get_value(view_board,i0,j0+1,value)&&'.'==value)//展开右边格子
merge(i0,j0+1);
if(get_value(view_board,i0-1,j0-1,value)&&'.'==value)//展开左上格子
merge(i0-1,j0-1);
if(get_value(view_board,i0-1,j0+1,value)&&'.'==value)//展开右上格子
merge(i0-1,j0+1);
if(get_value(view_board,i0+1,j0-1,value)&&'.'==value)//展开左下格子
merge(i0+1,j0-1);
if(get_value(view_board,i0+1,j0+1,value)&&'.'==value)//展开右下格子
merge(i0+1,j0+1);
}
其中bool get_value(char** &board,int i,int j,char &value)函数用于取board[i][j]的值并赋值给value并返回true,如果改点不在数组内,如i<0或j<0,则返回false
三、 ANALYZE()
函数原型:void ANALYZE(int lin_current, int
col_current, int
&lin_des,
这个模块要完成的主要工作是通过对当前棋盘情况的分析,确定下一个点击位置。方法是通过3次的循环遍历(这是我觉得程序效率比较低的地方),计算出每个格子的分值(分值=每个格子有雷的概率x 10000),记录在二维数组
int **score_board 中。分值最低的格子的位置即为下一个点击位置。如果分值最低的格子有多个,则距离当前光标最近的那个即为下一个点击位置。
(1)
将棋盘中,将肯定有雷的格子的分值标记为10000
具体的方法是遍历至view_board中为数字的格子时,如果它周围未被点击的格子数等于该格子的数值,则说明这些空格上肯定是雷。将该格子的分值标记为-1(说明这是一个无法点击的点),再将它周围的空格在score_board的对应格子分值标记为10000。
具体的实现如下:
for(lin=0;lin<Height;lin++)
for(col=0;col<Width;col++)
if(get_value(view_board,lin,col,temp)&&temp>'0'&&temp<'9')
{
neighbor(view_board,lin,col,count_empty,count_mine,count_notmine);
//上面的函数功能为计算view_board[lin][col]这个格子周围有多少
//空格(count_empty),多少已经标记过的雷(count_mine),多少
//已经标记过肯定不是雷的格子(count_notmine)
if(count_empty==(view_board[lin][col]-'0'))//空格数等于该格子数值
{
score_board[lin][col]=-1;//将这个格子赋值-1
//然后将其周围所有的空格分值赋值为10000
if(get_value(view_board,lin-1,col-1,score)&&score=='.')//左上
{score_board[lin-1][col-1]=10000;}
if(get_value(view_board,lin-1,col,score)&&score=='.')//上
{score_board[lin-1][col]=10000;}
if(get_value(view_board,lin-1,col+1,score)&&score=='.')//右上
{score_board[lin-1][col+1]=10000;}
if(get_value(view_board,lin,col-1,score)&&score=='.')//左
{score_board[lin][col-1]=10000;}
if(get_value(view_board,lin,col+1,score)&&score=='.')//右
{score_board[lin][col+1]=10000;}
if(get_value(view_board,lin+1,col-1,score)&&score=='.')//左下
{score_board[lin+1][col-1]=10000;}
if(get_value(view_board,lin+1,col,score)&&score=='.')//下
{score_board[lin+1][col]=10000;}
if(get_value(view_board,lin+1,col+1,score)&&score=='.')//右下
{score_board[lin+1][col+1]=10000;}
}
}
经过这样的遍历就将肯定是雷的格子标记出来了
http://s7/middle/67fc4f6a492c6748fa7a6&690
用红色记号标记的格子就是数字相邻格,之所以要首先计算它们的分值,是因为只有在确定数字相邻格的数量和棋盘中已打开的格子的数量之后,才能够计算棋盘中剩下的空白格子中有雷的概率。
则很容易证明,对于这个格子,它有雷的概率
P=
例如,下图用黑色标记的格子
http://s5/middle/67fc4f6a492c67dfcf6a4&690
其有雷的概率为P=1-(1-(3-1)/(4-1))×(1-(1/3))=7/9
其分值score=P×10000=7777
具体的实现过程是:
for(lin=0;lin<Height;lin++)
{
}
其中set_score()函数即是通过上述的方法计算每一点的分值。
经过上面的循环,就可以确定那些不与任何数字相邻的空格有雷的概率为
P=(Mine_num-signed_mine)/(empty_block)
分值为score_empty= (Mine_num-signed_mine)/(empty_block)*10000
(3)
第三遍循环要做的工作是将 不与任何数字相邻的空格(以下叙述中,简
称空格) 的值赋值为刚才计算得到的score_empty。 并找出所有分值最小的格子,将它们放入一个 格子数组min_blocks[ ] 中。
实现过程如下:
temp_score=score_empty; //将空格的分值赋给一个临时变量
score_empty=float(Mine_num-signed_mine)/(empty_block)*10000;
//重新计算空格的分值
for(lin=0;lin<Height;lin++)
{
if(score_board[lin][col]>=0)
}
再得到所有分值最小的格子后,首先判断最小分值min的大小。
如果min=0,说明这个数组中所有的格子都可确定不是雷,要做的事就是从中选出距离当前光标位置最近的格子,这个格子就是下一个要点击的格子。
如果min>0,说明这个数组中的格子仍然有一定的概率为雷,如果按照上面的方法选择距离当前光标位置最近的格子,则可能出现的情况是导致光标在一块区域密集地点击,由于雷是在雷盘上随机均匀分布的,这样会导致触雷的可能性增大。所以,如果在这种情况下,应当从数组中随机选取一个格子作为下一个点击的格子。
程序每个模块的介绍基本就是这样。