C语言-扫雷游戏(简易版)的实现

使用C语言实现扫雷游戏,并在控制台进行游玩

0 可以跳过的垃圾话

想着练练手,刚好看见舍友买了个扫雷游戏,于是就想着来写个扫雷看看,再试试能不能给它加点不一样的功能。当然,图形不会像现在的扫雷游戏那么精美就是了。

1 分析扫雷游戏

扫雷游戏的大体分析放在了下面的game.h里

2 扫雷游戏主体

代码运行于visual studio 2022中
我一共写了三个文件,包括两个.c源文件和一个.h头文件
分别是test.c、game.c、game.h。
<game.h>

	//我把需要用到的头文件、宏定义、部分自定义函数声明放在了game.h里
	//这样后续在.c文件里,直接#include "game.h"就好
	
#pragma once

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>

#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE 5
#define CHECK '='

//扫雷游戏-minesweeper的结构:开始菜单、棋盘布局、布雷格局、扫雷功能、游戏结算

//开始菜单在test.c中直接写就可以

//游戏本体
	//棋盘
		//空白棋盘(初始化棋盘)
		void initialize_board(char board[ROWS][COLS], int rows, int cols, char component);
		//表面棋盘
		//显示9*9的棋盘,使用二维字符数组,以放置“*”或ASCII码的方式来表示棋盘的位置
		void print_board(char board[ROWS][COLS], int row, int col);
		
		//布雷棋盘
		void set_mine(char board[ROWS][COLS], int row, int col, int mine);
	//扫雷
		//探查指定位置,使用scanf读入键盘输入
		//读入行列后,与布雷棋盘比对,给出踩中雷的提示或者没踩中雷,而周围有多少雷的提示
		void find_mine(char mine[ROWS][COLS], char surface[ROWS][COLS], int rows, int cols);
		//以递归的方式,实现自动排除连续的“周围无雷”地块

<game.c>

#define _CRT_SECURE_NO_WARNINGS 1

#include"game.h"

void initialize_board(char board[ROWS][COLS], int rows, int cols, char component)
{
	//初始化就是给数组中的各个元素赋值,这里是给字符数组赋值,两个循环直接搞定即可
	for (int i = 0; i < rows; i++)
	{
		for (int j = 0; j < cols; j++)
		{
			board[i][j] = component;
		}
	}

}
void print_board(char board[ROWS][COLS], int row, int col)
{
	for (int i = 0; i <= col; i++)
	{
		printf("%2d", i);
	}
	printf("\n");
	//这里就是先用来测试初始化有没有成功,而且后续用来把表面棋盘打印出来,因为打印的棋盘只要在中间的9*9,不需要11*11,所以使用1到ROL(9)就可以了
	for (int i = 1; i <= row; i++)
	{
		printf("%2d", i);//打印行号
		for (int j = 1; j <= col; j++)
		{
			printf("%2c", board[i][j]);//给一定的栏宽把它们对齐
		}
		printf("\n");//换行
	}
}


void set_mine(char board[ROWS][COLS], int row, int col, int mine)
{
	for (int i = 1; i <= mine; i++) {
		board[rand() % row + 1][rand() % col + 1] = '1';//设置mine个雷,雷的范围在ROW*COL的内部棋盘内
	}
}

int count_mine(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
	if (row <= 0 || col <= 0)
	{
		return -1;
	}
	else 
	{
		//如果没踩中雷,则在表面棋盘显示周围8格内的雷的数量,
		//即[row-1][col-1],[row-1][col],[row-1][col+1]
		//与[row][col-1],			   ,[row][col+1]
		//与[row+1][col-1],[row+1][col],[row+1][col+1]
		int mines = mine[row - 1][col - 1] + mine[row - 1][col] + mine[row - 1][col + 1] + mine[row][col - 1] + mine[row][col + 1] + mine[row + 1][col - 1] + mine[row + 1][col] + mine[row + 1][col + 1];
		//下面这段与check_auto中使用的符号有关,在不使用新的二维数组的情况下,我能做到的就是给数据修正
		//为了方便后续修改,我给用上了宏定义变量,名为CHECK,在game.h处修改
		if (mine[row - 1][col - 1] == CHECK)
		{
			mines -= CHECK - '0';
		}if (mine[row - 1][col] == CHECK)
		{
			mines -= CHECK - '0';
		}if (mine[row - 1][col + 1] == CHECK)
		{
			mines -= CHECK - '0';
		}if (mine[row][col - 1] == CHECK)
		{
			mines -= CHECK - '0';
		}if (mine[row][col + 1] == CHECK)
		{
			mines -= CHECK - '0';
		}if (mine[row + 1][col - 1] == CHECK)
		{
			mines -= CHECK - '0';
		}if (mine[row + 1][col] == CHECK)
		{
			mines -= CHECK - '0';
		}if (mine[row + 1][col + 1] == CHECK)
		{
			mines -= CHECK - '0';
		}return mines - 8 * '0';
	}
}

void find_mine(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
	//如果踩中雷,则游戏结束
	if (1 <= row && row <= ROW && 1 <= col && col <= COL)
	{
		if(surface[row][col] == '*')
		{
			surface[row][col] = count_mine(mine, surface, row, col) + '0';
		}
	}
}

void auto_surrounding(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
	if (1 <= row && row <= ROW && 1 <= col && col <= COL)
	{
//		find_mine(mine, surface, row - 1, col - 1);
		find_mine(mine, surface, row - 1, col);
//		find_mine(mine, surface, row - 1, col + 1);
		find_mine(mine, surface, row, col - 1);
		find_mine(mine, surface, row, col + 1);
//		find_mine(mine, surface, row + 1, col - 1);
		find_mine(mine, surface, row + 1, col);
//		find_mine(mine, surface, row + 1, col + 1);
	}
}

int check(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
	int flag = 0;

//	if (1 <= row && row <= ROW && 1 <= col && col <= COL)
//	{
		if (surface[row][col] == '*')		
		{
			return flag + 1;
		}
//	}
	return 0;
}



void auto_sweeper(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col, int mines, int depth)
{
//	system("cls"); //测试用,带此段代码运行测试时,请在进入游戏、输入非地雷坐标后按住任意键以观察显示变化
//	printf("depth = %d", depth);
//	print_board(surface, ROW, COL);
//	print_board(mine, ROW, COL);
//	system("pause");

	if (mines == 0)
	{
		int flag = 0;
		auto_surrounding(mine, surface, row, col);
		if ((1 <= row && row <= ROW) && (1 <= col && col <= COL))//防止越界,导致探查的位置超出棋盘
		{
			//以上、下、左、右的方式把没雷的地方走一遍
			if (flag = check(mine, surface, row - 2, col)) {
				mines = count_mine(mine, surface, row - 1, col);
				auto_sweeper(mine, surface, row - 1, col, mines, depth + 1);//上
			}
			if (flag = check(mine, surface, row, col + 2)) {
				mines = count_mine(mine, surface, row, col + 1);
				auto_sweeper(mine, surface, row, col + 1, mines, depth + 1);//右
			}
			if (flag = check(mine, surface, row + 2, col)) {
				mines = count_mine(mine, surface, row + 1, col);
				auto_sweeper(mine, surface, row + 1, col, mines, depth + 1);//下
			}
			if (flag = check(mine, surface, row, col - 2)) {
				mines = count_mine(mine, surface, row, col - 1);
				auto_sweeper(mine, surface, row, col - 1, mines, depth + 1);//左
			}
		}
	}
}

void check_auto(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col, int flag, int depth)
{
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			if (surface[i][j] == '0' && mine[i][j] != CHECK) 
			{
				mine[i][j] = CHECK;//'='关联count_mine函数中的mines的计算公式,ASCII中‘=’=61
				auto_sweeper(mine, surface, i, j, flag, 0);
			}
		}
	}
}

int success(char mine[ROWS][COLS], char surface[ROWS][COLS])
{
	int marked = 0;
	int leftover = 0;
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			if (surface[i][j] == '#' && mine[i][j] == '1')//正确标记贡献的地雷数
			{
				marked++;
			}
			if (surface[i][j] == '*') {
				leftover++;
			}
		}
	}
	return (marked != MINE) ? (leftover + marked): (marked);
	//使用三目操作符,在marked不为MINE的时候,返回leftover + marked,由于marked的增长条件,它不会大于MINE
	//而当leftover + marked不为MINE时,有这种情况:
	//未标记或者已标记的地块中存在没有地雷且未探查的。
	//而我设置的游戏胜利条件是此函数返回的值为MINE才会胜利并结束游戏
}

void game()
{
	int mark = 0;
	printf("test\n");
	
	char surface_board[ROWS][COLS];
	char mine_board[ROWS][COLS];

	initialize_board(surface_board, ROWS, COLS, '*');//这里数组实参填数组名即可,无需加【】【】,那样就是表示数组中的具体元素了
	initialize_board(mine_board, ROWS, COLS, '0');//ROWS和COLS这里是11,为了方便后续进行地雷的判断。最后的字符表示初始化时的字符内容,也就是初始化后数组所有元素的内容应该时字符‘0’

	set_mine(mine_board, ROW, COL, MINE);//布雷
//	print_board(surface_board, ROW, COL);//打印棋盘
//	print_board(mine_board, ROW, COL);
	

	//探查地雷
	while(1){
		system("cls");
		print_board(surface_board, ROW, COL);//打印棋盘
		print_board(mine_board, ROW, COL);
		int i = 0;
		int row, col;
		printf("请输入要探查的行号,若要进入标记模式,请输入0:");
		scanf("%d", &row);
		if (row == 0)
		{
			while (1) {
				printf("若要退出标记模式,请输入0行0列\n");
				printf("标记同一位置偶数次,即可取消标记\n");
				printf("剩余标记次数:%d\n", MINE - mark);
				printf("请输入要标记的行、列,使用空格或回车隔开:");
				scanf("%d%d", &row, &col);
				if (row == 0 && col == 0) {
					break;
				}
				else if (mark >= MINE) {
					printf("标记次数已耗尽,标记失败\n");
				}
				else if ((0 < row || row < ROW || 0 < col || col < COL) && (surface_board[row][col] == '*' || surface_board[row][col] == '#'))
				{
					if (surface_board[row][col] == '#')
					{
						surface_board[row][col] == '*';
						mark--;
					}
					else {
						surface_board[row][col] = '#';
						mark++;
					}
					i = success(mine_board, surface_board);
					if (i == MINE)
					{
						system("cls");
						print_board(surface_board, ROW, COL);//打印棋盘
						print_board(mine_board, ROW, COL);
						printf("恭喜你找到了所有的雷!游戏胜利!\n");
//						system("pause");
						break;
					}
				}
				else {
					printf("请输入棋盘内的行列号!\n");
					system("pause");
				}
				system("cls");
				print_board(surface_board, ROW, COL);//打印棋盘
				print_board(mine_board, ROW, COL);
			}
			if (i == MINE)
				break;
		}
		else {
			printf("请输入要探查的列号:");
			scanf("%d", &col);
			if (0 > row || row > ROW || 0 > col || col > COL)
			{
				printf("请输入棋盘内的行列号!\n");
				system("pause");
				continue;
			}
			find_mine(mine_board, surface_board, row, col);
			if (mine_board[row][col] == '1')
			{
				printf("很遗憾,你踩中地雷了\n");
				system("pause");
				break;
			}
			int flag = count_mine(mine_board, surface_board, row, col);
			auto_sweeper(mine_board, surface_board, row, col, flag, 0);
			check_auto(mine_board, surface_board, row, col, flag, 0);//给自动延伸(auto_sweeper)查缺补漏
			//此处后续加一个功能,当探查地块周围没有雷时,自动探查周围直到地块数字不为0为止
			i = success(mine_board, surface_board);
			if (i == MINE)
			{
				system("cls");
				print_board(surface_board, ROW, COL);//打印棋盘
				print_board(mine_board, ROW, COL);
				printf("恭喜你找到了所有的雷!游戏胜利!\n");
				break;
			}
		}
	}
	Sleep(8000);
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"game.h"

void menu()
{
	printf("* * * * * * * * * * * * * *\n");
	printf("* M I N E - S W E E P E R *\n");
	printf("* *   author:fireqian   * *\n");
	printf("* *                     * *\n");
	printf("* *   enter 1 to start  * *\n");
	printf("* *   enter 0 to exit   * *\n");
	printf("* * * * * * * * * * * * * *\n");
}

int main()
{
	int start = 1;
	//菜单
	menu();
		while (scanf("%d", &start) != EOF)
		{
			srand((unsigned int)time(NULL));//每一局游戏的不同种子
			switch (start) {
			case 1://开始游戏
				printf("开始游戏,加载中...\n");//提示操作结果
				Sleep(300);
				system("cls");
				game();
				system("pause");
				system("cls");
				menu();
				break;
			case 0://结束游戏
				break;
			default:
				printf("请输入1或0,以开始或结束游戏\n");
				system("pause");
				system("cls");
				menu();
				break;
			}
			if (start == 0) {
				break;//实现结束游戏,终止程序while循环
			}
		}
}

12.23 这里放个运行的图片(其实自动延伸还有个bug)
12.23上面的是游戏的棋盘,下面的是布雷的棋盘,方便我看运行情况

12.22 game.c和test.c的主体我都写好了,等我摸一会拓展功能在复制上来。分析在那个时候一块写
12.23 啊,写那个遇见0地雷可自动延伸的功能发现挺多bug的,简单修修凑合用了。代码写的太乱了,还没整理。要是各位读者也想写这个功能,不妨试试8方向判定,我这个是4方向判定的,时间太晚了,11点23分了,我先休息(打街霸6)去了,今天也是摸了,晚安。
12.24 早上11点,想到了探查周围8格其实可以改成周围4格,就是auto_surrounding函数可以只留下上、下、左、右四个方向的地块探查,防止出bug。今天稍晚再写个插旗标记的功能吧(说是插旗,其实没有旗帜符号,我到时候用@来代替吧),然后要是有时间就再写一个游戏胜利的判定。
关于如何减少检查次数(也就是不让check_auto检查已经检查过的地方),我第一想法是再设置第三个棋盘(也就是第三个二维数组),用来标记检查痕迹,用有无检查痕迹作为check_auto的一个检查条件,避免重复检查。
(现在还没写出来,简单的思路就是先新建一个同样大小的二维数组,再初始化。然后在check_auto里加入一个功能,使检查过的地块在check棋盘里变为某一约定好的值,这里我还是想用char类型,毕竟一个char才一个字节。再然后在check_auto里加一个if(check[i][j]) != ‘某一约定好的值’)就行了)。
12.25 昨天晚上睡不着,突然脑子里想到,不需要追加新的棋盘也能做避免重复检查的标记。反正自动延展不会在地雷地块生效,就在布雷棋盘上做标记就是了,不过记号要和地雷区分开来。写了才深刻感受到什么叫史山代码,哈哈。一环套一环的,没有注释的话不知道在干嘛。想方便还是直接做个新的二维数组吧,省心。这里我没做,就直接在地雷的二维数组里改,当场出现bug,修了一会哈哈。
顺带摸了标记功能,然后加了点保护措施。标记功能就懒得再打包成一个函数了,就这样吧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值