目录
0. 前言
大家好!今天我们来学习如何用 C 语言编写一个简单的 扫雷游戏。这个程序将帮助你熟悉 C 语言中的数组、循环、条件判断、自定义函数以及如何进行用户输入输出。
1. 扫雷游戏介绍
扫雷游戏(Minesweeper)是一款经典的单人电脑益智游戏,最早出现在 1989 年随 Windows 3.0 系统发布。游戏的核心玩法是通过逻辑推理,避免点击到隐藏在格子下的地雷。
玩家的目标是打开所有没有地雷的方格,同时避免点击到包含地雷的格子。玩家可以通过数字提示来推测每个方格周围的地雷位置。
2. 简化版扫雷游戏概述
在这个扫雷游戏中,我们将为玩家提供一个 9x9(也可以修改为其他大小) 的游戏棋盘,玩家通过输入坐标来“访问”某个位置。如果该位置是地雷,游戏结束;如果不是,系统将显示该位置周围有多少个地雷。目标是清除所有非雷格子,并避免点击到地雷。
我们将分步实现:
- 初始化棋盘:创建两个棋盘,一个存储地雷信息,另一个用于显示给玩家。
- 设置地雷:随机生成地雷的分布。
- 显示棋盘:显示玩家的游戏界面。
- 查找地雷:用户选择一个位置,判断是否为地雷,并显示该位置周围地雷的数量。
- 判断胜负:判断是否全部非雷区域已被清除。
2.1 程序结构分析
我们的扫雷游戏程序主要由以下几个部分组成:
menu
:显示游戏菜单,选择是否开始游戏或退出。game
:管理整个游戏流程,包括设置雷数、初始化棋盘、设置地雷、显示棋盘、查找地雷等。InitBoard
:初始化棋盘,将每个格子填充为默认值。SetMine
:随机选择地雷的位置。DisplayBoard
:显示当前的棋盘状态。GetMineCount
:根据周围的格子,计算该位置周围有多少个地雷。FindMine
:让玩家输入坐标并查找该位置是否有地雷,更新棋盘并判断游戏是否结束。
2.1.1 程序流程图(概述)
-
开始
- 进入
main
函数。
- 进入
-
显示菜单
- 调用
menu()
,显示用户选择的选项(开始游戏或退出)。
- 调用
-
选择操作
- 用户输入选择,进入
operation()
。
- 用户输入选择,进入
-
选择开始游戏(当用户输入
1
)- 调用
game()
函数,进行游戏。 - 设置雷的数量(调用
settings()
)。
- 调用
-
初始化棋盘
- 调用
InitBoard()
初始化mine
(雷区)和show
(展示区)棋盘。
- 调用
-
设置地雷
- 调用
SetMine()
随机设置地雷位置。
- 调用
-
显示棋盘
- 显示初始化后的展示棋盘(
show
)不显示设置后的雷区棋盘(mine
)。
- 显示初始化后的展示棋盘(
-
开始寻找地雷
- 进入
FindMine()
,玩家输入坐标来查找地雷。
- 进入
-
检查游戏状态
- 如果玩家点击了地雷,游戏失败并显示评分。
- 如果清除的空格数达到目标,游戏胜利并显示评分。
-
退出或重新开始
- 用户选择退出游戏或重新开始(调用
clc()
清屏)
2.1.2 流程图
3. 程序教学
3.1 菜单创建
创建一个菜单展现进入退出游戏功能,这里主要使用的是do...while循环,当输入1时进入游戏,0时while(0)中0为假,结束循环。
打印菜单界面:
void menu()
{
//Display Option for Users(Menu)
printf("************************************\n");
printf("******* 1. PLAY *******\n");
printf("******* 0. EXIT *******\n");
printf("************************************\n");
}
以下为菜单界面:
do...while循环 + switch语句:
void operation()
{
srand((unsigned int)time(NULL));//随机数发生器的初始化函数,之后会用到 rand()
int select = 0;
do
{
menu();// 菜单选项
printf("Please Select the Option: >>> ");
scanf("%d",&select);
switch (select)
{
case 1:
printf("Minesweeper ---- GAME START !\n");
game();
break;
case 0:
printf("Game Exit\n");
break;
default:
printf("Not a Valid Option, Please Re-Select\n");
break;
}
} while (select);
}
int main()
{
operation();//调用operation自定义函数
return 0;
}
3.2 初始化棋盘
首先,我们需要为扫雷游戏创建一个棋盘。扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以我们需要⼀定的数据结构来存储这些信息。我们的第一个目标是n*n的最基础版扫雷,而创建棋盘就能非常容易联想到二维数组。所以,我们可以先创建一个个9*9的数组来存放信息。那如果这个位置布置雷,我们就存放1,没有布置雷就存放0
为了避免越界问题,我们在设计时将棋盘数组扩展为11×11,但实际雷区仍为中间的9×9区域,周围一圈不布置雷。这样可以轻松处理坐标边界问题。
当检查某个坐标(例如 (2,5))时,我们会访问其周围的8个位置,统计附近的雷数。如果某个位置(如 (8,6))靠近边界,数组扩展能防止越界。
为了避免混淆雷信息和统计数据,我们不在存储雷的数组中记录雷数,而是使用另一个数组存储排查后的雷数信息。这样可以方便区分和打印数据,用作排雷参考。
我们将棋盘分为两部分:一个是显示给用户看的棋盘(show
),另一个是存储地雷位置的棋盘(mine
)。
show
用来显示玩家的操作界面,初始时全部用 *
填充,表示还没有打开。
mine
用来存储地雷的位置,初始时用字符 0
(‘0’)填充,地雷位置用 字符1(‘1’)
标识。
void game()
{
char mine[ROWS][COLS] = { 0 };// operation
char show[ROWS][COLS] = { 0 };// user interface
// Initialization
InitBoard(mine, ROWS, COLS, '0'); // user COULD NOT see
InitBoard(show, ROWS, COLS, '*'); // user COULD see
}
这里我们可以通过两个for循环来填充元素,同时我们也可以通过定义ROW和COL的数值来设置一下棋盘的大小
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;
}
}
}
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
在设置完后,我们可以打印一下当前棋盘查看当前设置的情况。可以在打印的时候加上行列序号从而方便查看坐标。
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("---------Minesweeper---------\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");//打印换行
}
printf("-----------------------------\n");
}
DisplayBoard(show, ROW, COL);
DisplayBoard(mine, ROW, COL);
3.3 设置地雷
3.3.1 输入地雷数量
通过scanf函数输入地雷数量
void settings()
{
printf("Please Set the Number of Mine:\n");
scanf("%d", &mine_count);
}
settings();
3.3.2 设置地雷
地雷的设置是通过随机数生成的。我们会根据用户输入的雷数,在棋盘上随机放置地雷。
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = mine_count;//雷的数量
int x = 0;
int y = 0;
while (count)// the while loop will run 10 times at least
{
x = rand() % row + 1; //生成 1 到 row 的随机数
y = rand() % row + 1;//生成 1 到 col 的随机数
if (mine[x][y] != '1')// 如果该位置没有地雷
{
mine[x][y] = '1';// 设置为地雷
count--;
}
}
}
SetMine(mine, ROW, COL);
在 SetMine
函数中,首先使用 rand()
函数生成随机坐标 (x, y)
,然后检查该位置是否已经有地雷(即 mine[x][y]
是否为 '1'
)。如果没有地雷,我们就将该位置设置为 '1'
,表示地雷,并减少雷数。这个过程会一直持续,直到雷数设置完成。
3.3.3 显示棋盘
显示棋盘的函数会根据当前的状态将棋盘展示给玩家。玩家看到的棋盘是 show
数组,它的初始状态为 '*'
,当玩家点击某个位置时,系统会将该位置显示为数字,表示该位置周围的地雷数量。这里详细的流程图可以参考3.2初始化棋盘的末尾,已经讲过了所以就一笔带过。
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("---------Minesweeper---------\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");
}
printf("-----------------------------\n");
}
DisplayBoard
函数通过嵌套循环输出每个位置的内容。在此过程中,列号和行号都会显示出来,帮助玩家定位棋盘的位置。
DisplayBoard(show, ROW, COL);// 展现给用户的棋盘
DisplayBoard(mine, ROW, COL); // 仅供自己检查的棋盘(程序全部完成时此行代码须注释掉)
比如说我在此处设计的地雷数量为10,我们可以打印一下show数组和mine数组来检查一下展现给用户的界面和地雷实际的布置情况(“1”为雷)。在检查无误后,我们便可以进行下一步的开发。
左边是开发时方便我们自己查看的界面,右边是实际程序运行时用户看到的界面。
3.4 查找地雷
当玩家选择一个位置时,我们需要判断该位置是否为地雷。如果是地雷,游戏结束,给用户展示雷的排布情况;如果不是,我们计算该位置周围的地雷数量,并更新棋盘。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0;
int win = 0;
while (win < row * col - mine_count) {
//请输入要查看的坐标 (x y):
printf("Please Enter the Coordinate You Want to Look Up : >>> ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) {
if (mine[x][y] == '1') {
printf("BOOM !---YOU DIED---GAME OVER\n");//踩雷了!游戏结束。
break;
} else {
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, row, col);
win++;
}
} else {
printf("Coordinate NOT VALID ! \n");//坐标无效,请重新输入!
}
}
if (win == row * col - mine_count) {
printf("YOU WIN\n");//恭喜你,赢得了游戏!
}
}
FindMine(mine, show, ROW, COL);//Find Mine
在 FindMine
函数中,我们首先检查用户输入的坐标是否合法。如果合法且没有踩到地雷,我们就计算该位置周围的地雷数量,并更新 show
棋盘。
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y - 1]
+ mine[x - 1][y]
+ mine[x - 1][y + 1]
+ mine[x][y - 1]
+ mine[x][y + 1]
+ mine[x + 1][y - 1]
+ mine[x + 1][y]
+ mine[x + 1][y + 1]
- 8 * '0';
}
在这里,我们需要通过调用GetMineCount函数来计算周围地雷的个数。我们可以通过把排查的坐标表达为(x,y)通过对x,y进行加减运算,我们就可以得出所有相邻坐标的坐标表达式。通过将8个二维数组中的值相加再减去8个‘0’ ,我们就可以得到周围雷的个数。
看到这里可能有些朋友会对涉及到字符的运算感到困惑,在这里我来详细解释一下。字符串1(‘1’)的ASC II code值为49,字符串0(‘0’)的ASC II code值为48。假设我们排查的坐标周围有 3个雷,那意味着我们有3个字符1(‘1’)和5个字符0(‘0’),我们将它们相加,那也就意味着ASC II code 的值为3 * 49 + 5 * 48 = 387,此时我们再计算8个字符0的值 48 * 8 = 384 。我们用387 - 384 得到了 数值 3, 即为雷的个数。
由于x,y加减的数字范围均为[-1 1],我们在这里便可以通过循环来遍历每一个坐标,从而简化代码。
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
count += (mine[x + i][y + j] - '0');
}
}
return count;
}
每次用户打开一个位置后,我们都会检查是否胜利。如果玩家清除了所有非雷的格子,就表示游戏胜利,此时可以调用一下DisplayBoard来给用户展示一下雷的排布情况。
if (win == row * col - mine_count) {
printf("YOU WIN\n");//恭喜你,赢得了游戏!
DisplayBoard(mine, ROW, COL);//给用户展示一下雷的排布位置
}
4. 基础入门扫雷完整代码展示
operation.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
//Display Option for Users(Menu)
printf("************************************\n");
printf("******* 1. PLAY *******\n");
printf("******* 0. EXIT *******\n");
printf("************************************\n");
}
void settings()
{
printf("Please Set the Number of Mine:\n");
scanf("%d", &mine_count);
}
void game()
{
settings();
char mine[ROWS][COLS] = { 0 };// operation
char show[ROWS][COLS] = { 0 };// user interface
// Initialization
InitBoard(mine, ROWS, COLS, '0'); // user COULD NOT see
InitBoard(show, ROWS, COLS, '*'); // user COULD see
//Print Board
DisplayBoard(show, ROW, COL);
//DisplayBoard(mine, ROW, COL); // Display before set, check only
// Set Mine
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL); // Admin display only
//Find Mine
FindMine(mine, show, ROW, COL);
}
void operation()
{
srand((unsigned int)time(NULL));
int select = 0;
do
{
menu();// Option Selection
printf("Please Select the Option: >>> ");
scanf("%d",&select);
switch (select)
{
case 1:
printf("Minesweeper ---- GAME START !\n");
game();
break;
case 0:
printf("Game Exit\n");
break;
default:
printf("Not a Valid Option, Please Re-Select\n");
break;
}
} while (select);
}
int main()
{
operation();
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
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;
}
}
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("---------Minesweeper---------\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");
}
printf("-----------------------------\n");
}
// Setting Mine = Finding 10 coordinates randomly on the board.
// x: 1-9
// y; 1-9
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = mine_count;
int x = 0;
int y = 0;
while (count)// the while loop will run 10 times at least
{
x = rand() % row + 1; // If any number is divided by 9, the reminder == range [0,8]
y = rand() % row + 1;// 0 + 1 = 1 ; 8 + 1 = 9, Thus we could get 1 - 9 randomly
if (mine[x][y] != '1')// avoid duplicate
{
mine[x][y] = '1';// set mine (mine = '1')
count--;
}
}
}
// [x-1,y-1] [x-1,y] [x-1,y+1]
// [x,y-1] [x,y] [x,y+1]
// [x+1,y-1] [x+1,y] [x+1,y+1]
// '1' - '0' = 1 (ASCII code 49 - 48 = 1)
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
/*return mine[x - 1][y - 1]
+ mine[x - 1][y]
+ mine[x - 1][y + 1]
+ mine[x][y - 1]
+ mine[x][y + 1]
+ mine[x + 1][y - 1]
+ mine[x + 1][y]
+ mine[x + 1][y + 1]
- 8 * '0';*/
int count = 0;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
count += (mine[x + i][y + 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 - mine_count) // total space - num of mine ex: 9 * 9 -10
{
printf("Please Enter the Coordinate You Want to Look Up : >>> ");
scanf("%d %d", &x, &y);
if (x >= SC && x <= row && y >= SC && y <= col)// SC = 1 (start coordinate)
{
if (mine[x][y] == '1')//coord is mine
{
printf("BOOM !---YOU DIED---GAME OVER\n");
DisplayBoard(mine, ROW, COL);
break;
}
else //coord is not mine
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else // corrd out of board range
{
printf("Coordinate NOT VALID ! \n");
}
}
if (win == row * col - mine_count && mine_count < row * col)
{
printf("YOU WIN\n");
DisplayBoard(mine, ROW, COL);
}
}
game.h
#define _CRT_SECURE_NO_WARNINGS 1
#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
#define SC 1
//Initialization 11 * 11
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//Print Board 9 * 9
void DisplayBoard(char board[ROWS][COLS], int row, int col);
void SetMine(char mine[ROWS][COLS], int row, int col);
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
// int GetMineCount(char mine[ROWS][COLS], int x, int y);
5. 扫雷扩展(进阶)
正在更新中......