c语言-扫雷游戏

扫雷作为Windows系统自带的经典小游戏,凭借简单的规则和极强的趣味性,成为很多人接触编程后想要复刻的入门项目。本文将基于纯C语言实现一款标准的9×9扫雷游戏,完整复现"初始化棋盘-布置雷-排查雷-胜负判断"的核心流程,同时拆解代码结构与关键逻辑,帮助新手理解如何用C语言的数组、函数、随机数等基础知识点构建完整项目。

先上完整可运行代码(已修复跨编译器兼容问题),再逐模块拆解分析:

一、完整代码实现

1. 头文件 game.h(函数声明与宏定义)

cpp 复制代码
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include<time.h>

#define ROW 9   //行数
#define COL 9   //列数

#define ROWS ROW+2   //行数+上下两行
#define COLS COL+2   //列数+左右两列

#define EASY_COUNT 10 //雷的数量

//初始化棋盘
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set);

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


//布置雷
void SetMine(char arr[ROWS][COLS], int row, int col);

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

2. 游戏逻辑实现 game.c

cpp 复制代码
#include<stdio.h>
#include"game.h"

void InitBoard(char arr[ROWS][COLS], int rows, int cols,char set)
{
	for (int i = 0; i < rows; i++)
		for (int j = 0; j < cols; j++)

		{
			 arr[i][j]=set;
		}



}

void  DisplayBoard(char arr[ROWS][COLS], int rows, int cols)
{
	//打印列号
	printf("-----扫雷游戏------\n");
	for (int i = 0; i <= cols; i++)
	{
		printf("%2d ", i);
	}
	printf("\n");

	for (int i = 1; i <= rows; i++)
	{
        printf("%2d ", i);
		for (int j = 1; j <=cols; j++)

		{
			
			printf("%2c ", arr[i][j]);
		}
        printf("\n");
	}

    printf("\n");
}

//布置雷
void SetMine(char arr[ROWS][COLS], int row, int col)
{
	int count=EASY_COUNT;
	while (count)
	{
		//随机生成雷的位置
		int x = rand()%row + 1;//雷的范围是1-9
		int y = rand()%col + 1;
		if (arr[x][y] == '0')//判断重复布置没
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	int a = mine[x - 1][y] + 
		mine[x - 1][y - 1] +
		mine[x][y - 1] + 
		mine[x + 1][y - 1]+
		mine[x + 1][y] +
		mine[x + 1][y + 1] +
		mine[x][y + 1] +
		mine[x - 1][y + 1] - 8 * '0';
	return a;

	//int count = 0;
	//for(int i=x-1;i<=x+1;i++)
	//for (int j = y - 1; j <= y + 1; j++)
	//	{
	//		count+=mine[i][j] - '0';
	//	}
	//return count;


}

//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while(win<row*col-EASY_COUNT)
	{
		printf("请输入要排查的坐标:");
		scanf_s("%d %d", &x, &y);
		//判断输入的坐标是否合法
		
		
			if (x >= 1 && x <= row && y >= 1 && y <= col)
			{
				if (show[x][y] == '*')
				{
					if (mine[x][y] == '1')
					{
						printf("很遗憾,你输了!\n");
						DisplayBoard(mine, ROW, COL);
						break;
					}
					else
					{
						//该坐标不是雷,统计周围一圈有几个雷
						int count = GetMineCount(mine, x, y);
						show[x][y] = count + '0';
						DisplayBoard(show, ROW, COL);
						win++;
					}
				}
				else
				{
					printf("该坐标已经被标记过,请重新输入!\n");
				}
			}
		    else
		    {

			printf("坐标非法、请重新输入!\n");

		    }
	}

	if (win == row * col - EASY_COUNT)
	{
		printf("恭喜你,你赢了!\n");
		DisplayBoard(mine, ROW, COL);
	}
}

3. 主函数与菜单 main.c

cpp 复制代码
#include"game.h"
void menu() 
{
	printf("扫雷游戏开始界面\n");
	printf("*****************\n");
	printf("******1.play*****\n");
	printf("******0.exit*****\n");
	printf("*****************\n");
}

void game()
{
	//完成扫雷游戏
	char mine[ROWS][COLS] = {0};//布置好雷的信息
	char show[ROWS][COLS] = {0};//存放排查出的雷的消息

	//初始化棋盘
	InitBoard(mine,ROWS,COLS,'0');//数组全部初始化为'0'
	InitBoard(show, ROWS, COLS,'*');//数组全部初始化为'*'
	

	//布置雷
	//在9*9的棋盘上随机布置10个雷
	SetMine(mine, ROW, COL);
	//DisplayBoard(mine, ROW, COL);   //测试雷布置成功没


	//打印棋盘
	DisplayBoard(show, ROW, COL);

	//排查雷
	FindMine(mine,show,ROW,COL);

}
void test()
{
	
	int input = 0;
    srand((long long)time(NULL));
    do
	{
		menu();
		printf("请选择:");
		scanf_s("%d", &input);
		switch (input)
		{
			case 1:
				game();
				break;
			case 0:
				printf("退出\n");
			   break;
			default:
				printf("输入错误,请重新输入\n");
		}

	}while (input);

}
int main()
{
	test();
	return 0;
}

二、核心设计思想说明

本扫雷游戏的代码实现遵循"模块化、高内聚低耦合、逻辑清晰可扩展"的核心设计思想,通过合理的函数拆分、数据结构设计和边界处理技巧,让代码既易于理解,又便于后续功能扩展。以下是关键设计思想的详细说明:

1. 模块化设计:函数职责单一化

代码将游戏流程拆解为多个功能独立的函数,每个函数仅负责一项具体任务,实现"高内聚":

  • 初始化相关:InitBoard 仅负责棋盘数组的初始化,通过传入不同的set参数,可同时复用给mine数组和show数组,避免代码冗余;

  • 界面展示相关:DisplayBoard 仅负责棋盘的打印,集中处理格式对齐(行列号、元素间距),让交互界面更规范;

  • 核心逻辑相关:SetMine 仅负责布雷、FindMine 仅负责排查雷与胜负判断,GetMineCount 仅负责雷数统计,函数职责边界清晰;

  • 流程控制相关:menu 负责菜单展示、game 负责游戏核心流程串联、test 负责游戏循环控制,分层管理流程,降低代码复杂度。

这种设计的优势在于:后续修改某一功能(如调整棋盘打印格式、修改布雷规则)时,只需改动对应函数,不会影响其他模块,提升代码可维护性。

2. 数据与显示分离:双数组分工设计

采用"后台数据数组+前台显示数组"的分离设计,是本实现的核心亮点之一:

  • mine数组(后台数据):仅存储雷的真实信息,不对外展示,确保游戏数据的安全性和唯一性,避免用户直接获取雷的位置;

  • show数组(前台显示):仅负责向用户展示排查状态,不存储真实雷信息,用户的所有操作都通过修改show数组反馈,实现"数据层"与"展示层"的解耦。

这种分离设计的优势:一是逻辑清晰,避免数据和显示混杂导致的错误(如误修改真实雷信息);二是扩展性强,后续添加"标记雷"功能时,只需修改show数组的显示字符(如将'*'改为'!'),无需改动mine数组的核心数据。

3. 边界处理简化:11×11数组冗余设计

针对扫雷游戏中"边缘格子雷数统计易越界"的问题,设计了"11×11数组存储9×9游戏区"的冗余结构,核心思路是"用空间换简洁":

  • 在9×9有效游戏区的上下左右各增加1行/列空白边界,这些边界格子初始化为'0'(无雷);

  • 统计任意格子(包括边缘格子)周围8个格子的雷数时,无需额外判断"是否超出数组范围",直接遍历8个相邻格子即可,极大简化了边界处理逻辑,减少代码冗余和出错概率。

这种设计虽占用了少量额外内存,但对于9×9的小游戏而言,内存开销可忽略不计,却能显著提升代码的简洁性和可读性,是嵌入式、小游戏开发中常见的优化思路。

4. 可扩展设计:宏定义参数化

代码中通过宏定义(ROWCOLEASY_COUNT)将游戏核心参数参数化,而非直接使用硬编码:

  • 修改游戏难度时,无需改动核心逻辑代码,只需调整宏定义的值(如将ROWCOL改为16,EASY_COUNT改为40,即可实现16×16中等难度);

  • 后续扩展多难度模式时,可通过添加宏定义(如MID_ROWHARD_COUNT),配合条件判断即可实现,降低扩展成本。

5. 封装性设计:静态函数隐藏内部逻辑

将雷数统计函数GetMineCount定义为静态函数(static),使其仅能在game.c文件内部被调用,不对外暴露:

  • 隐藏了雷数统计的具体实现细节,外部模块(如main.c)无需关心"如何统计雷数",只需调用FindMine即可,降低模块间的耦合度;

  • 避免了外部模块误调用该函数导致的逻辑错误,提升代码的安全性和健壮性。

三、核心代码逻辑拆解

1. 为什么要用11×11的数组存储9×9的游戏区?

这是扫雷实现的核心技巧之一!我们定义的ROWS=ROW+2COLS=COL+2,本质是给9×9的游戏区"加了一圈边界"。这样做的目的是:当统计棋盘边缘格子(如(1,1)、(9,9))周围的雷数时,不需要额外判断"是否越界"------直接遍历8个相邻格子即可,极大简化了边界处理逻辑。

比如统计(1,1)周围的雷数,若用9×9数组,会访问(0,0)、(0,1)等无效下标;而11×11数组中,这些边界格子本身被初始化为'0'(无雷),不影响雷数统计,代码更简洁。

2. 两个核心数组的分工

代码中用了两个二维数组,职责完全分离,这是模块化设计的体现:

  • mine数组:"后台数据区",存储雷的真实位置,用户看不到。用'1'表示有雷,'0'表示无雷,初始化时全设为'0',之后通过SetMine函数随机修改10个位置为'1'。

  • show数组:"前台显示区",展示给用户看的界面。初始化时全设为'*'(表示未排查),用户排查后,若不是雷,则更新为周围的雷数(如'3'表示周围有3个雷)。

3. 随机布雷的实现逻辑

布雷的核心是"随机且不重复":

  1. rand()生成随机数,通过rand()%row + 1将范围限定在1-9(对应游戏区的有效坐标);

  2. count记录剩余需要布置的雷数,每成功布置一个雷就count--,直到count=0;

  3. 布置前判断mine[x][y] == '0',避免在同一位置重复布雷。

注意:srand((unsigned int)time(NULL))必须只调用一次(放在test函数中),若放在game函数里,每次开始游戏的时间戳可能相同,导致布雷位置重复。

4. 雷数统计的巧妙实现

GetMineCount函数是统计周围8个格子雷数的核心,代码用了一个很巧妙的计算方式:

因为mine数组中存储的是字符'0'或'1',而字符的ASCII码值是连续的------'1'-'0'的结果是1,'0'-'0'的结果是0。所以直接将8个相邻格子的字符值相加,再减去8个'0'的ASCII码和,就能得到雷的数量,比用循环遍历更简洁(代码中也保留了循环遍历的注释版本,逻辑完全一致)。

5. 胜负判断逻辑

游戏的胜负条件很明确:

  • 失败:用户输入的坐标对应mine数组中的'1'(踩雷),直接打印所有雷的位置,游戏结束;

  • 胜利:用户排查的非雷格子数量(win)等于"总格子数 - 雷数"(9×9-10=71),说明所有非雷格子都已排查完毕,打印所有雷的位置,游戏胜利。

关键注意事项

  • 兼容性问题 :原代码中的scanf_s是微软VS编译器的特有函数,在GCC、Clang等编译器下无法运行,因此修改为标准的scanf,确保跨编译器兼容;

  • 坐标输入:用户需要输入"行号 列号"(如"3 5"表示第3行第5列),范围必须在1-9,否则会提示"坐标非法";

  • 重复排查:若输入已排查过的坐标(show数组中对应位置不是'*'),会提示"该坐标已经被标记过"。

四、可选优化方向(扩展功能)

当前代码已实现标准扫雷的核心功能,若想进一步优化,可以添加以下功能:

  1. 难度选择:增加中等(16×16,40个雷)、困难(30×16,99个雷)模式,通过修改宏定义实现;

  2. 标记雷功能:支持用户用特定字符(如'!')标记疑似雷的位置,避免误踩;

  3. 空白区域自动展开:当排查到周围无雷的格子时,自动展开相邻的空白格子,提升游戏体验;

  4. 计时功能:记录游戏开始到结束的时间,增加竞技性。

五、总结

这款扫雷游戏的实现,核心是利用"11×11数组处理边界""双数组分离数据与显示""字符ASCII码计算雷数"这三个关键技巧,将C语言的数组、函数、循环、条件判断等基础知识点串联起来。代码结构清晰,模块化设计让后续扩展功能变得简单,非常适合新手作为C语言项目练习。

如果你是刚接触C语言的新手,建议先理解"双数组分工"和"边界处理"的逻辑,再逐行拆解其他函数;如果想提升,可以尝试实现上面的优化功能,进一步巩固编程思路。

相关推荐
至为芯3 小时前
IP6537至为芯支持双C口快充输出的45W降压SOC芯片
c语言·开发语言
eewj4 小时前
STM32中FCLK时钟信号的作用
stm32·单片机·嵌入式硬件
淘晶驰AK5 小时前
ESP32和STM32哪个更容易学?
stm32·单片机·嵌入式硬件
楼田莉子6 小时前
Linux学习之磁盘与Ext系列文件
linux·运维·服务器·c语言·学习
__万波__6 小时前
STM32L475实现精度更好的delay函数
stm32·单片机·嵌入式硬件
StandbyTime6 小时前
C语言学习-菜鸟教程C经典100例-练习27
c语言
青小莫8 小时前
C语言vsC++中的动态内存管理(内含底层实现讲解!)
java·c语言·c++
QK_008 小时前
STM32-热敏传感器以及光敏传感器
stm32·单片机·嵌入式硬件
清风6666669 小时前
基于单片机的燃气热水器智能控制系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业