当前位置: 首页 > news >正文

C语言旅程之扫雷游戏(基础版讲解)

一、扫雷游戏介绍

《扫雷》是一款经典的逻辑益智单机游戏,1992 年随 Windows 3.1 系统推出后风靡全球,最初设计目的是帮助用户熟悉鼠标操作,如今仍是不少人休闲与锻炼逻辑思维的选择,以下是其详细介绍:

1. 界面元素

包含雷区、左上角的剩余地雷计数器和右上角的计时装置。雷区方格有未打开、已打开、插旗标记三种状态,已打开方格若显示数字,就代表其 3×3 范围内的地雷数量,显示空白则意味着周围无地雷。

2. 分级难度

常规分三个固定难度,具体如下:

  • 初级:9×9 方格配 10 颗地雷
  • 中级:16×16 方格配 40 颗地雷
  • 高级:16×30 方格配 99 颗地雷

此外还有自定义模式,可自主设定雷区大小和地雷数量,不过多数版本中雷区最大不超过 24×30。

3. 基础玩法

3.1 核心目标

在不触发地雷的前提下,尽快揭开所有安全方格,若误点地雷则游戏直接结束。

3.2 操作方式
  • 左键单击:用于揭开方格,若揭开的是空白方格,会连带打开相邻的安全方格
  • 右键单击:可给疑似地雷的方格插旗标记,再次点击能取消标记
  • 左右键双击:当数字周围标记的地雷数与数字一致时,双击该数字,可自动揭开其周围未打开的方格

二、代码实现过程用到的知识

(一)函数的运用

  1. 提升可读性:拆分功能为函数(如扫雷的雷区初始化、计时),逻辑清晰,凭函数名可懂用途,降低理解成本。
  2. 减少冗余:重复功能(如扫雷插旗逻辑)只需写一次函数,多次调用,避免重复编码,改代码时仅改对应函数即可。
  3. 易维护调试:问题可定位到具体函数(如雷区出错查初始化函数),不用全局找错;新增功能(如扫雷难度保存)只需加新函数,不影响旧代码。
  4. 方便协作:多人可分工写不同函数(一人做界面、一人处理操作),各模块独立测试,减少冲突,提高效率。
  5. 契合结构化设计:按 “自顶向下” 拆分复杂程序(如扫雷拆分为初始化 - 操作 - 判断结束),避免代码混乱。

(二)C 语言中函数、主程序、头文件各自独立成文件

  1. 函数独立成文件:实现功能模块化,如扫雷中将 “雷区初始化”“计算地雷数” 等函数分存,单个文件聚焦单一功能,便于定位修改逻辑,支持多人分工编写,减少代码冲突,且函数文件可在多项目中复用,提升开发效率。
  2. 主程序(main 函数)独立成文件:明确区分程序 “启动逻辑” 与 “功能实现”,如修改扫雷的启动流程或难度选择顺序时,无需改动其他功能文件,降低对整体代码的影响,也能让开发者快速识别程序核心执行脉络。
  3. 头文件独立成文件:可集中声明函数、宏定义等,如将扫雷各功能函数声明统一存放,其他源文件通过 #include 引入即可调用,避免重复声明,保证接口一致性;同时隐藏函数实现细节,仅暴露调用接口,简化调用逻辑,方便统一维护接口规范。

三、扫雷游戏 C 语言实现过程

(一)游戏选择界面实现

首先制作游戏选择界面,判断用户是否要玩游戏,可用 do-while 以及 switch 函数实现,1 代表要玩,0 代表退出,以下是程序的入口:

int main() { int input = 0; do { Interface(); printf("请输入数字1或0\n"); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("结束游戏\n"); break; default: printf("输入错误,请重新输入\n"); } } while (input); return 0; }

其中选择界面函数如下:

void Interface() //游戏开始界面 { printf("* * * * * * * * * * *\n"); printf("* * * * * * * * * * *\n"); printf("* * * 1. play * * *\n"); printf("* * * 0. exit * * *\n"); printf("* * * * * * * * * * *\n"); printf("* * * * * * * * * * *\n"); }

(二)棋盘设计与相关问题解决

以基础版 9×9 棋盘为例,先思考以下问题并解决:

思考问题
  1. 如何用二维数组制作棋盘并存放数据,用 1 代表雷,0 代表不是雷?
  2. 不仅要生成存放数据的界面,还要展示给玩家,该如何处理?
  3. 是否要用整型 1 和 0 来代表是否是雷,有没有更好的解决办法?
  4. 9×9 的棋盘对于边界数据(最外层的雷)怎么判断?
问题解答
  1. 可生成两个棋盘,一个为游戏界面(向玩家展示),用 * 表示棋盘,采用 char 类型的二维数组;另一个为数据棋盘,存放雷的位置信息。
  2. 向玩家展现的棋盘用 char 类型,数据棋盘也用 char 类型,避免单独为整型数据写一个数组,简化代码。
  3. 对于边界数据判断问题,可将棋盘扩大到(1+9+1)×(1+9+1),即 11×11 的棋盘,初始化棋盘时边界也初始化为‘0’,统计时可正常统计,不影响雷的数量且化繁为简;向玩家显示棋盘时,选择打印 9×9 的棋盘即可。
相关代码实现

宏定义棋盘规格

//棋盘规格 #define horizontal 9 #define vertical 9 //存放规格(扩大后的棋盘) #define horizontals vertical + 2 #define verticals vertical + 2

定义棋盘数组

char content[verticals][horizontals];//存放雷的数据 char show[verticals][horizontals];//生成界面

初始化棋盘

//初始化棋盘(埋雷) //content开始全是“0” //show开始全是“*” Initialization(content, verticals, horizontals, '0'); //数据棋盘传入‘0’ Initialization(show, verticals, horizontals, '*'); //显示棋盘传入‘*’

显示棋盘

//棋盘显示 //Desplaygame(content, vertical, vertical);//数据(前期打印出数据,方便调试) Desplaygame(show, vertical, horizontal);//游戏界面

布置雷与排查雷

//布置雷 Set(content, vertical, horizontal); //排查雷 Find(content, show, verticals, horizontals);

(三)初始化棋盘函数

void Initialization(char Ip[verticals][horizontals], int c, int v, char set) //设置棋盘 { int i = 0; for (i = 0; i < verticals; i++) { int j = 0; for (j = 0; j < horizontals; j++) { Ip[i][j] = set; } } }

这时,我们发现坐标输入时要自己数,很麻烦,那么怎么优化一下?

显而易见,需要我们加上坐标轴.

(四)棋盘显示函数(含坐标轴优化)

最初的棋盘显示函数未加坐标轴,用户输入坐标时需自行计数,较为麻烦,优化后添加坐标轴的完整代码如下:

void Desplaygame(char Ip[verticals][horizontals], int row, int col) //棋盘显示 { int i = 0; printf("------扫雷游戏------\n"); for (i = 0; i <= col; i++) //打印横坐标 { printf("%d ", i); } printf("\n"); for (i = 1; i <= row; i++) { int j = 0; printf("%d", i); //打印纵坐标 printf(" "); for (j = 1; j <= col; j++) { printf("%c ", Ip[i][j]); } printf("\n"); } }

(五)布置雷函数

  1. 定义雷的个数(当然后期可以增加数量,以及让用户自定义难度)

  2. 使用while循环语句,直到雷的个数减为零停止循环;

  3. 需要生成随机坐标(借助srand函数#include<stdlib.h>和time函数#include<time.h>,下文对这两个函数介绍)

const int EASY_COUNT = 10; //埋雷个数;

布置雷代码

void Set(char Ip[verticals][horizontals], int row, int col) //布雷 { //布置十个雷 int count = EASY_COUNT; while (count) { int x = rand() % row + 1; //1-9随机数(后文加解释) int y = rand() % col + 1; if (Ip[x][y] == '0') { Ip[x][y] = '1'; count--; } } }

随机数生成初始化在 main 函数内加上以下代码,确保每次运行程序生成不同的随机数序列:

srand((unsigned int)time(NULL));
  • 调用 time (NULL) 获取当前系统的秒级时间戳,将其强制转换为 unsigned int 类型后,作为 “随机数种子” 传入 srand 函数,初始化随机数生成器,确保后续调用 rand () 时,每次程序运行都能得到不同的随机数序列。

(六)排查雷函数

6.1 思考问题
  1. 如果踩到雷游戏直接结束,是否不妥?
  2. 如何计算周围雷的个数?
  3. 怎么判断玩家成功排雷?
6.2 问题解答
  1. 踩到雷后应打印出雷的数据棋盘,让玩家输得明白。
  2. 计算周围雷的个数有两种方法:一是把周围八个格子是‘1’的坐标相加;二是用循环(从 x-1 到 x+1,y-1 到 y+1)统计‘1’的个数,第二种方法中多加的中间格子无影响,因为若中间格子是‘1’,游戏早已结束。
  3. 判断玩家成功排雷:可计算玩家排查出的无雷数量,当排查无雷数量等于棋盘总坐标数减去雷的数量时,游戏成功完成(后续可优化游戏,改变判断条件)。
6.3 排查雷代码
void Find(char Ip[verticals][horizontals], char show[verticals][horizontals], int row, int col) { int x = 0; int y = 0; int win = 0; while (win < row * col - EASY_COUNT) { printf("请输入要排查的坐标例如1 3,中间空格:先排后列 >\n"); scanf("%d %d", &x, &y); if (x >= 1 && y >= 1 && x <= row && y <= col) { if (Ip[x][y] == '1') { printf("对不起,你踩雷了\n"); { Desplaygame(Ip, vertical, horizontal); break; } } else { //计算周围雷的个数 int count = GetIpCount(Ip, x, y); show[x][y] = count + '0'; Desplaygame(show, vertical, horizontal); //Desplaygame(Ip, vertical, horizontal);//---- win++; } } else { printf("输入不合法,重新输入 > \n"); } } if (win == row * col - EASY_COUNT) { printf("恭喜你,排雷成功\n"); Desplaygame(Ip, vertical, horizontal); } }

(七)统计雷的个数函数

7.1 方法一
int GetIpCount(char mine[verticals][horizontals], int x, int y) { return (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'); }
7.2 方法二
int GetIpCount(char mine[verticals][horizontals], int x, int y)//统计雷的数目 { char a = 0; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { if (mine[x + i][y + j] == '1') { a += 1; } } } return a; }

四、完整代码

(一)game.h(头文件)

#pragma once #define _CRT_SECURE_NO_WARNINGS #include<time.h> #include<stdio.h> #include<stdlib.h> //棋盘规格 #define horizontal 9 #define vertical 9 //存放规格 #define horizontals vertical + 2 #define verticals vertical + 2 //埋雷个数 const int EASY_COUNT = 10; void Initialization(char Ip[verticals][horizontals], int c, int v, char set);//生成界面 void Desplaygame(char Ip[verticals][horizontals], int row, int col);//打印棋盘 void Set(char Ip[verticals][horizontals], int row, int col);//布置雷 void Find(char Ip[horizontals][verticals], char show[horizontals][verticals], int row, int col);//排查雷

(二)game.c(函数实现文件)

#include"game.h" //统计雷的数目 int GetIpCount(char mine[verticals][horizontals], int x, int y) { char a = 0; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { if (mine[x + i][y + j] == '1') { a += 1; } } } return a; } //设置棋盘 void Initialization(char Ip[verticals][horizontals], int c, int v, char set) { int i = 0; for (i = 0; i < verticals; i++) { int j = 0; for (j = 0; j < horizontals; j++) { Ip[i][j] = set; } } } //棋盘显示 void Desplaygame(char Ip[verticals][horizontals], int row, int col) { int i = 0; printf("------扫雷游戏------\n"); for (i = 0; i <= col; i++) { printf("%d ", i); } printf("\n"); for (i = 1; i <= row; i++) { int j = 0; printf("%d", i); printf(" "); for (j = 1; j <= col; j++) { printf("%c ", Ip[i][j]); } printf("\n"); } } //布雷 void Set(char Ip[verticals][horizontals], int row, int col) { //布置十个雷 int count = EASY_COUNT; while (count) { int x = rand() % row + 1; int y = rand() % col + 1; if (Ip[x][y] == '0') { Ip[x][y] = '1'; count--; } } } //排查雷 void Find(char Ip[verticals][horizontals], char show[verticals][horizontals], int row, int col) { int x = 0; int y = 0; int win = 0; while (win < row * col - EASY_COUNT) { printf("请输入要排查的坐标例如1 3,中间空格:先排后列 >\n"); scanf("%d %d", &x, &y); if (x >= 1 && y >= 1 && x <= row && y <= col) { if (Ip[x][y] == '1') { printf("对不起,你踩雷了\n"); { Desplaygame(Ip, vertical, horizontal); break; } } else { //计算周围雷的个数 int count = GetIpCount(Ip, x, y); show[x][y] = count + '0'; Desplaygame(show, vertical, horizontal); //Desplaygame(Ip, vertical, horizontal);//---- win++; } } else { printf("输入不合法,重新输入 > \n"); } } if (win == row * col - EASY_COUNT) { printf("恭喜你,排雷成功\n"); Desplaygame(Ip, vertical, horizontal); } }

(三)test.c(主程序文件)

#include"game.h" //游戏开始界面 void Interface() { printf("* * * * * * * * * * *\n"); printf("* * * * * * * * * * *\n"); printf("* * * 1. play * * *\n"); printf("* * * 0. exit * * *\n"); printf("* * * * * * * * * * *\n"); printf("* * * * * * * * * * *\n"); } //游戏主题 void game() { char content[verticals][horizontals];//存放雷的数据 char show[verticals][horizontals];//生成界面 //初始化棋盘(埋雷) //content开始全是“0” //show开始全是“*” Initialization(content, verticals, horizontals, '0'); Initialization(show, verticals, horizontals, '*'); //棋盘显示 //Desplaygame(content, vertical, vertical);//数据 Desplaygame(show, vertical, horizontal);//游戏界面 //布置雷 Set(content, vertical, horizontal); //排查雷 Find(content, show, verticals, horizontals); } int main() { int input = 0; srand((unsigned int)time(NULL)); do { Interface(); printf("请输入数字1或0\n"); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("结束游戏\n"); break; default: printf("输入错误,请重新输入\n"); } } while (input); return 0; }

五、补充知识

(一)srand 函数与 time 函数(C 语言)介绍

1. 核心作用

两者配合使用的核心目的:生成真正的随机数(避免每次运行程序得到相同的随机序列),尤其适合扫雷等需要随机布置地雷的场景。

2. 各自介绍
(1)srand 函数(#include<stdlib.h>)
  • 功能:设置随机数生成器的 “种子”(随机数序列的起始值)。
  • 特性
    • 若种子固定(如 srand (10)),每次运行程序,rand () 生成的随机数序列完全相同(伪随机);
    • 无返回值,参数为 unsigned int 类型(种子值);
    • 需搭配 rand () 函数使用,且程序中通常只调用 1 次(多次调用会导致随机序列混乱)。
  • 示例:srand (123);(固定种子,随机序列固定)
(2)time 函数(#include<time.h>)
  • 功能:获取当前系统时间(从 1970 年 1 月 1 日 00:00:00 到当前的秒数),返回值为 time_t 类型(本质是整数)。
  • 特性
    • 参数通常传 NULL(表示直接返回当前时间);
    • 核心价值:时间时刻变化,返回值永远不同,可作为 srand 的种子,实现 “每次运行种子不同”。
  • 示例:time (NULL);(返回当前系统时间的秒数)
3. 扫雷场景核心用法(随机布置地雷)

通过 time (NULL) 获取动态种子,结合 srand 初始化随机数生成器,再用 rand () 生成随机坐标,布置地雷:

#include<stdlib.h> #include<time.h> int main() { // 初始化随机数种子(程序开头调用1次即可) srand((unsigned int)time(NULL)); // 强制转换time_t为unsigned int // 生成0~8的随机行、列坐标(适配9×9雷区) int row = rand() % 9; // rand()%n 生成0~n-1的随机数 int col = rand() % 9; // 用生成的坐标布置地雷(如mine[row][col] = '#') return 0; }
4. 关键注意事项
  • srand 只需调用 1 次,通常放在 main 函数开头,多次调用会导致随机数重复;
  • 必须包含对应头文件:stdlib.h(srand/rand)和 time.h(time);
  • 若不调用 srand,rand () 默认使用种子 1,每次运行随机序列完全相同。

(二)随机坐标生成代码解析(int x = rand () % row + 1; int y = rand () % col + 1;)

1. rand () 函数
  • 功能:生成 0~RAND_MAX(通常是 32767)的伪随机整数(需先通过 srand ((unsigned int) time (NULL)) 初始化种子,否则每次运行序列相同);
  • 头文件依赖:#include <stdlib.h>。
2. % row(取模运算)
  • 作用:限制随机数的上限,结果范围是 0~(row-1);
  • 示例:若 row=9,则 rand ()%9 会得到 0、1、2、…、8 中的随机数(取模后余数不会超过除数)。
3. + 1的作用
  • 作用:将随机数的范围整体上移 1,把 0~(row-1) 转为 1~row;
  • 示例:rand ()%9 +1 最终得到 1、2、…、9(即注释中 “1-9 随机数” 的实现)。
4. 扫雷场景的实际意义

若扫雷雷区是 9×9(行、列数均为 9),则:

  • row=9 时,x 生成 1~9 的随机行号;
  • col=9 时,y 生成 1~9 的随机列号;
  • 结合这两个坐标,可实现 “随机在雷区布置地雷”(如 mine [x][y] = '#')。
5. 关键注意
  • 需确保 row 和 col 是正整数(否则取模可能出错);
  • 若雷区坐标习惯从 0 开始(如 0~8),可去掉 + 1,直接写 rand ()% row;
  • 必须在调用 rand () 前初始化种子(srand ((unsigned int) time (NULL))),否则每次运行会生成相同的坐标(地雷位置固定)。

六、总结

1.继续扩展这个游戏

1.可以增加游戏难度,让玩家有选择(简单9*9棋盘,10个雷;中等16*16棋盘,40个雷;困难30*16棋盘,99个雷);

2.如果排查位置及周围没有雷,直接展开周围一片,提高游戏体验;

3.增加游戏功能,可以标记雷;

4.可以增加排雷时间显示。

2.作者寄语

亲爱的读者:

  • 衷心感谢你抽出宝贵时间阅读这份扫雷游戏的介绍与 C 语言实现指南!无论是出于学习 C 语言的需求、对经典游戏的热爱,还是对编程实践的探索,你的关注都是这份内容最珍贵的意义。
  • 在编程学习的道路上,每一次尝试、每一次调试都离不开耐心与坚持。希望这份教程能为你提供清晰的思路、实用的技巧,也能让你在实践中感受到编程的乐趣与成就感。愿你在后续的学习中,既能攻克技术难关,也能收获满满的知识与成长;愿每一段代码都能顺遂运行,每一个创意都能落地生根。
  • 再次感谢你的陪伴与支持!祝你学习进步、万事顺意,在编程的世界里不断探索新可能,收获属于自己的精彩!
http://www.jsqmd.com/news/1186152/

相关文章:

  • 如何快速上手SkinTokens-bf16:从零开始实现3D模型自动绑定的完整教程
  • 从零实现C语言Web服务器:深入解析Socket、HTTP与Linux网络编程
  • 2026福清市暴雨多发!楼顶阳光房阳台窗边漏水如何彻底解决? - 宅安选房屋修缮
  • AI驱动的跨境电商智能体集群解决方案
  • 2026 收钻石不踩坑,当场结算无套路|深圳本地6家钻石回收门店实测盘点 - 名奢变现站
  • 银行级多维聚合:生产环境下的pandas实战指南
  • C/C++服装销售管理系统:从零到一的项目实战指南
  • 智能化的用户体验创新方法
  • 从0到1部署SeedVC-MLX:Linux环境下的完整安装与运行指南 [特殊字符]
  • 本地大模型也能跑!SmartShell接入Ollama教程:离线使用AI命令生成
  • 告别原生终端:在Windows 11/10中配置XShell与WinSCP高效连接WSL2 Ubuntu
  • 深入理解Phi-3-mini-4k-instruct_rai_1.7.1_hybrid的架构设计:终极指南
  • 2026年7月最新天梭南京鼓楼吾悦广场维修保养服务电话 - 天梭服务中心
  • 芯片制造探秘:从硅石到晶圆的工艺全景与市场格局
  • 新手也能学会:用微信自带功能给照片加文字水印 - 软件工具教程方法
  • 多维聚合实战:从GROUP BY到立方体思维的数据操作
  • NV-KERMT-70M-v2代码实现详解:从SMILES解析到分子嵌入生成
  • 2026年保密单位私有化即时通讯横向对比 - IM软件测评
  • OpenViking 上下文数据库 | 08 - 会话如何变成长时记忆:Session 与 Compressor
  • 如何提升后端代码的可维护性:设计模式应用
  • DGRunkeeperSwitch动画优化:打造流畅滑动切换效果的终极指南
  • Bitters核心变量全解析:掌握Sass样式定制的黄金法则
  • 2026筑宅安|榆林卫生间漏水专业维修,解决墙面潮湿发霉、渗水到楼下难题 - 筑宅安
  • 2026年杭州江干区拱墅区家电维修业主真实体验分享 - 简单到家
  • Viola-Jones检测器(VJ)--- 从理论到实践的工程化拆解
  • 避免照片被盗用?2026年加水印的正确操作步骤 - 软件工具教程方法
  • 从配置到推理:SmolLM2-135M-Instruct_rai_1.7.1_hybrid模型参数详解与调优技巧
  • 专网环境下企业即时通讯要检查哪些边界 - 内网通讯研究
  • UniAR:统一视觉与文本建模的多模态大模型创新实践
  • Qwythos-9B-v2部署完全指南:本地、云端和Docker三种方案