文章目录
- 前言
- 一、思路
- 二、代码实现
-
- 1.头文件
- 2.全局变量和宏定义
- 3.main()
-
- 初始化
- 核心
- 4.功能函数
-
- 生成雷区new_mine()
- 游戏主要操作operate()
- 显示雷区图像display()
- 检查赢check_win()
- 三、效果展示
- 总结
前言
经过一整子的瞎折腾,我终于完成了ncurses版的扫雷。这让我进一步认识到了ncurses库的强大。这里主要的新知识点是"鼠标事件的获取和处理",可以参考这篇文章,虽然排版凑合但还是挺实用的。相比写情诗,扫雷的代码量还是比较大的,我的版本前后有100多行的样子。我用思维导图整理了我的思路,一个功能基本就一个函数。扩展功能我这里完成了前两个,但已经够用了,扫雷的基本操作测试下来很流畅。
一、思路
二、代码实现
1.头文件
#include <stdlib.h> //要用到srand()和rand()生成雷
#include <time.h> //要用到clock()生成随机数种子
#include <ncurses.h> //主角不用解释
2.全局变量和宏定义
#define MINENUM 10 //雷的总数
#define row 9 //雷区的行数
#define line 9 //雷区的列数
int a[row][line]; //0:周围没有雷 -1:此处有雷 1~8:周围雷的数量
int f[row][line]; //0:未探索 1:已探索
int win; //用于记录输赢的信息
3.main()
初始化
initscr(); //初始化屏幕
curs_set(0); //不显示光标
noecho(); //禁止输入的字符出现在屏幕上
keypad(stdscr, TRUE); //因为缺少这行一直出错
mousemask(BUTTON1_RELEASED,0);//激活鼠标事件左键释放,使得getch()能获取鼠标事件
MEVENT event; //创建事件变量
核心
while(1){ //这个循环使游戏可以玩很多局
new_mine(MINENUM); //新建雷区
display(); //显示雷区(刚开始全是不可见的)
while(1){ //这个循环使游戏的操作可以持续
switch (getch()){ //获取操作
case KEY_MOUSE: //如果是鼠标事件,进行游戏操作
win=operate(event);//如果踩雷operate()会返回-2
break;
case 'q': win=-2; //如果输入的是字符'q'则退出本局游戏
}
display();
if(win==-2){ //输掉游戏(包括中途退出)
printw("You lose!"); break;
}
if(check_win()){ //检查是否赢得游戏
printw("You win!"); break;
}
}
if(getch()=='q') break; //如果再次输入字符'q'则退出游戏
}
endwin(); //退出curses模式
4.功能函数
生成雷区new_mine()
int add_mine(int r,int l){ //用于累加雷区的数字
if(r>=0&&r<row&&l>=0&&l<line&&a[r][l]!=-1)//排除在界外的情况,而且雷上面不用进行处理
a[r][l]++;
}
int new_mine(int num){
int n=0,r=0,l=0;
win=0; //千万不要忘记初始化,下一局开始时win要再次初始化为0
for(r=0;r<row;r++){
for(l=0;l<line;l++){
a[r][l]=0; //雷区的状态也要初始化
f[r][l]=0; //雷区的探索情况也要初始化
}
} //生成雷
srand(clock()); //用clock()生成随机种子
while(n<num){
r=rand()%row; //用随机数获取行数
l=rand()%line; //用随机数获取列数
if(a[r][l]==0){ //确保即使有重复位置的雷也能生成所需的数量的雷
a[r][l]=-1; //放置雷
n++;
}
}
for(r=0;r<row;r++) //布置雷区数字
for(l=0;l<line;l++)
if(a[r][l]==-1){ //在雷的周围累加数字
add_mine(r-1,l-1);
add_mine(r-1,l);
add_mine(r-1,l+1);
add_mine(r,l-1);
add_mine(r,l+1);
add_mine(r+1,l-1);
add_mine(r+1,l);
add_mine(r+1,l+1);
}
}
游戏主要操作operate()
int show_mine(int r,int l){ //用于显示雷区的状态
if(r>=0&&r<row&&l>=0&&l<line&&a[r][l]!=-1)//排除在界外的情况,而且雷不用显示
f[r][l]=1;
}
int operate(MEVENT mouse){ //游戏的主要操作
static int r=0,l=0;
getmouse(&mouse); //翻译获取的鼠标事件
mouse_trafo(&mouse.y,&mouse.x,1); //将获取的鼠标的坐标转换为对应的屏幕坐标
if (!wenclose(stdscr,mouse.y, mouse.x))
return -1; //如果坐标不在屏幕内,直接返回
if(mouse.bstate==BUTTON1_RELEASED){ //当鼠标事件为左键释放时
l=mouse.x/3; //将鼠标坐标转化为对应的雷区坐标
r=mouse.y/2;
if(a[r][l]==-1){ //该坐标上有雷,踩到雷了
for(int i=0;i<=row;i++)
for(int j=0;j<=line;j++)
f[i][j]=1; //全部雷区的状态都设为可见
return -2; //返回-2
}
show_mine(r,l); //没踩到雷,就设置该坐标的雷区状态为可见
if(a[r][l]==0){ //如果该坐标周围没有雷,则设置周围的雷区状态为可见
show_mine(r-1,l-1);
show_mine(r-1,l);
show_mine(r-1,l+1);
show_mine(r,l-1);
show_mine(r,l+1);
show_mine(r+1,l-1);
show_mine(r+1,l);
show_mine(r+1,l+1);
}
}
}
显示雷区图像display()
int display(){ //用于显示雷区
int r=0,l=0;
clear(); //清屏
for(r=0;r<row;r++){ //遍历整个雷区
for(l=0;l<line;l++){
if(f[r][l]==1) //如果该雷区坐标状态为可见
if(a[r][l]==-1) //如果该坐标是雷,打印雷
mvaddch(r*2+1,l*3+1,'*');
else //不是雷就打印数字
mvaddch(r*2+1,l*3+1,'0'+a[r][l]);
}
}
//绘制网格,细节就不解释了
for(r=0;r<row;r++){
for(l=0;l<line;l++){
mvaddch(r*2,l*3+1,'-');
mvaddch(r*2,l*3+2,'-');
mvaddch(r*2,l*3,'|');
mvaddch(r*2+1,l*3,'|');
mvaddch(row*2,l*3,'|');
mvaddch(row*2,l*3+1,'-');
mvaddch(row*2,l*3+2,'-');
}
mvaddch(r*2,line*3,'|');
mvaddch(r*2+1,line*3,'|');
}
mvaddch(row*2,line*3,'|');
refresh(); //将缓冲区的所有字符都打印到屏幕上
}
检查赢check_win()
int check_win(void){ //看看游戏有没有赢
int count=0; //用于统计未探索的雷区的数量
for(int i=0;i<row;i++)
for(int j=0;j<line;j++)
count+=1-f[i][j]; //因为未探索是0,已探索是1,只有未探索才+1
return count==MINENUM; //剩下的雷区的数量就是雷的总数,说明雷全找出来了
}
三、效果展示
如果中途按q
退出,会判定为输
我输的时候
当然也有赢的时候
总结
通过这次用ncurses写扫雷的尝试,除了加深对c语言和ncurses库的理解和熟练度,更让我了解到思路的重要性。c语言和ncurses库只是工具,如果思路不清晰明朗就很难完成目标,反之是总有办法实现的。所以在敲代码之前,尤其是做复杂的任务的时候,一定要先做需求分析整理思路,将需要的数据和功能都剖析出来,这样对代码的实现非常有利。养成这种良好的习惯对于提高效率和代码品质是十分有利的。