C语言实现的扫雷 2.0 版
对前几天写的代码进行了亿点点更改,写成了现在这个运行效率更高,规则更贴近原版的无页面扫雷。
首先要明确Windows7扫雷的一些特色规则
- 第一次点击的时候,一定能触发连锁打开
- 第一次点击的时候,不会点到雷
- 点到数字为0的方块时,才可以连锁打开
特色规则实现思路or 第一次打开方块的流程
- 定义并填充一个棋盘
- 打印棋盘
- 响应用户第一次打开,获取安全点坐标
- 给棋盘布雷,避开安全点
- 给棋盘上的格子设置数字
- 调用函数打开用户原来希望打开的方块坐标
程序设计
这次我把代码分成了三个文件,分别是
SweepMine.h
SweepMine.cpp
main.cpp
也就是说,把这个游戏设计成了一个接口,需要用的时候再用main.cpp文件调用。
- 用一个square结构体来表示每个格子信息
- 用二维数组land[][]来表示棋盘
- 既然是接口嘛,我还打算搞一个user_land[][]来表示目前的棋盘,方便后来的程序了解到棋盘的情况
SweepMine.h 文件
#ifndef __SWEEPMINE_H__
#define __SWEEPMINE_H__
#define MAX_LANDMINE 99 //雷数量
#define MAX_WIDTH 16 //棋盘宽度(高度)
#define MAX_LENGTH 30 //棋盘长度
#define AREA MAX_WIDTH * MAX_LENGTH
#define REL_WIDTH MAX_WIDTH + 2
#define REL_LENGTH MAX_LENGTH + 2
#define OVER -20220202 //游戏结束的标志
#define MARKED -1 //方块标记的标志
#define STILLUNK -2 //方块未知的标志
#define OPENED -3 //方块被打开的标志
#define isSquAround(x,y,i,j) ((i + 1 == x || i - 1 == x || i == x) && (j + 1 == y || j - 1 == y || j == y))
#define Sweep_fflush() {char c;do{scanf("%c",&c);}while(c !='\n');}
typedef struct square_struct {
int number;
bool landmine; //0无雷,1有雷
short now;
} square;
int game_start (void);
int initialize (int,int);
int set_squ (void);
int fill (void);
int show_land (void);
int player_action (void);
int open_squ (int,int);
int mark_squ (int,int);
int game_over (void);
int game_win (void);
#endif
其中isSquAround(x,y,i,j)
宏用来判断(i,j)在不在(x,y)的周围
另外一个宏Sweep_fflush()
用来刷新输入缓冲区,取这个名字是为了避免和fflush()
重名
你可能会问,都考虑到重名了,那为什么我不直接用fflush(stdio)
呢?
这是因为ANSI C没规定它清理缓冲区,只是某些编译器加上了这个功能。虽然可能没什么人会用我的代码,但是写的时候还是要注意一下。
之前定义表示棋盘的二维数组的时候,都是直接按照棋盘的长宽来定义的。要访问某个格子周围的时候,就比较麻烦,很容易发生数组越界。所以这一次的数组的长宽等于实际棋盘长宽+2也就是REL_WIDTH
与REL_LENGTH
当访问到边缘方块的周围的时候,就不需要判断坐标的正误了。
SweeoMine.cpp里一些变量的定义
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include "SweepMine.h"
square land[REL_WIDTH][REL_LENGTH]; //实际雷区
int user_land[REL_WIDTH][REL_LENGTH]; //用户雷区
int opennum = 0; //打开格子数量
bool isGameFirst = false; //第一次打开操作的标记
填充棋盘的函数
int fill(void){
/*填充方块*/
for (int i = 0; i <= REL_WIDTH - 1; ++i) {
for (int j = 0; j <= REL_LENGTH - 1; ++j) {
land[i][j] = (square) {0, 0, STILLUNK};
user_land[i][j] = STILLUNK;
}
}
return 0;
}
打印棋盘就比较“个性”,每个人都有自己的独特想法可以打印出来,接下来的代码只展示主要的棋盘部分
int show_land(void) {
/*打印装饰与坐标*/
...╮(╯3╰)╭...
/*打印棋盘本身*/
for (int i = 1; i <= MAX_WIDTH; ++i) {
for (int j = 1; j <= MAX_LENGTH; ++j)
if(land[i][j].now == MARKED)
printf("回 "); //标记格子样式
else if(land[i][j].now == STILLUNK)
printf("口 "); //未知格子样式
else
printf("%d ",land[i][j].number);
printf("\n\n");
}
return 0;
}
我用中文的“口”字来代表未知的格子,“回”字代表标记的格子,一打开的格子直接打印该格子的数字就好啦
接下来的响应用户页面,也就是交互页面的设计也很“个性”,每个人都有自己的想法,就不展示了。只要你的程序能知道用户想做什么就可以
打开格子的函数如下,传入打开格子的坐标
int open_squ(int x, int y) {
if(isGameFirst){ //若是第一次点击,则初始化棋盘
bray(x,y);//布雷
set_squ();//设置每个方块的数字
isGameFirst = false; //从这以后,就不是第一次打开了
open_squ(x,y); //最后还是要用正常流程打开这个方块
} else {
if (land[x][y].landmine == 1) {
opennum = OVER; //若打开的格子是雷,就把opennum设置为常量OVER表示游戏结束
} else {
/*连锁打开规则:点到0后递归打开周围所有方块*/
land[x][y].now = OPENED;
user_land[x][y] = land[x][y].number;
++opennum; // 打开方块数量++
if (land[x][y].number == 0){
if(y + 1 <= MAX_LENGTH){
if(land[x][y + 1].now == STILLUNK){
open_squ(x, y + 1);
}
if(x - 1 >= 1){
if(land[x - 1][y + 1].now == STILLUNK)
open_squ(x - 1, y + 1);
}
if(x + 1 <= MAX_WIDTH){
if(land[x + 1][y + 1].now == STILLUNK)
open_squ(x + 1, y + 1);
}
}
if(y - 1 >= 1){
if(land[x][y - 1].now == STILLUNK)
open_squ(x, y - 1);
if(x - 1 >= 1){
if(land[x - 1][y - 1].now == STILLUNK)
open_squ(x - 1, y - 1);
}
if(x + 1 <= MAX_WIDTH){
if(land[x + 1][y - 1].now == STILLUNK)
open_squ(x + 1, y - 1);
}
}
if(x - 1 >= 1){
if(land[x - 1][y].now == STILLUNK)
open_squ(x - 1, y);
}
if(x + 1 <= MAX_WIDTH){
if(land[x + 1][y].now == STILLUNK)
open_squ(x + 1, y);
}
}
}
}
return 0;
}
棋盘的布雷,就是随机取到一个坐标,若该坐标上没有雷(不然就有可能重复布雷!),而且这个坐标不在安全点周围的话,就成功布下一个雷。其实雷格子的数字是什么都无所谓,但是为了当时调试方便,就设置了初始值为-10。
int bray(int x,int y) {
//给棋盘布雷 接收两个坐标 此坐标对应的格子叫做"安全点" 安全点及其周围不会有雷
//安全点上数字为0
srand((unsigned)time(0)); //重置随机数种子
for (int n = 1;n <= MAX_LANDMINE;) {
int i = rand() % (MAX_WIDTH) + 1; //随机生成坐标
int j = rand() % (MAX_LENGTH) + 1;
if(!isSquAround(x,y,i,j)){
//雷如果不在安全点周围,就成功埋下雷
if (land[i][j].landmine == 0) {
land[i][j] = (square){-10, 1, STILLUNK};
++n; //只有成功布雷才会推动代码运行,保证雷数量
}
}
}
return 0;
}
给方块设置数字:我们知道,每个格子的landmine属性不是0就是1,是0的时候不是雷,是1的时候是雷。若我们要了解某个格子周围的雷数,直接把周围方块的landmine加起来就可以了。
int set_squ(void){
/*设置方块数字*/
for (int i = 1; i <= MAX_WIDTH; ++i) {
for (int j = 1; j <= MAX_LENGTH; ++j) {
land[i][j].number += \
land[i - 1][j ].landmine + \
land[i - 1][j - 1].landmine + \
land[i - 1][j + 1].landmine + \
land[i + 1][j ].landmine + \
land[i + 1][j - 1].landmine + \
land[i + 1][j + 1].landmine + \
land[i ][j - 1].landmine + \
land[i ][j + 1].landmine; //有雷的landmine值为1,反之为0
}
}
return 0;
}
标记方块,只需要把方块的now属性变一下就行~
int mark_squ(int x,int y){
//改变方块的now属性来标记方块
land[x][y].now = land[x][y].now == MARKED? STILLUNK : MARKED;
user_land[x][y] = MARKED;
return 0;
}
其他的比较简单的东西,比如胜利时的展示,失败时的展示,在这里就不挨个展示了。
全部代码
SweepMine.h
#ifndef __SWEEPMINE_H__
#define __SWEEPMINE_H__
#define MAX_LANDMINE 99 //雷数量
#define MAX_WIDTH 16 //棋盘宽度(高度)
#define MAX_LENGTH 30 //棋盘长度
#define AREA MAX_WIDTH * MAX_LENGTH
#define REL_WIDTH MAX_WIDTH + 2
#define REL_LENGTH MAX_LENGTH + 2
#define OVER -20220202
#define MARKED -1
#define STILLUNK -2
#define OPENED -3
#define isSquAround(x,y,i,j) ((i + 1 == x || i - 1 == x || i == x) && (j + 1 == y || j - 1 == y || j == y))
#define Sweep_fflush() {char c;do{scanf("%c",&c);}while(c !='\n');}
typedef struct square_struct {
int number;
bool landmine; //0无雷,1有雷
short now;
} square;
int game_start (void);
int initialize (int,int);
int set_squ (void);
int fill (void);
int show_land (void);
int player_action (void);
int open_squ (int,int);
int mark_squ (int,int);
int game_over (void);
int game_win (void);
#endif
SweepMine.cpp
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include "SweepMine.h"
square land[REL_WIDTH][REL_LENGTH]; //实际雷区
int user_land[REL_WIDTH][REL_LENGTH]; //用户雷区
int opennum = 0; //打开格子数
bool isGameFirst = false;
int game_start(void){
//一盘游戏的开始
MAX_LENGTH;MAX_WIDTH;
isGameFirst = true;
fill(); //先填上空方块
show_land(); //直接展示
return 0;
}
int bray(int x,int y) {
//给棋盘布雷 接收两个坐标 此坐标对应的格子叫做"安全点" 安全点及其周围不会有雷
//安全点上数字为0
srand((unsigned)time(0)); //重置随机数种子
for (int n = 1;n <= MAX_LANDMINE;) {
int i = rand() % (MAX_WIDTH) + 1; //随机生成坐标
int j = rand() % (MAX_LENGTH) + 1;
if(!isSquAround(x,y,i,j)){
//雷如果不在安全点周围,就成功埋下雷
if (land[i][j].landmine == 0) {
land[i][j] = (square){-10, 1, STILLUNK};
++n; //地雷格子上的数字为一定是负数
}
}
}
return 0;
}
int fill(void){
/*填充方块*/
for (int i = 0; i <= REL_WIDTH - 1; ++i) {
for (int j = 0; j <= REL_LENGTH - 1; ++j) {
land[i][j] = (square) {0, 0, STILLUNK};
user_land[i][j] = STILLUNK;
}
}
return 0;
}
int set_squ(void){
/*设置方块数字*/
for (int i = 1; i <= MAX_WIDTH; ++i) {
for (int j = 1; j <= MAX_LENGTH; ++j) {
land[i][j].number += \
land[i - 1][j ].landmine + \
land[i - 1][j - 1].landmine + \
land[i - 1][j + 1].landmine + \
land[i + 1][j ].landmine + \
land[i + 1][j - 1].landmine + \
land[i + 1][j + 1].landmine + \
land[i ][j - 1].landmine + \
land[i ][j + 1].landmine; //有雷的landmine值为1,反之为0
}
}
return 0;
}
int show_land(void) {
printf("============================================================================================================\n");
/*打印装饰与坐标*/
printf("扫 雷 游 戏 ");
for (int i = 1; i <= MAX_LENGTH; ++i)
printf("[%2d] ", i);
printf("\nBL_hongzhong |");
for (int i = 1; i <= MAX_LENGTH - 1; ++i)
printf(" |");
printf("\n");
printf("2022年2月2日 |");
for (int i = 1; i <= MAX_LENGTH - 1; ++i)
printf(" |");
printf("\n");
printf(" V");
for (int i = 1; i <= MAX_LENGTH - 1; ++i)
printf(" V");
printf("\n");
/*打印棋盘本身*/
for (int i = 1; i <= MAX_WIDTH; ++i) {
printf("[%2d]--------> ", i);
for (int j = 1; j <= MAX_LENGTH; ++j)
if(land[i][j].now == MARKED)
printf("回 "); //标记格子样式
else if(land[i][j].now == STILLUNK)
printf("口 "); //未知格子样式
else
printf("%d ",land[i][j].number);
printf("\n\n");
}
printf("============================================================================================================\n");
return 0;
}
int player_action(void) {
/*获取用户输入 响应输入*/
int choose, input_x, input_y;
printf("选择您的操作\n输入0:打开方块\n输入1:(取消)标记方块\n");
printf("您的选择 >>> ");
scanf("%d", &choose);
Sweep_fflush(); //清空缓冲区 避免scanf()函数的错误输入导致 程序内存溢出
printf("请输入待操作方块的坐标 中间用英文逗号隔开 >>> ");
scanf("%d,%d", &input_x, &input_y);
Sweep_fflush();
if (choose != 0 && choose != 1) {
printf("无效的操作 %d\n", choose);
} else if (input_x < 1 || input_y < 1 || input_x > MAX_WIDTH || input_y > MAX_LENGTH) {
printf("无效的坐标 (%d,%d)\n", input_x, input_y);
} else {
if (choose == 0) {
if (land[input_x][input_y].now == OPENED || land[input_x][input_y].now == MARKED) {
printf("无法打开方块,可能是因为方块(%d,%d)被标记或已被打开\n", input_x, input_y);
} else {
open_squ(input_x, input_y);
}
} else if (choose == 1) {
if (land[input_x][input_y].now == OPENED) {
printf("无法标记方块,因为方块(%d,%d)已经被打开\n", input_x, input_y);
} else {
mark_squ(input_x,input_y);
}
}
}
return 0;
}
int open_squ(int x, int y) {
if(isGameFirst){ //若是第一次点击,则初始化棋盘
bray(x,y);//布雷
set_squ();//设置每个方块的数字
isGameFirst = false;
open_squ(x,y);
} else {
if (land[x][y].landmine == 1) {
opennum = OVER;
} else {
/*连锁打开规则:点到0后递归打开周围所有方块*/
land[x][y].now = OPENED;
user_land[x][y] = land[x][y].number;
++opennum; // 打开方块数量++
if (land[x][y].number == 0){
if(y + 1 <= MAX_LENGTH){
if(land[x][y + 1].now == STILLUNK){
open_squ(x, y + 1);
}
if(x - 1 >= 1){
if(land[x - 1][y + 1].now == STILLUNK)
open_squ(x - 1, y + 1);
}
if(x + 1 <= MAX_WIDTH){
if(land[x + 1][y + 1].now == STILLUNK)
open_squ(x + 1, y + 1);
}
}
if(y - 1 >= 1){
if(land[x][y - 1].now == STILLUNK)
open_squ(x, y - 1);
if(x - 1 >= 1){
if(land[x - 1][y - 1].now == STILLUNK)
open_squ(x - 1, y - 1);
}
if(x + 1 <= MAX_WIDTH){
if(land[x + 1][y - 1].now == STILLUNK)
open_squ(x + 1, y - 1);
}
}
if(x - 1 >= 1){
if(land[x - 1][y].now == STILLUNK)
open_squ(x - 1, y);
}
if(x + 1 <= MAX_WIDTH){
if(land[x + 1][y].now == STILLUNK)
open_squ(x + 1, y);
}
}
}
}
return 0;
}
int mark_squ(int x,int y){
//改变方块的now属性来标记方块
land[x][y].now = land[x][y].now == MARKED? STILLUNK : MARKED;
user_land[x][y] = MARKED;
return 0;
}
int game_win(void) {
for (int i = 0; i <= MAX_WIDTH - 1; ++i) {
printf(" ");
for (int j = 0; j <= MAX_LENGTH - 1; ++j) {
printf("帅 ");
}
printf("\n\n");
}
printf("============================================================================================================\n");
printf("恭喜你,你成功通关了游戏!\n");
return 0;
}
int game_over(void) {
for (int i = 1; i <= MAX_WIDTH; ++i) {
printf(" ");
for (int j = 1; j <= MAX_LENGTH; ++j) {
if (land[i][j].landmine == 1) {
printf("雷 ");
} else {
printf("口 ");
}
}
printf("\n\n");
}
printf("============================================================================================================\n");
printf("很抱歉,游戏失败了!\n");
return 0;
}
main.cpp
#include "SweepMine.h"
extern int opennum; //打开格子数
int main(void){
game_start();
do{
player_action();
show_land();
if(AREA - opennum == MAX_LANDMINE){
//若棋盘面积 - 打开方块数 == 雷数,说明游戏胜利(没打开的格子此时都是雷了!)
game_win();
return 0;
}
}while(opennum != OVER);
game_over();
return 0;
}