C语言——简单扫雷小游戏(末尾有全码)

一、扫雷玩法介绍

        在下面方格中,有若干个雷随机分布在格子中,当玩家选择任意一个格子时,若格子不是“雷”,则会显示以此格子为中心,显示附近八个格子的“雷”的数量。

如果玩家选择的区域是“雷”则游戏结束

           下面让我们用C语言一步步实现这个游戏

二、扫雷的逻辑实现

       由游戏UI我们可以知道,我们需要准备一个格子盘(在下文统称“棋盘”),并在棋盘内设置若干个雷,当玩家选择棋盘上某个格子时,显示当前棋盘及其有当前位置有关雷的信息。

1.棋盘的准备

    由扫雷UI界面不难看出,我们对于棋盘中格子的信息可以用数组的形式来存放。但我们需要考虑这个数组的类型,假定我们用1和0分别来对应雷和非雷,在这里很多人第一时间想的是用int类型的数组,但这样会和以下问题有混淆:雷在这个程序中以1来定义,那么当我们选择一个格子时,格子周围八个区域刚好只有一个雷,那么程序就很容易搞混;那么我们可以使用char类型的数组来存放雷,即字符”1“与字符”0“。

       那么确认了数组的类型后,接下来我们来考虑下一个问题,我在这个棋盘上需要完成布置雷,与每次扫雷后统计周围雷的数量,那么在这个棋盘上要设置的参数就有3个,那么我们有可能会混淆,因此,我们可以设置一个相同规格的棋盘,原本的棋盘用来布置雷的,另一个棋盘用来排查雷。简单理解为,平时玩的扫雷游戏它棋盘的数据是完全可视化的,但反馈给玩家的棋盘却是部分可视化。

    这是用于布置雷的棋盘,但我们在游戏中并不会将这个棋盘显示给玩家:

111111111
011111111
111111111
111111111
111111111
111111111
1


       这是显示给玩家的棋盘(还没进行扫雷的区域用*显示),因此,我们只需要在上方的棋盘完成游戏内的逻辑,再在下方的棋盘显示信息。

*********

当我们进行排雷时,会根据选择的位置对周围八个格子进行排查并显示雷的个数,那么就会发现,在最外围的边与列所统计到的格子数并不一致,我们以下面5*5的棋盘为例

       我们在最外围的行与列统计到的是周围3个格子的信息,而其他方位的格子统计到的是除自身外的8个位置,此时,我们可以在选择格子的时候附带一个判断,如果是最外围的格子,就统计周围3个格子的信息,如果不是最外围的格子就统计周围8个格子的信息。如果是这样的话,那么我们就会面临一个新的问题:当我们选择不同方向的最外围格子时,需要统计的格子位置都是不一样的,这样就会让我们编写统计格子信息时的工作量呈指数型上升。

        如果我们为了能让统计周围格子信息的时候都是统计周围8个格子,那么就是很好的方法,我们不妨让9*9的棋盘再大一圈,即变成一个11*11的棋盘,那么我们在这个11*11的棋盘中的9*9棋盘中统计任意格子周围的信息时都是统计八个格子;我们用5*5的棋盘为例,将它扩大成7*7的棋盘,然后在7*7的棋盘里的5*5棋盘完成一系列操作。而当玩家进行游戏时,我们只需要将这个7*7棋盘显示出其中的5*5棋盘即可。

首先,确定了要建立一个n*n的棋盘,我们可以通过宏定义(显示9*9的棋盘,那么我们需要创建一个11*11的棋盘) 

COL与ROW是显示给玩家的棋盘大小

#define COLS 11 //列
#define ROWS 11//行
#define COL COLS-2
#define ROW ROWS-2
#define fires 10  //布置的雷的数量

由此,我们可以这样创建两个棋盘:

char show[ROWS][ROWS];//显示排查雷的信息,即正式游戏中显示的扫雷棋盘
char behind_show[ROWS][COLS];//安排雷的棋盘

2.对雷区和非雷区的准备

既然我们选择了以字符’1‘与字符’0‘分别代替雷和非雷,*表示未排雷区,首先,我们需要对这两个棋盘进行初始化,对于展现给玩家的棋盘全部初始化为*,布置雷的棋盘全部初始化为字符‘0’。

而在初始化棋盘的函数中,我们需要接收的形参为:一个二维数组,行数,列数,以及要初始化成的元素 set ,如字符‘1’,‘0’,‘*‘。然后通过两个for循环遍历整个二维数组,将数组元素初始化。

/棋盘初始化(接收一个二维数组,初始化为set)
void star(char arr[ROWS][COLS], int row, int col, char set) {
	for (int i = 0; i <row; i++) {
		for (int j = 0; j < col; j++) {
			arr[i][j] = set;
		}
	}
}

将两个棋盘通过star函数初始化: 

star(show, ROWS, COLS, '*');
star(behind_show, ROWS, COLS, '0');

初始化后,我们可以先验证这两个二维数组是否初始化成功,再进行后续的操作:

这里我们可以编写一个查看二维数组元素的函数get_show,对于查看二维数组的函数,我们需要查看这个二维数组的每一行和每一列的元素,因此,函数的形参可以设置为:一个二维数组,行数,列数。

void get_show(char arr[ROWS][COLS], int row, int col) {
	for (int i = 1; i <= ROW; i++) {
		for (int j = 1; j <= COL; j++) {
			printf("%c ", arr[i][j]);
		}
		printf("\n");
	}

}
int main(){
//我们需要查看的棋盘大小为9*9,二维数组11*11行与列最大下标为10,通过控制循环的初始条件与终止范围可以取得其中9*9的棋盘
     get_show(show,ROW,COL);
     get_show(behind_show,ROW,COL);
}

 此时调用get_show函数可以看到,两个棋盘初始化成功了。但是,对于第几行第几列的元素并不直观。

        那么,我们可以在棋盘的上方与左侧打印出相对应的行数与列数,以便更直观的知道对应位置的元素。对于棋盘的上方,在遍历函数之前,我们可以通过循环打印出坐标数1-9,对于左侧,在打印棋盘每一行之前,先打印其对应的行号,再打印棋盘,只需要添加几行代码即可完成。 此时我们再调用get_show函数查看效果

void get_show(char arr[ROWS][COLS], int row, int col) {
//先通过一个循环打印行数的标记
	for (int i = 0; i <= 9; i++) {
		printf("%d ", i);
	}
	printf("\n");
	for (int i = 1; i <= ROW; i++) {
		printf("%d ", i);//每次打印新的一行前,先打印当前的行号
		for (int j = 1; j <= COL; j++) {
			printf("%c ", arr[i][j]);
		}
		printf("\n");
	}

}
int main(){
    get_show(show,ROW,COL);
    get_show(behind_show,ROW,COL);
}

 

 3.安排雷区

在完成上述工作后,我们就可以在棋盘上布置雷了。我们需要在11*11的二维数组棋盘上的9*9区域里布置雷,显然,传递给安排雷区的函数的形参是和打印棋盘函数的形参是一致的。对于每次游戏雷的位置都是不一样的,我们需要用到随机数。

在这里,我们先认识一下函数rand(),它包含在库函数中,需要调用头文件stdio.h,但是注意,它并不是正在意义上的随机数,是一个伪随机数,范围是0-Rand_Max;为了解决这个问题,我们可以用srand()函数,通过更新时间种子来获取新的随机数。用法如下:

srand((unsigned int)time(NULL));
int a=rand();

 认识了rand函数和srand函数后,我们就可以完成安排雷区的函数:

void  mine(char arr[ROWS][COLS], int row, int col) {
	int count = fires;//总共需要布置的雷数
	while (count) {
		int a = rand() % row + 1;//得到的位置是1-9(得到的随机数取余n后,得到的余数最大为n-1,最小为0)
		int b = rand() % col + 1;//得到的位置是1-9
        //判断是否为雷区,如果该位置没有雷,则布置雷
		if (arr[a][b] == '0') {
			arr[a][b] = '1';
			count--;
		}
	}
}

接下来,让我们验证这个函数功能是否成功实现:

//棋盘初始化(接收一个二维数组,初始化为set
void star(char arr[ROWS][COLS], int row, int col, char set) {
	for (int i = 0; i <row; i++) {
		for (int j = 0; j < col; j++) {
			arr[i][j] = set;
		}
	}
}
//棋盘的打印
void get_show(char arr[ROWS][COLS], int row, int col) {
	for (int i = 0; i <= 9; i++) {
		printf("%d ", i);
	}
	printf("\n");
	for (int i = 1; i <= ROW; i++) {
		printf("%d ", i);
		for (int j = 1; j <= COL; j++) {
			printf("%c ", arr[i][j]);
		}
		printf("\n");
	}

}
//安排雷
void  mine(char arr[ROWS][COLS], int row, int col) {
	int count = fires;
	while (count) {
		int a = rand() % row + 1;
		int b = rand() % col + 1;
		if (arr[a][b] == '0') {
			arr[a][b] = '1';
			count--;
		}
	}
}

int main(){
    char show[ROWS][ROWS];//显示排查雷的信息,即正式游戏中显示的扫雷棋盘
	char behind_show[ROWS][COLS];//安排雷的棋盘

	//对两个棋盘初始化
	star(show, ROWS, COLS, '*');
	star(behind_show, ROWS, COLS, '0');

	//查看布置雷的棋盘
	get_show(behind_show,ROW,COL);
    printf("\n");
    //安排雷
    mine(behind_show, ROW, COL);
    
    //查看是否安排雷成功
    get_show(behind_show,ROW,COL);

    
    

}

可以看到,已经在棋盘上成功布置雷,且每次游戏雷的位置都是不一样的: 

4.开始扫雷

现在我们到了最关键的一步——扫雷。当我们选择一个格子时,游戏会返回给玩家反馈:

  1. 选择的区域是雷,游戏结束;
  2. 选择的区域是非雷,当前区域显示一个数字表示周围八个格子雷的数量;
  3. 如果选择的格子是第9*9棋盘上除去雷区的最后一个格子,游戏胜利。

那么,在这一步最大的问题是,当我们选择一个格子时,如何统计周围八个格子的雷的数量? ,假如我们输入的位置是a,b,那么在arr[a][b]里,周围八个格子的方位如下所示

 

 

 在计算机中,字符以ASCII码的形式存储,字符1的ASCII码是49,字符0是48,两者的差刚好是1,通过计算周围八个格子的ASCII码的和,减去周围八个格子都是ASCII码‘0’的和,所得的差就是周围八个格子的雷的数量,然后我们再转为整形即可。通过一个sum_mine函数实现:

int sum_mine(char arr[ROWS][COLS], int a, int b) {
	return  (arr[a - 1][b + 1] + arr[a - 1][b] + arr[a - 1][b - 1] + arr[a][b - 1] + arr[a + 1][b - 1] + arr[a + 1][b] + arr[a + 1][b + 1] + arr[a][b - 1]) - 8 * '0';

}

那么到这里,我们就完成了百分之八十的内容,现在,让我们来完成选择棋盘格子后的逻辑函数。

  1. 玩家输入所选择的行数与列数。
  2. 判断该位置是否为雷,是雷则游戏结束,非雷的话调用sum_mine函数返回周围八个格子的雷的个数
  3.  如果选择的格子是第9*9棋盘上除去雷区的最后一个格子,游戏胜利。

在这个函数形参上与之前的函数有所不同,它需要传递两个二维数组。 因为输入选择的行数与列数,雷区的安排等操作在behind_show[ROWS][COLS]数组上,show[ROWS][COLS]数组显示选择区域的信息。选择的坐标遍布在11*11中的9*9,所以当我们调用这个函数时,传参应该是ROW与COL,也就是9与9。

void seek_mine(char show[ROWS][COLS], char behind_show[ROWS][COLS], int row, int col) {
	int a = 0;
	int b = 0;
	int win = 0;
	while (fires > 0) {
		printf("请输入要排除的坐标:");
		scanf_s("%d %d", &a, &b);
		if (a >= 1 && a <= row && b >= 1 && b <= col) {
			if (behind_show[a][b] == '1') {
				printf("你被炸死了\n");
				get_show(behind_show, row, col);
				break;
			}
			else {
				//统计附近八个格子的雷
				int sum = sum_mine(behind_show, a, b);
				show[a][b] = '0' + sum;
				win++;
				get_show(show, row, col);
			}
		}
		else {
			printf("坐标非法,重新输入\n");
		}
	}

	if (win == row * col - fires) {
		printf("恭喜你赢了\n");
		get_show(behind_show, row, col);
	}

}

至此,整个游戏所需的函数皆已完成,最后,我们只需要如同积木一般将这些函数组装在主函数起来。

5.游戏实现与原码

为了界面不显得那么单调,我们可以设置一个menu函数,使得界面美观起来。

void menu() {
	printf("-------\n");
	printf("-------\n");
	printf("-------\n");
	printf("0.退出游戏\n");
	printf("1.开始游戏\n");
	
}

对于这个游戏,在主函数的逻辑也是非常清晰的

  1. 游戏主界面
  2. 选择是否游戏
  3. 进入游戏后,第一次选择格子,随后判断
  4. 游戏胜利与否

 而在主函数,我们需要判断输入的数来选择相应的选项,可以通过do.....while循环来实现。而对于上面所有的函数,我们都可以放在一个open_game()函数中。

那么,在头文件中,我们存放宏定义与函数的声明

#pragma once
//注意,宏定义中不分大小写
#define COLS 11 //行
#define ROWS 11//列
#define COL COLS-2
#define ROW ROWS-2
#define fires 10
//头文件放函数的声明

//棋盘初始化
void star(char arr[ROWS][COLS], int row, int col, char set);

//打印棋盘
void get_show(char arr[ROWS][COLS], int row, int col);

//
void open_game();

//游戏菜单
void menu();

//安排雷
void mine(char arr[ROWS][COLS], int row, int col);

//排查雷
void seek_mine(char show[ROWS][COLS], char behined_show[ROWS][COLS], int row, int col);

//统计雷
int sum_mine(char arr[ROWS][COLS], int a, int b);

在存放函数主体的.c文件中:

#include<stdio.h>
#include"game_head.h"
#include<Windows.h>
#include <stdlib.h>
//游戏开始前页面
void menu() {
	printf("-------\n");
	printf("-------\n");
	printf("-------\n");
	printf("0.退出游戏\n");
	printf("1.开始游戏\n");
	
}
//游戏逻辑函数
void open_game() {
	char show[ROWS][ROWS];//显示排查雷的信息,即正式游戏中显示的扫雷棋盘
	char behind_show[ROWS][COLS];//安排雷的棋盘
	//对两个棋盘初始化
	star(show, ROWS, COLS, '*');
	star(behind_show, ROWS, COLS, '0');
	//查看棋盘
	get_show(show,ROW,COL);
	printf("\n");
	get_show(behind_show,ROW,COL);
	printf("\n");
	//安排雷
	mine(behind_show, ROW, COL);
	//给玩家显示游戏棋盘
	//get_show(show, ROW, COL);
	get_show(behind_show, ROW, COL);

	//开始排查雷
	//seek_mine(show, behind_show, ROW, COL);



}
//棋盘初始化(接收一个二维数组,初始化为set
void star(char arr[ROWS][COLS], int row, int col, char set) {
	for (int i = 0; i <row; i++) {
		for (int j = 0; j < col; j++) {
			arr[i][j] = set;
		}
	}
}
//棋盘的打印
void get_show(char arr[ROWS][COLS], int row, int col) {
	for (int i = 0; i <= 9; i++) {
		printf("%d ", i);
	}
	printf("\n");
	for (int i = 1; i <= ROW; i++) {
		printf("%d ", i);
		for (int j = 1; j <= COL; j++) {
			printf("%c ", arr[i][j]);
		}
		printf("\n");
	}

}
//安排雷
void  mine(char arr[ROWS][COLS], int row, int col) {
	int count = fires;
	while (count) {
		int a = rand() % row + 1;
		int b = rand() % col + 1;
		if (arr[a][b] == '0') {
			arr[a][b] = '1';
			count--;
		}
	}
}

//排查雷
void seek_mine(char show[ROWS][COLS], char behind_show[ROWS][COLS], int row, int col) {
	int a = 0;
	int b = 0;
	int win = 0;
	while (fires > 0) {
		printf("请输入要排除的坐标:");
		scanf_s("%d %d", &a, &b);
		if (a >= 1 && a <= row && b >= 1 && b <= col) {
			if (behind_show[a][b] == '1') {
				printf("你被炸死了\n");
				get_show(behind_show, row, col);
				break;
			}
			else {
				//统计附近八个格子的雷
				int sum = sum_mine(behind_show, a, b);
				show[a][b] = '0' + sum;
				win++;
				get_show(show, row, col);
			}
		}
		else {
			printf("坐标非法,重新输入\n");
		}
	}

	if (win == row * col - fires) {
		printf("恭喜你赢了\n");
		get_show(behind_show, row, col);
	}

}

//统计附近的雷
//字符1的ASCII码是49,0是48
int sum_mine(char arr[ROWS][COLS], int a, int b) {
	return  (arr[a - 1][b + 1] + arr[a - 1][b] + arr[a - 1][b - 1] + arr[a][b - 1] + arr[a + 1][b - 1] + arr[a + 1][b] + arr[a + 1][b + 1] + arr[a][b - 1]) - 8 * '0';

}

 在main函数文件里:

#include<stdio.h>
#include"game_head.h"
#include<Windows.h>
#include <time.h>
int main() {
	int input_number = 0;;
	srand((unsigned int)time(NULL));
	do {
		menu();
		scanf_s("%d", &input_number);
		switch (input_number) {
		case 1:
			open_game();
			break;
		case 0:
			printf("退出游戏成功");
			break;

		default:
			printf("请重新输入\n");
			break;
		}
	} while (input_number);

	return 0;
}

 至此,我们这个扫雷小游戏便完成了,但它目前还有些许不足,当第一次选择格子时,无法衍生到带有雷区信息的格子,后面,我们会完善这个游戏。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值