news 2026/10/14 3:32:57

C语言实现扫雷游戏(完整代码+详细注释)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言实现扫雷游戏(完整代码+详细注释)

扫雷是一款经典的益智游戏,规则简单却充满挑战。本文以 C 语言实现一个控制台版扫雷,面向刚接触编程的初学者,分享从需求分析、代码实现到功能优化的完整过程,希望能帮助大家更好地理解数组、函数与多文件编程。

一、扫雷游戏的分析与设计

1.扫雷游戏的功能说明

(1)玩家可以通过菜单选择扫雷游戏的开始与结束

(2)在9*9的棋盘上随机布置十个雷,玩家通过输入想要排查的坐标,来排查此处是否有雷:

如果是雷,则被炸死,游戏结束;

如果不是雷,则显示玩家排查的位置周围一圈八个坐标共有多少个雷

(3)当玩家将所有不是雷的坐标排查出,则游戏胜利

2.扫雷游戏程序设计与说明

(1)数据结构分析

首先我们需要一个棋盘来存放我们的雷,以及我们排查雷的坐标信息,很显然,如果只有一个棋盘,既想要存放我们布置的雷的信息,还要将这这些信息隐藏起来防止玩家看到,还要将玩家想要排查的雷的信息在棋盘上呈现,这个数组的设定一定是复杂并且难以想到的。所以我们就想到用两个棋盘来实现这个游戏,一个棋盘(对应数组mine)来存放雷的信息,另一个棋盘(对应数组show)用来存放被排查出的雷的信息。如图一所示为建立的第一个棋盘,将棋盘全部初始化。

图一

然后需要随机布置十个雷在mine数组中,如图二所示(已经随机布置好十个雷的棋盘)

图二

将另一个棋盘(show数组)用来显示排查雷的信息,如图三所示(还未开始排查雷时棋盘的样子)。

图三

之后玩家输入需要排查的坐标,程序在第一个棋盘上检测该位置是否为‘1’(代表有雷),为‘1’则输出“你被炸死了,游戏结束”,不是’1‘则在该坐标处显示周围一圈八个位置上共有多少个雷存在,便于玩家排查。如图四所示,表示该位置处不是雷,且位置周围有0个雷。

图四

但是有一个疑问?当我们想要排查位于棋盘最外围的那些坐标时,如果不是雷,那么周围一圈的雷的数量应该怎么去计算。我们知道,雷的数量是根据第一个9*9的棋盘上计算的,所以为了防止越界,我们将第一个棋盘设置为11*11的大小,然后规定只在中间的9*9的棋盘上埋雷,所以最外一圈肯定都不是雷,这样就可以成功计算9*9棋盘上最外一圈的雷的个数。

数组定义:

char mine[11][11] = {0};// ⽤来存放布置好的雷的信息

char show[11][11] = {0};// ⽤来存放排查出的雷的个数信息

(2)游戏文件设计

由于游戏的代码过长,所以为了使代码结构清晰,便于阅读以及为了实现代码复用,一份声明多出调用,所以我们采用多文件的形式:

test.c // ⽂件中写游戏的测试逻辑

game.c // ⽂件中写游戏中函数的实现等

game.h // ⽂件中写游戏需要的数据类型和函数声明等

点击这里-多文件的函数声明与定义

二.游戏代码的编写和实现

1.game.h //用来存放游戏中的函数声明

#pragma once #include <stdio.h> #include<time.h> #include<stdlib.h> //rand()的头文件,rand()的用处是生成随机整数,用来随机布置雷 //单纯rand()每次运行得到的随机序列是固定的,所以需要配合time,time.h是时间相关的头文件 #define COUNT 10 #define ROW 9 #define COL 9 //用define定义变量,便于数字更改 #define ROWS ROW+2 #define COLS COL+2 //mine棋盘要比原本的棋盘多两行两列,便于计算外围坐标周围一圈八个位置的雷的总数 void initboard(char board[ROWS][COLS], int rows, int cols, char set); //棋盘初始化的函数声明 void displayboard(char board[ROWS][COLS], int row, int col); //打印棋盘的函数声明 void setmine(char board[ROWS][COLS],int row,int col); //布置雷的函数声明 void findmine(char mine[ROWS][COLS],char show[ROWS][COLS], int row, int col); //排查雷的函数声明

2.game.c //game.h头文件中声明的函数的定义

#define _CRT_SECURE_NO_WARNINGS 1 //防止微软的VS编译器判定scanf函数不安全的警告 #include "game.h" //引入我们自己写的头文件 game.h,告诉编译器这个文件里存放的函数声明、宏、结构体可以在当前`.c`文件里使用。 void initboard(char board[ROWS][COLS], int rows, int cols, char set) { for (int i = 0;i < rows;i++) { for (int j = 0;j < cols;j++) { board[i][j] = set; //给mine数组初始化全部为‘0’,给show数组初始化全部为‘*’ } } }//初始化棋盘函数的定义 void displayboard(char board[ROWS][COLS], int row, int col) { printf("------扫雷游戏-----\n"); for (int i = 0;i <= col;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 ", board[i][j]);//打印棋盘 } printf("\n"); } }//打印棋盘函数的定义 void setmine(char board[ROWS][COLS], int row, int col) { //随机布置十个雷 int count = COUNT; while (count) { int x = rand() % row + 1;//x坐标的范围 int y = rand() % col + 1;//y坐标的范围 if (board[x][y] == '0') { board[x][y] = '1'; count--;//判断条件防止在一个地方多次埋雷 } } }//布置雷函数的定义 int getminecount(char mine[ROWS][COLS], int x, int y) { int sum = 0; for (int i = -1;i <= 1;i++) { for (int j = -1;j <= 1;j++) { sum+=mine[x + i][y + j]-'0';//mine函数中放的雷的信息‘0’和‘1’是字符形式,化为整型时要减‘0’ } } return sum; }//计算周围一圈雷个数的函数定义 void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int count = COUNT; int win = 0; //循环体:若将非雷的坐标全部排查完,则退出循环 while (win &lt; row * col - count) { printf("请输入你要排查的坐标\n"); scanf("%d%d", &amp;x, &amp;y); if (x &gt;=1 &amp;&amp; x &lt;= row &amp;&amp; y &gt;= 1 &amp;&amp; y &lt;= col) { if (mine[x][y] == '0') { int minecount = getminecount(mine, x, y);//调用函数,将(x,y)坐标周围一圈的雷的个数赋值给minecount变量 show[x][y] = minecount + '0';//以字符的形式呈现 displayboard(show, row, col); win++; } else { printf("你被炸死了,游戏结束\n"); displayboard(mine, row, col);//将所有埋雷的信息打印出来 break; } } else { printf("输入不合法,请重新输入\n"); } } if (win == row * col - count) { printf("恭喜你,排雷成功\n"); displayboard(mine, row, col); } }//排查雷函数的定义

3.test.c //主函数

#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include "game.h" //在 test.c 中引入该头文件之后,我们就可以调用 game.c 里面实现的各个游戏函数。 void menu() { printf("\n"); printf("选择1开始游戏\n"); printf("选择0结束游戏\n"); printf("****\n"); }//菜单函数的定义 void game(){ char mine[ROWS][COLS];//存放布置好的雷 char show[ROWS][COLS];//存放查好出来雷的信息 //棋盘初始化 initboard(mine, ROWS, COLS, '0'); initboard(show, ROWS, COLS, '*'); //打印棋盘 displayboard(show, ROW, COL); //布置雷 setmine(mine, ROW, COL); //发现雷 findmine(mine, show, ROW, COL); }游戏函数的定义,存放许多游戏函数,便于阅读以及整理代码 int main() { int input = 0; srand((unsigned int)time(NULL));//生成随机数 do { menu(); printf("请输入你的选择:\n"); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("退出游戏\n"); break; default: printf("重新输入\n"); break; } }while (input); return 0; }//主函数

4.游戏实现界面

图五(打印菜单)

图六(输入1:开始游戏)

图七(输入0:结束游戏)

图八(输入要排查的坐标)

三.扫雷游戏优化

1.自动展开空白格子

上述代码所描写的扫雷,只翻开当前一格,效率太慢。可以利用递归展开,如果当前格子周围没有雷,则自动把相邻空白格子全部翻开。

实现思路:当玩家排查的坐标周围没有雷时,递归地翻开该坐标周围一圈的格子;如果周围格子也没有雷,就继续向外展开,直到遇到有雷的边界为止。这样一次点击就能翻开一大片空白区域,体验和真正的扫雷就一样了。

首先在game.h中声明递归展开函数:

void expandblank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y); //自动展开空白格子的函数声明

然后在game.c中实现该函数。核心逻辑是:先判断当前坐标是否越界、是否已经翻开过;如果当前格子周围没有雷,就把它翻开并标记为空格,再递归处理它周围一圈的八个格子:

void expandblank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y) { // 越界检查,防止访问到棋盘外 if (x < 1 || x > ROW || y < 1 || y > COL) { return; } // 如果该格子已经翻开过,直接返回,避免死循环 if (show[x][y] != '*') { return; } // 计算当前格子周围一圈的雷的个数 int count = getminecount(mine, x, y); if (count == 0) { // 周围没有雷,翻开当前格子,标记为空格 show[x][y] = ' '; // 递归展开周围一圈的八个格子 for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { if (i == 0 && j == 0) { continue; // 跳过当前格子本身 } expandblank(mine, show, x + i, y + j); } } } else { // 周围有雷,只翻开当前格子,显示雷的个数 show[x][y] = count + '0'; } }//自动展开空白格子的函数定义

最后修改findmine函数。原来玩家输入坐标后,无论周围有没有雷都只翻开当前一格;现在改成:如果该坐标周围没有雷,就调用expandblank自动展开空白区域,否则只翻开当前格子。修改后的findmine完整代码如下:

void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int count = COUNT; int win = 0; //循环体:若将非雷的坐标全部排查完,则退出循环 while (win < row * col - count) { printf("请输入你要排查的坐标\n"); scanf("%d%d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (mine[x][y] == '0') { int minecount = getminecount(mine, x, y);//调用函数,将(x,y)坐标周围一圈的雷的个数赋值给minecount变量 if (minecount == 0) { //周围没有雷,自动展开空白格子 expandblank(mine, show, x, y); } else { //周围有雷,只翻开当前格子 show[x][y] = minecount + '0'; } displayboard(show, row, col); win++; } else { printf("你被炸死了,游戏结束\n"); displayboard(mine, row, col);//将所有埋雷的信息打印出来 } } else { printf("输入不合法,请重新输入\n"); } } if (win == row * col - count) { printf("恭喜你,排雷成功\n"); displayboard(mine, row, col); } }//排查雷函数的定义

需要注意:win的计数逻辑在自动展开后会有偏差,因为一次展开可能翻开了多个格子,但win只加了一次。更严谨的做法是在expandblank中统计实际翻开的格子数,这里为了保持代码简洁,先保留原有计数方式,读者可以自行优化。

2.标记地雷功能

增加标记:玩家按下某个选项,可以给怀疑是雷的格子插上旗子'#'。标记后不能点开;统计剩余未标记地雷数量。

3.计时功能

给游戏设计倒计时时间,比如设置为五分钟(说明随机埋藏的十个地雷将会在五分钟结束后爆炸),若五分钟内没有将非雷的位置排查完,则说明游戏失败,被炸死了。

实现思路:在game.h中引入windows.h头文件,使用clock()函数记录游戏开始时间,在findmine循环中不断检查当前时间与开始时间的差值,若超过设定的五分钟(300 秒)则判定游戏失败。

首先在game.h中声明计时相关函数,并添加时间头文件:

#include <windows.h> //windows.h中包含clock()函数,用于计时 #define TIME_LIMIT 300 //设置倒计时时间,单位为秒,这里设置为五分钟 void starttimer(); //开始计时的函数声明 int checktime(); //检查是否超时的函数声明,超时返回1,未超时返回0

然后在game.c中实现计时函数。使用clock()获取程序运行到当前时刻的时钟周期数,除以CLOCKS_PER_SEC得到秒数:

static clock_t start_time; //用静态变量保存游戏开始的时间,static保证整个游戏过程中该变量不会被重置 void starttimer() { start_time = clock(); //记录游戏开始时的时钟周期数 }//开始计时的函数定义 int checktime() { clock_t current_time = clock(); //获取当前时钟周期数 double elapsed = (double)(current_time - start_time) / CLOCKS_PER_SEC; //计算已经过去的时间,单位是秒 if (elapsed >= TIME_LIMIT) { return 1; //超过五分钟,返回1表示超时 } return 0; //未超时,返回0 }//检查是否超时的函数定义

最后修改findmine函数,在循环开头调用checktime()判断是否超时。同时需要在game()函数中调用starttimer()开始计时。修改后的findmine完整代码如下:

void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int count = COUNT; int win = 0; //循环体:若将非雷的坐标全部排查完,则退出循环 while (win < row * col - count) { //每次循环先检查是否超时 if (checktime()) { printf("时间到!五分钟内没有排完所有雷,你被炸死了\n"); displayboard(mine, row, col);//将所有埋雷的信息打印出来 return; //超时直接结束游戏 } printf("请输入你要排查的坐标\n"); scanf("%d%d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (mine[x][y] == '0') { int minecount = getminecount(mine, x, y);//调用函数,将(x,y)坐标周围一圈的雷的个数赋值给minecount变量 if (minecount == 0) { //周围没有雷,自动展开空白格子 expandblank(mine, show, x, y); } else { //周围有雷,只翻开当前格子 show[x][y] = minecount + '0'; } displayboard(show, row, col); win++; } else { printf("你被炸死了,游戏结束\n"); displayboard(mine, row, col);//将所有埋雷的信息打印出来 } } else { printf("输入不合法,请重新输入\n"); } } if (win == row * col - count) { printf("恭喜你,排雷成功\n"); displayboard(mine, row, col); } }//排查雷函数的定义

同时,需要在test.c的game()函数中,在调用findmine之前加上starttimer()开始计时:

void game() { char mine[ROWS][COLS];//存放布置好的雷 char show[ROWS][COLS];//存放查好出来雷的信息 //棋盘初始化 initboard(mine, ROWS, COLS, '0'); initboard(show, ROWS, COLS, '*'); //打印棋盘 displayboard(show, ROW, COL); //布置雷 setmine(mine, ROW, COL); //开始计时 starttimer(); //发现雷 findmine(mine, show, ROW, COL); }//游戏函数的定义

这样,游戏就具备了五分钟倒计时功能。玩家需要在五分钟内排完所有非雷格子,否则游戏失败。计时功能让游戏更有紧迫感和挑战性,也符合经典扫雷的玩法体验。

四.写代码的感悟与知识总结

写这个扫雷游戏的过程,让我在编程实践上收获了不少具体的知识。虽然一开始觉得扫雷不过是随机放几个雷、数数周围有几个雷,但真正动手才发现,很多细节远比想象中复杂。

首先是数组越界问题。一个格子周围有八个格子,但棋盘最外圈的格子如果直接计算周围八个位置,就会访问到数组边界之外。为了解决这个问题,我把9*9的棋盘扩展成了11*11,只在中间的9*9区域埋雷,最外一圈作为缓冲区。这个设计让我深刻理解了「用空间换安全」的思路——在数组外围多加一圈冗余,既能避免越界访问,又能统一计算逻辑,不需要为边界格子单独写特殊判断。

其次是数组下标从0开始还是从1开始的问题。调试时发现要么全屏都是雷,要么一个雷都没有,排查了半天才发现是下标起始位置搞混了。C语言的数组下标从0开始,但为了让玩家输入的坐标更直观,代码里把棋盘的有效区域设计成从1开始。这个经历让我意识到,在写代码时一定要明确下标约定,并且保持前后一致,否则很容易在循环和边界判断中出错。

另外,随机数的使用也让我学到了新东西。单纯调用rand()每次运行得到的随机序列是固定的,必须配合srand((unsigned int)time(NULL))来以当前时间作为随机种子,才能让每次游戏的雷的位置都不一样。这让我理解了随机数生成的基本原理,也明白了为什么需要引入time.h头文件。

多文件编程也是这次实践的重要收获。把函数声明放在game.h中,函数实现放在game.c中,主逻辑放在test.c中,通过#include "game.h"来关联。这种模块化的组织方式让代码结构清晰,也便于复用和维护。我体会到,当代码量变大时,合理的文件划分能显著提升可读性和开发效率。

总的来说,这次写扫雷游戏让我把课堂上学到的数组、函数、循环、多文件编程等知识真正用了起来,也踩了不少坑。但正是这些坑让我对C语言的内存管理和代码组织有了更具体的认识。后续还有一部分扫雷游戏的优化功能,我会继续学习并逐步完善,等实现后再和大家分享。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 3:32:34

13 Validation校验在springboot中使用

1 引入依赖 Spring Boot 3.x 也使用这个 starter&#xff0c;但包名迁移到了 jakarta.validation。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId> </dependency>2…

作者头像 李华
网站建设 2026/10/14 3:32:04

强化学习的动态规划

一、动态规划的介绍1、动态规划动态规划 (Dynamic programming, DP)是一类优化算法动态规划将待求解问题分解成若干子问题&#xff0c;先求解子问题&#xff0c;然后从这些 子问题的解得到目标问题的解。2、动态规划与强化学习的关系在完备的马尔可夫决策过程中&#xff0c;DP可…

作者头像 李华
网站建设 2026/10/14 3:31:51

移动端射击游戏性能优化实战:帧率、渲染与手感调优全攻略

做移动端射击游戏&#xff0c;和做PC/主机射击游戏完全不是一个物种。去年我们团队把《三角洲行动》移动端的帧率、内存、发热和手感反复打磨了大半年&#xff0c;期间踩了大量文档里根本找不到的坑。这篇文章不讲教科书式的架构&#xff0c;而是把真机调试、压测和上线前优化过…

作者头像 李华
网站建设 2026/10/14 3:31:49

Codex Skill 入门与实战:安装、调用和自建

整理时间&#xff1a;2026-10-12 核心标签&#xff1a;#Codex #AgentSkills #AI编程 #效率工具 资料来源 [S1] 视频&#xff1a;《Codex Skill 到底是什么&#xff1f;10分钟入门使用、安装、自建》&#xff0c;Bilibili。[S2] 官方文档&#xff1a;Build skills&#xff08;O…

作者头像 李华
网站建设 2026/10/14 3:31:06

airllm分层推理:低显存跑大模型的工程实践

1. 当显存告急时&#xff0c;我们还能不能跑大模型第一次在单卡24G显存上尝试加载一个70B参数级别的模型时&#xff0c;我盯着终端里那行红色的显存溢出报错看了很久。这不是什么罕见的场景——任何一个在消费级硬件上折腾过大模型推理的人&#xff0c;大概都经历过类似的时刻。…

作者头像 李华
网站建设 2026/10/14 3:31:04

3秒焦耳热合成Cu-N₃单原子催化剂,锌碘电池循环5万次

最近某高校团队发表了一项让我印象很深的工作&#xff1a;用焦耳热在3秒内合成出配位不饱和的Cu-N₃单原子催化剂&#xff0c;把它用在水系锌碘电池正极上&#xff0c;实现了50,000次超长循环。这个数据一出来&#xff0c;圈内不少人都挺振奋的。单原子催化剂虽然已经被追捧了很…

作者头像 李华