扫雷是一个男女老少皆宜的一个小游戏,读大学的时候见同学玩的多,自己主要以暗黑2,泡泡堂,街霸为主。那个时候觉得扫雷是个很神奇的东西,鼠标左击两下居然可以扫出一片,当时连模拟扫雷的想法都没有。
在我的大学四年,从教学资源上确实乏善可陈,作为医学院校的第一届工科专业,我搜索了整个三层的图书馆,只找到一本vb数据库编程的书。说好的理科学位,最后居然拿到管理学位,对于满满的计算机课程,我只能黯然神伤。记得当初学习计算机的时候,好像每学会一种技术,甚至一个API,都觉得自己技能升级了一般,内心里都是练出独孤九剑,唯我独尊的想法,直到多年后,才慢慢认识到一个人无论怎么学都有限,唯有一个开放的思维,才能运用各家所长,为我所用。门户之见,闭门造车,害人不浅。电影四大名捕完结了,有网友说,最遗憾的是,再也见不到黄秋生吃火锅了,对我而言,最遗憾的莫过于再也听不到里面能让我感动的台词。记得名捕2开头,诸葛正我和捕神的对话:"就像这杯水,你拿着它不放,能装的水一辈子就这么多了 ,放弃了,它可以装的水就不可估量了”“无论何门何派 ,我们修的都是心 ,都是一条路 ,往上能到那里,就看我们自己了”。有人会说我入戏太深,但换角度想想,你是不是刚好就拿着一杯满满的水呢。
扯远了,现在说说扫雷的整体思路,它大致分为几个部分,初始化,生成地雷的信息,计算雷周围的信息,鼠标左键单击,双击和右键单击事件。
地雷的初始化的关键是随机生成地雷的位置,程序随机生成雷的位置,将这几个位置标志为地雷,然后对整个雷区进行扫描,针对每个点统计其周围八个点的地雷数目,然后设置该点的数字。
先解释一下关于雷区块的计算机,为了简便,我用一个字节去表示一个块的信息,低四位(b0-b4)表示雷的基本信息(0-8)表示块周围的雷的个数,按上图所示,八领域最多有8个雷,9表示地雷。除了这些基本信息之外,块还有是否被鼠标点开过,是否被标记过等信息。
这里用D7和D6分别表示,对于D7而言0表示未被点开,1表示已经被点开过,同理,对于D6而言,0表示未标记,1表示已经被标记
雷的左键事件需要一个递归的过程,当点中一个周围没有地雷的块或者相应的周边地雷已经全部被标记的,需要对其八领域做递归搜索,这是扫雷有时候能扫出一片的原理所在。具体参见代码
以下为初始化效果
全部未点开的状态
全部点开的状态
代码测试
以下为全部代码信息,跟windows的那套算法不完全一样,为了演示方便,做了一些简化。
#include<stdlib.h>
#include <stdio.h>
#include <string.h>
#include<time.h>
#define UNFOLD_MASK 0x80
#define FOLD_MASK 0x7F
#define UNFOLD(X) X = (X|UNFOLD_MASK)
#define FOLD(X) X = (X&FOLD_MASK);MINER_UNMASK(X);
#define IS_UNFOLD(X) (X&UNFOLD_MASK)
#define M_MASK 0x40
#define M_UNMASK 0xBF
#define MINER_MASK(X) X = (X|M_MASK)
#define MINER_UNMASK(X) X = (X&M_UNMASK)
#define IS_M_MASK(X) (X&M_MASK)
#define FOLD_EMPTY 0
#define MINER 9
#define BLOCK_SIZE 10 //雷区大小
#define MINE_COUNT 10 //地雷的数量
#define IS_OUT_BOUND(X) (X<0||X>=BLOCK_SIZE)
int blocks[BLOCK_SIZE*BLOCK_SIZE] = {0};
char *block_empty = "■";
char *block_encodings[12] = {
"□","1 ","2 ","3 ",
"4 ","5 ","6 ","7 ",
"8 ","¤"
};
/*force==1时,强制输出信息,调试用*/
void print_one_block(int block_data,int force)
{
if(force) UNFOLD(block_data);
if(IS_M_MASK(block_data)&&(!IS_UNFOLD(block_data)))
{
printf("★");
}
else if(!IS_UNFOLD(block_data)) //未扫块
{
printf("%s",block_empty);
}
else
{
//block_data =
block_data &= 0x0F;
printf("%s",block_encodings[block_data]);
}
}
void srand_time()
{
srand((unsigned)time(NULL));
}
/*对点x,y所在块的八个相邻块进行检测目标数量,
这里可以是地雷数,也可以标记雷数*/
int cal_mine_count(int x,int y,int *bs,int target,int target_mask)
{
int idx = x*BLOCK_SIZE+y;
int i,j,count=0;
int k =x,z = y;
bs[idx] = bs[idx]&0x0F;
if(bs[idx]==MINER) return bs[idx];//该点为地雷,直接返回
for(i=-1;i<=1;i++)
for(j=-1;j<=1;j++)
{
x =k+i;
y =z+j;
if(IS_OUT_BOUND(x)||IS_OUT_BOUND(y)) continue;//超过上下限返回
idx = x*BLOCK_SIZE+y;
if((bs[idx]&target_mask)==target) count++;
}
return count;//返回目标个数
}
void init_mine(int *bs)
{
int i,j,x;
for(i=0;i<MINE_COUNT;i++)
{
x = rand()%(BLOCK_SIZE*BLOCK_SIZE);
if(bs[x]==FOLD_EMPTY)//该点未被设置过
{bs[x] = MINER;}
else //如果已经被设置,放弃这次操作
{i--;} //效率不高,但是相对简便,
}
for(i=0;i<BLOCK_SIZE;i++)
for(j=0;j<BLOCK_SIZE;j++)
{
x= i*BLOCK_SIZE+j;
bs[x] = cal_mine_count(i,j,bs,MINER,0x0F);
}
}
void print_blocks(int *bs)
{
int i,j;
for(j=0;j<BLOCK_SIZE;j++) printf(" %d",j);
printf("\n");
for(i=0;i<BLOCK_SIZE;i++)
{
printf("%d ",i);
for(j=0;j<BLOCK_SIZE;j++)
{
print_one_block(bs[i*BLOCK_SIZE+j],0);
}
printf("\n");
}
}
void check_blank_point(int x,int y,int *bs)
{
int idx = x*BLOCK_SIZE+y;
int i,j,count=0;
int k =x,z = y;
//if(IS_UNFOLD(bs[idx])) return;//已经扫描过,返回
UNFOLD(bs[idx]); //标记已经扫描
if((bs[idx]&0x0F)!=0) return;
for(i=-1;i<=1;i++)
for(j=-1;j<=1;j++)
{
x =k+i;
y =z+j;
if(IS_OUT_BOUND(x)||IS_OUT_BOUND(y)) continue;
idx = x*BLOCK_SIZE+y;
if(bs[idx]==MINER) continue;
if(IS_UNFOLD(bs[idx])) continue;
check_blank_point(x,y,bs);
}
// return count;//返回目标个数
}
/*模拟右键点击点x,y事件*/
void right_click(int x,int y,int *bs)
{
int idx = x*BLOCK_SIZE+y;
if(IS_UNFOLD(bs[idx]))
{
printf("位置(%d,%d)已经被扫描过,无需标记\n",x,y);
return;
}
if(IS_M_MASK(bs[idx]))
{
MINER_UNMASK(bs[idx]);
printf("位置(%d,%d)被取消标记\n",x,y);
}
else
{
MINER_MASK(bs[idx]);
printf("位置(%d,%d)已经被标记\n",x,y);
}
}
void left_click(int x,int y,int *bs)
{
int idx = x*BLOCK_SIZE+y;
int i;
if(IS_UNFOLD(bs[idx])) return; //已经扫描部分,跳过
UNFOLD(bs[idx]);
if((bs[idx]&0x0F)==MINER)
{
printf("踩到雷(%d,%d)了,游戏结束\n",x,y);
return;
}
bs[idx]&=0x0F;
if((bs[idx]&0x0F)==0)//递归扫描所有空点
{
check_blank_point(x,y,bs);//对于周边地雷数为0的点进行递归扫描,
//打开周边非地雷的快,若有同样的点,
//对该点进行同样的操作
}
else
{
i = cal_mine_count(x,y,bs,M_MASK,M_MASK);
if(i==(bs[idx]&0x0F))
{
check_blank_point(x,y,bs);
}
}
}
void main()
{
init_mine(blocks);
right_click(0,0,blocks);
right_click(0,0,blocks);
left_click(0,9,blocks);
left_click(0,0,blocks);
print_blocks(blocks);
}
有的朋友很多会发现用上面的代码生成的雷区信息都是一样的,原因是出在rand调用的随机序列没有发生变化,这里需要在init_mine函数前,加入srand_time函数以生成新的伪随机序列,为了说明问题,我需要保持雷区信息不变,就没有加入该函数。
我个人曾经调试过windows自带的扫雷程序,它关于雷区的表示方法不大一样。后续会说说怎么编写一个扫雷的作弊器,功能是改写雷区的信息,达到快速扫雷的目的。这里牵涉到动态调试和反汇编的一些基本技能,所以,高手笑过,新手飘过。
下一节预告 FLASH 提灯过桥游戏
游戏地址参考链接 http://www.freegame.tw/flash.asp?id=10118