一、游戏设计分析
本次设计的扫雷游戏是展示一个 9 * 9 的棋盘,然后输入坐标进行判断,若是雷,则游戏结束,否则继续判断,直到排除所有雷,游戏胜利。然后选择退出或者重新开始游戏。
(1)游戏菜单界面实现
提供开始游戏和退出游戏两个选项,游戏结束后可以再次选择,直到退出游戏。
(2)游戏初始化
开始游戏后,初始化棋盘,随机设置地雷,然后显示棋盘。这里需要两个二维数组,一个用来显示棋盘,一个用来存储地雷的信息。
(3)开始扫雷
提示玩家输入坐标,然后检查坐标是否合法,最后判断位置是否为雷。若为雷,则游戏结束;若不为雷则计算周围地雷的个数,若周围地雷的个数不为 0,则显示将其显示在棋盘上;若周围地雷个数为 0,则使用刚才的方法继续检查周围的 8 个位置,以此类推,检查结束后把信息全部显示在棋盘上。然后继续下一次坐标输入,直到把所有的雷找出,游戏获胜。
上述需要计算输入坐标周围的的雷的个数,如果在该坐标在最边上一圈,那么会存在排查的坐标非法的情况。这里可以使用 11 * 11 的棋盘,然后通过把雷设置为字符 ‘1’,非雷设置为字符 ‘0’,当计算时把周围八个坐标相加然后减去 8 个字符 ‘0’ 即可。
(4)游戏结束或重新开始游戏
二、各个步骤的代码实现
1. 游戏菜单界面的实现
游戏菜单界面需要提供开始游戏和退出游戏两个选项,且玩完一局游戏之后可以选择再玩一局。在主函数使用一个循环来控制实现,如下:
(1)test.c 测试文件
// 头文件
#include "Mine.h"
int main()
{
// 所需变量
int select;
// 选择
do
{
// 菜单
menu();
// 输入
scanf("%d", &select);
// 判断
switch (select)
{
case 1:
printf("扫雷游戏\n");
//Minesweeper_game();
break;
case 0:
printf("游戏结束!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (select);
return 0;
}
(2)Mine.h 头文件
// 头文件
#include <stdio.h>
// 函数声明
// 菜单
void menu();
(3)Mine.c 函数实现文件
// 头文件
#include "Mine.h"
// 函数定义
// 菜单
void menu()
{
printf("*****************************************\n");
printf("************* 1. play *************\n");
printf("************* 0. exit *************\n");
printf("*****************************************\n");
}
(4)代码运行效果:
2. 游戏初始化
游戏初始化需要显示棋盘,然后随机设置雷。这里需要使用两个数组,一个数组用来显示棋盘,另一个数组用来存储雷的信息。为了方便计算,两个数组都采用 11*11 的字符数组,初始棋盘使用符号 ‘*’,地雷使用字符 ‘1’,非地雷使用字符 ‘0’。数组大小使用在头文件中定义的符号常量,设计一个初始化函数 InitBoard() 和设置雷函数 SetMine(),还有显示棋盘函数 PrintBoard()。
InitBoard() 函数可以一次性传两个数组,也可以通过参数传递设置的值。SetMine() 函数需要使用随机数。PrintBoard() 函数显示的时候可以在左边和上面加上行号和列号,方便玩家输入坐标。
(1)test.c 测试文件
添加了扫雷游戏函数 Minesweeper_game()
// 头文件
#include "Mine.h"
int main()
{
// 所需变量
int select;
// 选择
do
{
// 菜单
menu();
// 输入
scanf("%d", &select);
// 判断
switch (select)
{
case 1:
Minesweeper_game(); // 扫雷游戏
break;
case 0:
printf("游戏结束!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (select);
return 0;
}
(2)Mine.h 头文件
// 头文件
#include <stdio.h>
// 常量声明
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
// 函数声明
// 菜单
void menu();
// 初始化棋盘
void InitBoard(char Board[ROWS][COLS], int rows, int cols, char set);
// 设置雷
void SetMine(char Board[ROWS][COLS], int row, int col);
// 显示棋盘
void PrintBoard(char Board[ROWS][COLS], int row, int col);
// 扫雷游戏
void Minesweeper_game();
(3)Mine.c 函数实现文件
// 初始化棋盘
void InitBoard(char Board[ROWS][COLS], int rows, int cols, char set)
{
int i;
for (i = 0; i < rows; ++i)
{
int j;
for (j = 0; j < cols; ++j)
{
Board[i][j] = set;
}
}
}
// 设置雷
void SetMine(char Board[ROWS][COLS], int row, int col)
{
// 所需变量
int x, y; // 雷的坐标
int num = MINE_COUNT; // 雷的数量
// 设置雷
while (num)
{
// 随机设置坐标值
x = rand() % row + 1; // 11 * 11 实际坐标 1 - 9
y = rand() % col + 1;
// 检查坐标是否重复
if (Board[x][y] == '0')
{
Board[x][y] = '1';
--num;
}
}
}
// 显示棋盘
void PrintBoard(char Board[ROWS][COLS], int row, int col)
{
// 分界线
printf("-----------------------扫雷----------------------\n");
// 列号
printf(" ");
int j;
for (j = 0; j < col; ++j)
{
printf("%d ", j + 1);
}
printf("\n");
// 棋盘
int i;
for (i = 0; i < row; ++i)
{
// 列号
printf("%d ", i + 1);
for (j = 0; j < col; ++j)
{
printf("%c ", Board[i+1][j+1]);
}
// 下一行
printf("\n");
}
// 分界线
printf("-----------------------扫雷----------------------\n");
}
// 扫雷游戏
void Minesweeper_game()
{
// 创建两个棋盘
char board[ROWS][COLS];
char mine[ROWS][COLS];
// 初始化棋盘
InitBoard(board, ROWS, COLS, '*');
printf("初始化两个数组:\n"); // 删除
PrintBoard(board, ROW, COL); // 删除
InitBoard(mine, ROWS, COLS, '0');
PrintBoard(mine, ROW, COL); // 删除
// 设置雷
SetMine(mine, ROW, COL);
printf("雷的信息:\n"); // 删除
PrintBoard(mine, ROW, COL); // 删除
}
上述标记了删除的语句都是不需要的,这是作者写完代码用来检测的。
(4)代码运行效果
3. 开始扫雷
开始扫雷,设计一个函数 find(),要求玩家输入坐标,检查该坐标是否合法,然后判断该位置是否为雷,是雷则游戏结束,不是雷则使用函数 calc_mine() 计算周围一圈的雷的个数。如果周围一圈没有雷,则按照刚才的步骤检查周围一圈,实现函数 calc_mine() 递归。然后继续扫雷,直到找出所有的雷。
函数 calc_mine() 只在 find() 函数中使用,可以设置为 Mine.c 函数实现文件中的静态函数。判断游戏结束也需要使用一个变量,这里使用一个全局变量 REAMIN 来进行控制,当 REAMIN 的值为棋子总数减去雷数时,则排除所有雷,游戏结束。
代码测试时,可以一个一个功能进行测试,一个功能完成无误后再进入下一个。作者是全部测试好了才放上来的。
(1)Mine.h 头文件
// ...
// 找雷
void find(char Board[ROWS][COLS], int row, int col);
// 扫雷游戏
void Minesweeper_game();
(2)Mine.c 函数实现文件
// 计算周围的雷
void calc_mine(char Board[ROWS][COLS], char Mine[ROWS][COLS], int row, int col, int x, int y)
{
// 坐标非法或者已经排查或该位置为雷则退出
if (x < 1 || x > 9 || y < 1 || y > 9 || Board[x][y] != '*' || Mine[x][y] != '0')
return;
// 所需变量
int num = 0;
int i;
for (i = x - 1; i <= x + 1; ++i)
{
int j;
for (j = y - 1; j <= y + 1; ++j)
{
num += Mine[i][j];
}
}
// 加的是字符 '0' 所以要减去
num = num - 9 * '0';
// 排雷 +1
++REMAIN;
if (num)
{
Board[x][y] = num + '0';
}
else
{
Board[x][y] = ' ';
// 如果该坐标周围没雷,则对其周围八个坐标进行检查,形成递归
for (i = x - 1; i <= x + 1; ++i)
{
int j;
for (j = y - 1; j <= y + 1; ++j)
{
calc_mine(Board, Mine, row, col, i, j);
}
}
}
}
// 找雷
int find(char Board[ROWS][COLS], char Mine[ROWS][COLS], int row, int col)
{
// 所需变量
int x, y;
// 输入排查坐标
while (1)
{
printf("请输入排雷坐标:");
scanf("%d %d", &x, &y);
// 判断坐标是否合法
if (Board[x][y] != '*')
{
printf("该坐标已被排查!\n");
continue;
}
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (Mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
printf("以下是本局雷的信息\n");
return 0;
}
else
{
calc_mine(Board, Mine, row, col, x, y);
return 1;
}
}
}
}
// 扫雷游戏
void Minesweeper_game()
{
// 重置
REMAIN = 0;
// 创建两个棋盘
char board[ROWS][COLS];
char mine[ROWS][COLS];
// 初始化棋盘
InitBoard(board, ROWS, COLS, '*');
InitBoard(mine, ROWS, COLS, '0');
// 设置雷
SetMine(mine, ROW, COL);
int ret = 0;
while (REMAIN != ROW * COL - MINE_COUNT)
{
// 显示棋盘
PrintBoard(board, ROW, COL);
// 排雷
ret = find(board, mine, ROW, COL);
// 如果是雷,则显示雷的信息,游戏结束
if (!ret)
{
PrintBoard(mine, ROW, COL);
break;
}
}
// 判断
if (REMAIN == ROW * COL - MINE_COUNT)\
{
printf("恭喜你排雷成功,游戏结束!\n");
printf("以下是本局雷的信息:\n");
PrintBoard(mine, ROW, COL);
}
}
(3)代码运行效果:
下图是完整代码运行效果,一整局游戏的运行图:
三、完整代码
完整代码分为三个文件:头文件 Mine.h,测试文件 test.c,函数实现文件 Mine.c
头文件 Mine.h
#pragma once
// 头文件
#include <stdio.h>
#include <stdlib.h>
// 常量声明
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE_COUNT 10
// 函数声明
// 菜单
void menu();
// 初始化棋盘
void InitBoard(char Board[ROWS][COLS], int rows, int cols, char set);
// 设置雷
void SetMine(char Board[ROWS][COLS], int row, int col);
// 显示棋盘
void PrintBoard(char Board[ROWS][COLS], int row, int col);
// 找雷
int find(char Board[ROWS][COLS],char Mine[ROWS][COLS], int row, int col);
// 扫雷游戏
void Minesweeper_game();
测试文件 test.c
// 头文件
#include "Mine.h"
#include <time.h>
int main()
{
// 设置随机数种子
srand((unsigned)time(0));
// 所需变量
int select;
// 选择
do
{
// 菜单
menu();
// 输入
scanf("%d", &select);
// 判断
switch (select)
{
case 1:
Minesweeper_game(); // 扫雷游戏
break;
case 0:
printf("游戏结束!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (select);
return 0;
}
函数实现文件 Mine.c
// 头文件
#include "Mine.h"
// 全局静态变量
static REMAIN = 0;
// 函数定义
// 菜单
void menu()
{
printf("*****************************************\n");
printf("************* 1. play *************\n");
printf("************* 0. exit *************\n");
printf("*****************************************\n");
}
// 初始化棋盘
void InitBoard(char Board[ROWS][COLS], int rows, int cols, char set)
{
int i;
for (i = 0; i < rows; ++i)
{
int j;
for (j = 0; j < cols; ++j)
{
Board[i][j] = set;
}
}
}
// 设置雷
void SetMine(char Board[ROWS][COLS], int row, int col)
{
// 所需变量
int x, y; // 雷的坐标
int num = MINE_COUNT; // 雷的数量
// 设置雷
while (num)
{
// 随机设置坐标值
x = rand() % row + 1; // 11 * 11 实际坐标 1 - 9
y = rand() % col + 1;
// 检查坐标是否重复
if (Board[x][y] == '0')
{
Board[x][y] = '1';
--num;
}
}
}
// 显示棋盘
void PrintBoard(char Board[ROWS][COLS], int row, int col)
{
// 分界线
printf("-----------------------扫雷----------------------\n");
// 列号
printf(" ");
int j;
for (j = 0; j < col; ++j)
{
printf("%d ", j + 1);
}
printf("\n");
// 棋盘
int i;
for (i = 0; i < row; ++i)
{
// 列号
printf("%d ", i + 1);
for (j = 0; j < col; ++j)
{
printf("%c ", Board[i+1][j+1]);
}
// 下一行
printf("\n");
}
// 分界线
printf("-----------------------扫雷----------------------\n");
}
// 计算周围的雷
void calc_mine(char Board[ROWS][COLS], char Mine[ROWS][COLS], int row, int col, int x, int y)
{
// 坐标非法或者已经排查或该位置为雷则退出
if (x < 1 || x > 9 || y < 1 || y > 9 || Board[x][y] != '*' || Mine[x][y] != '0')
return;
// 所需变量
int num = 0;
int i;
for (i = x - 1; i <= x + 1; ++i)
{
int j;
for (j = y - 1; j <= y + 1; ++j)
{
num += Mine[i][j];
}
}
// 加的是字符 '0' 所以要减去
num = num - 9 * '0';
// 排雷 +1
++REMAIN;
if (num)
{
Board[x][y] = num + '0';
}
else
{
Board[x][y] = ' ';
// 如果该坐标周围没雷,则对其周围八个坐标进行检查,形成递归
for (i = x - 1; i <= x + 1; ++i)
{
int j;
for (j = y - 1; j <= y + 1; ++j)
{
calc_mine(Board, Mine, row, col, i, j);
}
}
}
}
// 找雷
int find(char Board[ROWS][COLS], char Mine[ROWS][COLS], int row, int col)
{
// 所需变量
int x, y;
// 输入排查坐标
while (1)
{
printf("请输入排雷坐标:");
scanf("%d %d", &x, &y);
// 判断坐标是否合法
if (Board[x][y] != '*')
{
printf("该坐标已被排查!\n");
continue;
}
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (Mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
printf("以下是本局雷的信息\n");
return 0;
}
else
{
calc_mine(Board, Mine, row, col, x, y);
return 1;
}
}
}
}
// 扫雷游戏
void Minesweeper_game()
{
// 重置
REMAIN = 0;
// 创建两个棋盘
char board[ROWS][COLS];
char mine[ROWS][COLS];
// 初始化棋盘
InitBoard(board, ROWS, COLS, '*');
InitBoard(mine, ROWS, COLS, '0');
// 设置雷
SetMine(mine, ROW, COL);
int ret = 0;
while (REMAIN != ROW * COL - MINE_COUNT)
{
// 显示棋盘
PrintBoard(board, ROW, COL);
// 排雷
ret = find(board, mine, ROW, COL);
// 如果是雷,则显示雷的信息,游戏结束
if (!ret)
{
PrintBoard(mine, ROW, COL);
break;
}
}
// 判断
if (REMAIN == ROW * COL - MINE_COUNT)\
{
printf("恭喜你排雷成功,游戏结束!\n");
printf("以下是本局雷的信息:\n");
PrintBoard(mine, ROW, COL);
}
}
四、总结
本次扫雷游戏的代码实现,总体来说还比较可以。基本功能均实现,游戏也能正常进行。本游戏还有许多可以提升的地方,比如:代码的优化,雷的标记,难度的选择等等。
本代码稍微难一点的地方在于排查坐标周围地雷数的函数 calc_mine() 的递归,需要明确何时需要递归,和递归的结束条件。
复习一下递归需要满足两个条件:
(1)需要结束条件
(2)每次递归都更加靠近这个结束条件