之所以打算将扫雷游戏再次实现一次,主要是先检验一下自己的C语言是否学得扎实。因为我清晰的记得我的老师对我们说过:“如果你学完C语言之后,能够不作任何参考的写出扫雷游戏,那说明你的C语言是学的不错的”。
而且我本人对自己的要求也比较苛刻,我对自己是否熟练的掌握一门编程语言的衡量标准是:
当然啦,我知道我现在还达不到这个水平,不过手撕个扫雷我觉得自己还是可以做到的,所以今天打算来挑战一下。
今天的扫雷要达成的目标:
1、有多种模式可选择:经典模式(棋盘规模8X8)、简单模式(棋盘规模9X9)、中等模式(棋盘规模16X16)、专家模式(棋盘规模20X20)、自定义模式(棋盘规模?X?)。而且自定义模式也可以自定义雷的个数。
2、如果出现一大片空间都没有雷,将会展开一大片坐标(就像网页上的一样)。
3、胜出或失败都会显示棋盘结果。
设计棋盘我用的是“整型二维数组”(其实是动态申请的空间)。原因是使用整型数据更加直观。
当一个坐标周围有1个雷的时候,我们就将此坐标的数据赋值为1,如果有3个雷就赋值为3,而如果这个坐标是雷,我们就用-2来表示:
而如果一个坐标为被排查过(就像上图蓝方框所示),我们用-1来表示。至于周围没有雷的坐标,我们同样将其赋值为0,但我们最终并不会显示个0出来,就像上图一样。
先弄个简易的菜单:
void menu() {
printf(" ________________________________________________________\n");
printf("| 欢迎来到扫雷游戏! |\n");
printf("|--------------------------------------------------------|\n");
printf("| 1. | 经典模式(8X8) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 2. | 简单模式(9X9) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 3. | 中等模式(16X16) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 4. | 专家模式(20X20) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 5. | 自定义模式(?X?) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 0. | 退出游戏 |\n");
printf("  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n");
}
菜单弄好了,我们就可以通过接收用户输入来选择模式了,因为是动态的,所以我们在设计模式时候只需要根据情况初始化行数row和列数col即可,最后再统一用malloc函数申请空间:
int main() {
int row = 0;
int col = 0;
int mines_count = 0;
int input = 0;
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input) {
case 1:
row = 8;
col = 8;
mines_count = 9;
break;
case 2:
row = 9;
col = 9;
mines_count = 10;
break;
case 3:
row = 16;
col = 16;
mines_count = 40;
break;
case 4:
row = 20;
col = 20;
mines_count = 80;
break;
case 5:
printf("请输入行数(7 <= row <= 20):>");
while (1) {
scanf("%d", &row);
if (row < 7 || row > 20) {
printf("输入行数超出范围,请重新输入:");
}
else {
break;
}
}
printf("请输入列数(7 <= col <= 20):>");
while (1) {
scanf("%d", &col);
if (col < 7 || col > 20) {
printf("输入列数超出范围,请重新输入:");
}
else {
break;
}
}
int max_count = (int)((row * col) * 0.7);
printf("请输入雷的个数(5 <= mines_count <= %d):>", max_count);
while (1) {
scanf("%d", &mines_count);
if (mines_count < 5 || mines_count > max_count) {
printf("输入雷数多少或过多,请重新输入:");
}
else {
break;
}
}
break;
case 0:
printf("已退出游戏……\n");
break;
default:
printf("输入有误,请重新输入……\n");
break;
}
if (input >= 1 && input <= 5) {
// 创建棋盘
int** board = (int**)malloc(row * sizeof(int*));
if (NULL == board) {
perror("main");
return -1;
}
int i = 0;
for (i = 0; i < row; i++) {
board[i] = (int*)malloc(col * sizeof(int));
if (NULL == board[i]) {
perror("main");
return -2;
}
}
}
} while (input);
}
至于雷数的限制,出于对难度的考虑,我们就让最低限度统一为最低5个,而关于上限,我电脑中的扫雷游戏的上限大概是最多到坐标总数的70%:
所以我们这也将上限设置为坐标总数的70%。
模式选定后我们紧跟着就要来创建我们的棋盘了,我们的棋盘board其实是一个二级指针,我们只需要为board申请row个int*类型大小的空间,然后再为每个board[i]申请col个int类型大小的空间。就可以模拟出一个整型二维数组了:
if (input >= 1 && input <= 5) { // 先要判断,才能创建
// 创建棋盘
int** board = (int**)malloc(row * sizeof(int*));
if (NULL == board) {
perror("main");
return -1;
}
int i = 0;
for (i = 0; i < row; i++) {
board[i] = (int*)malloc(col * sizeof(int));
if (NULL == board[i]) {
perror("main");
return -2;
}
}
}
初始化棋盘
我们将棋盘内的坐标全都初始化为-1,表示所有的坐标都未被访问(排查)过,这交给一个函数来完成即可:
// 初始化棋盘
void init_board(int** board, int row, int col) {
assert(board);
int i = 0;
int j = 0;
for (i = 0; i < row; i++) {
for (j = 0; j < col; j++) {
board[i][j] = -1;
}
}
}
布置雷的逻辑其实也并不难,我们只需要产生mines_count个有效的随机坐标,然后在该坐标中放入-2即可:
// 布置雷到棋盘,我们用-2表示雷
void set_mines(int** board, int row, int col, int mines_count) {
assert(board);
srand((unsigned int)time(NULL));
int i = 0;
int j = 0;
while (mines_count) {
i = (rand() % row);
j = (rand() % col);
if (board[i][j] == -1) {
board[i][j] = -2;
mines_count--;
}
}
}
打印期盼的时候,我们只需要通过判断棋盘board具体坐标的值,选择性的打印出一些符号即可,如果一个坐标为未被访问过(即坐标值为-1)我们就在屏幕上打印一个“■”,例如在游戏的最开始,打印是这样的:
如一个坐标被拍查了,且它周围的雷数大于0,那就显示对应的数字即可,如果雷数是0,那就显示一个符号“□”,例如游戏玩到一半的时候打印是这样的:
如果点到雷,那就被炸死了,就将所有的雷显示出来,我们用符号“※”来表示雷,但为被排查过的坐标还是不显示,例如:
如果扫雷胜利了,我们就在雷的坐标上放一个符号“★”,表示胜利的标志,例如:
然后还有一件需要做的事就是在棋盘的周围打印上坐标,我在搜狗的符号库里找了很久,最后决定使用以下符号:
但有一挺坏的消息是,这些字符都不在ASCLL码的范围内,所以只能够用printf函数直接打印,而不能够将其转化成字符类型打印:
后来我思来想去,最终想出了一个比较挫的动态打印方式:
// 打印坐标的函数
void print_coord(int i) {
switch (i) {
case 0:
printf("⑴");
break;
case 1:
printf("⑵");
break;
case 2:
printf("⑶");
break;
case 3:
printf("⑷");
break;
case 4:
printf("⑸");
break;
case 5:
printf("⑹");
break;
case 6:
printf("⑺");
break;
case 7:
printf("⑻");
break;
case 8:
printf("⑼");
break;
case 9:
printf("⑽");
break;
case 10:
printf("⑾");
break;
case 11:
printf("⑿");
break;
case 12:
printf("⒀");
break;
case 13:
printf("⒁");
break;
case 14:
printf("⒂");
break;
case 15:
printf("⒃");
break;
case 16:
printf("⒄");
break;
case 17:
printf("⒅");
break;
case 18:
printf("⒆");
break;
case 19:
printf("⒇");
break;
default:
break;
}
}
我知道这很麻烦,但这也只仅仅是为了美观,如果不想这样,直接打印数字当坐标也可以。
所以最终的打印函数实现如下:
// 打印棋盘
// 参数flag表示的是打印的状态,0表示正常打印,1表示胜利时的打印,-1则表示被炸死时的打印
void print(int** board, int row, int col, int flag) {
assert(board);
// 坐标数组,用于打印
int i = 0;
int j = 0;
// 打印边界(都是为了美观,其实可以忽略)
for (i = 0; i < col + 2; i++) {
if (0 == i) {
printf(" ");
}
printf("__");
}
printf("\n");
// 先打印列坐标
for (i = 0; i < col; i++) {
if (0 == i) {
printf("|");
printf("◤");
}
print_coord(i);
if (col - 1 == i) {
printf("◥");
printf("|");
}
}
printf("\n");
// 打印棋盘内容与行坐标
for (i = 0; i < row; i++) {
printf("|");
print_coord(i); // 首先打印上一个纵坐标
for (j = 0; j < col; j++) {
if (board[i][j] == -1) {
printf("■");
}
else if (board[i][j] == -2) {
if (0 == flag) {
printf("■");
}
else if (1 == flag) {
printf("★");
}
else {
printf("※");
}
}
else if (board[i][j] == 0) {
printf("□");
}
else {
printf("%2d", board[i][j]);
}
}
print_coord(i); // 后面再跟上一个纵坐标
printf("|");
printf("\n");
}
// 最底端也打印一排坐标
for (i = 0; i < col; i++) {
if (0 == i) {
printf("|");
printf("◣");
}
print_coord(i);
if (col - 1 == i) {
printf("◢");
printf("|");
}
}
printf("\n");
// 打印边界
for (i = 0; i < col + 2; i++) {
if (0 == i) {
printf(" ");
}
printf(" ̄");
}
printf("\n");
}
做好了以上的工作,我们就可以开始排查雷了:
void sweep_mines(int** board, int row, int col) {
assert(board);
int i = 0;
int j = 0;
while (1) {
printf("请输入你要排查的坐标(i,j) :>");
scanf("%d %d", &i, &j);
if ((i < 1 || i > row) || (j < 1 || j > col)) {
printf("输入的坐标非法,请重新输入……\n");
}
else if (board[i - 1][j - 1] == -2) {
printf(" ________________________\n");
printf(" |**********(>_<)*********|\n");
printf(" |***很遗憾,你被炸死了!***|\n");
printf("  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n");
print(board, row, col, -1);
system("pause"); // 让程序先暂停好查看结果
printf("正在返回主菜单……\n");
Sleep(3000);
system("cls");
return;
}
else if (board[i - 1][j - 1] == -1) {
// 统计坐标周围有多少个雷
count_mines(board, row, col, i - 1, j - 1);
if (is_wins(board, row, col)) {
printf(" ____________________________________\n");
printf(" |★★★★★★★(*^O^*)★★★★★★★ |\n");
printf(" |★★★恭喜你,排雷成功!太棒了!★★★ |\n");
printf("  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n");
print(board, row, col, 1);
system("pause");
printf("正在返回主菜单……\n");
Sleep(3000);
system("cls");
return;
}
else {
print(board, row, col, 0);
}
}
else {
printf("该坐标已被排查过,请重新输入……\n");
}
}
}
既然是要通过输入的坐标来排查,就一定要对坐标的有效性做检查,只有当坐标有效是才执行后面的操作,如果被排查到的坐标是雷的话,游戏就直接结束。如果不是就统计该坐标周围有多少个雷,这个操作交给一个函数count_mines来完成。并在每一次count_mines调用完后都判断游戏是否胜出,这个操作交给一个函数is_wins来完成。
如果我们想要向电脑上的扫雷游戏一样,排查一个坐标,有可能展开一片空白:
那就可以使用递归的方法实现。
统计雷数的主要思路是用一个特殊的循环来遍历一个坐标周围的坐标:
int count = 0;
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (0 == i && 0 == j) {
continue;
}
if (-2 == board[x + i][y + j]) {
count++;
}
}
}
置于为什么能做到,相信画个图大家就能够理解了:
而我们在遍历周围的这些坐标时,也一定不要忘了要对其合法性进行判断。这在后面进行递归时候也是一样的,所以我们完整的函数实现就如下所示:
// 统计坐标周围有多少个雷
void count_mines(int** board, int row, int col, int x, int y) {
assert(board);
int count = 0;
int i = 0;
int j = 0;
// 开始计算雷数
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
int X = x + i;
int Y = y + j;
if ((X < 0 || X > row - 1) || (Y < 0 || Y > col - 1)) { // 判断坐标的有效性
continue;
}
if (i == 0 && j == 0) {
continue;
}
if (-2 == board[X][Y]) {
count++;
}
}
}
board[x][y] = count;
if (0 == count) { // 当count等于0时,就要开始递归。
// 开始递归
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
int X = x + i;
int Y = y + j;
if ((X < 0 || X > row - 1) || (Y < 0 || Y > col - 1)) { // 判断坐标的有效性
continue;
}
if (i == 0 && j == 0) {
continue;
}
if (-1 == board[X][Y]) {
count_mines(board, row, col, X, Y);
}
}
}
}
}
最后剩下的就是判断是否胜出的函数了,其实这个函数也非常简单,我们只需要遍历整个棋盘,当在棋盘中再也找不到-1时,就表示所有非雷的坐标已经被我们排查完了,所以游戏胜出:
// 判断游戏是否胜利,判断是否胜利的思路很简单,棋盘中再也找不到值为-1的坐标时候,游戏就胜利
// 胜利返回1,否则返回0
int is_wins(int** board, int row, int col) {
assert(board);
int i = 0;
int j = 0;
for (i = 0; i < row; i++) {
for (j = 0; j < col; j++) {
if (-1 == board[i][j]) {
return 0;
}
}
}
return 1;
}
该文件存放各种函数的声明和头文件的包含:
#include
#include
#include
#include
#include
// 初始化棋盘
void init_board(int **board, int row, int col);
// 布置雷到棋盘
void set_mines(int **board, int row, int col, int mines_count);
// 打印棋盘
void print(int** board, int row, int col, int flag);
// 排查雷
void sweep_mines(int **board, int row, int col);
// 统计坐标周围有多少个雷
void count_mines(int **board, int row, int col, int x, int y);
// 判断游戏是否胜利
int is_wins(int** board, int row, int col);
该文件存放各种函数的定义:
#include "Minesweeping.h"
// 初始化棋盘
void init_board(int** board, int row, int col) {
assert(board);
int i = 0;
int j = 0;
for (i = 0; i < row; i++) {
for (j = 0; j < col; j++) {
board[i][j] = -1;
}
}
}
// 布置雷到棋盘,我们用-2表示雷
void set_mines(int** board, int row, int col, int mines_count) {
assert(board);
srand((unsigned int)time(NULL));
int i = 0;
int j = 0;
while (mines_count) {
i = (rand() % row);
j = (rand() % col);
if (board[i][j] == -1) {
board[i][j] = -2;
mines_count--;
}
}
}
// 打印坐标的函数
void print_coord(int i) {
switch (i) {
case 0:
printf("⑴");
break;
case 1:
printf("⑵");
break;
case 2:
printf("⑶");
break;
case 3:
printf("⑷");
break;
case 4:
printf("⑸");
break;
case 5:
printf("⑹");
break;
case 6:
printf("⑺");
break;
case 7:
printf("⑻");
break;
case 8:
printf("⑼");
break;
case 9:
printf("⑽");
break;
case 10:
printf("⑾");
break;
case 11:
printf("⑿");
break;
case 12:
printf("⒀");
break;
case 13:
printf("⒁");
break;
case 14:
printf("⒂");
break;
case 15:
printf("⒃");
break;
case 16:
printf("⒄");
break;
case 17:
printf("⒅");
break;
case 18:
printf("⒆");
break;
case 19:
printf("⒇");
break;
default:
break;
}
}
// 打印棋盘
// 参数flag表示的是打印的状态,0表示正常打印,1表示胜利时的打印,-1则表示被炸死时的打印
void print(int** board, int row, int col, int flag) {
assert(board);
// 坐标数组,用于打印
int i = 0;
int j = 0;
// 打印边界(都是为了美观,其实可以忽略)
for (i = 0; i < col + 2; i++) {
if (0 == i) {
printf(" ");
}
printf("__");
}
printf("\n");
// 先打印列坐标
for (i = 0; i < col; i++) {
if (0 == i) {
printf("|");
printf("◤");
}
print_coord(i);
if (col - 1 == i) {
printf("◥");
printf("|");
}
}
printf("\n");
// 打印棋盘内容与行坐标
for (i = 0; i < row; i++) {
printf("|");
print_coord(i); // 首先打印上一个纵坐标
for (j = 0; j < col; j++) {
if (board[i][j] == -1) {
printf("■");
}
else if (board[i][j] == -2) {
if (0 == flag) {
printf("■");
}
else if (1 == flag) {
printf("★");
}
else {
printf("※");
}
}
else if (board[i][j] == 0) {
printf("□");
}
else {
printf("%2d", board[i][j]);
}
}
print_coord(i); // 后面再跟上一个纵坐标
printf("|");
printf("\n");
}
// 最底端也打印一排坐标
for (i = 0; i < col; i++) {
if (0 == i) {
printf("|");
printf("◣");
}
print_coord(i);
if (col - 1 == i) {
printf("◢");
printf("|");
}
}
printf("\n");
// 打印边界
for (i = 0; i < col + 2; i++) {
if (0 == i) {
printf(" ");
}
printf(" ̄");
}
printf("\n");
}
// 统计坐标周围有多少个雷
void count_mines(int** board, int row, int col, int x, int y) {
assert(board);
int count = 0;
int i = 0;
int j = 0;
// 开始计算雷数
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
int X = x + i;
int Y = y + j;
if ((X < 0 || X > row - 1) || (Y < 0 || Y > col - 1)) { // 判断坐标的有效性
continue;
}
if (i == 0 && j == 0) {
continue;
}
if (-2 == board[X][Y]) {
count++;
}
}
}
board[x][y] = count;
if (0 == count) { // 当count等于0时,就要开始递归。
// 开始递归
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
int X = x + i;
int Y = y + j;
if ((X < 0 || X > row - 1) || (Y < 0 || Y > col - 1)) { // 判断坐标的有效性
continue;
}
if (i == 0 && j == 0) {
continue;
}
if (-1 == board[X][Y]) {
count_mines(board, row, col, X, Y);
}
}
}
}
}
// 排查雷
void sweep_mines(int** board, int row, int col) {
assert(board);
int i = 0;
int j = 0;
while (1) {
printf("请输入你要排查的坐标(i,j) :>");
scanf("%d %d", &i, &j);
if ((i < 1 || i > row) || (j < 1 || j > col)) {
printf("输入的坐标非法,请重新输入……\n");
}
else if (board[i - 1][j - 1] == -2) {
printf(" ________________________\n");
printf(" |**********(>_<)*********|\n");
printf(" |***很遗憾,你被炸死了!***|\n");
printf("  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n");
print(board, row, col, -1);
system("pause"); // 让程序先暂停好查看结果
printf("正在返回主菜单……\n");
Sleep(3000);
system("cls");
return;
}
else if (board[i - 1][j - 1] == -1) {
// 统计坐标周围有多少个雷
count_mines(board, row, col, i - 1, j - 1);
if (is_wins(board, row, col)) {
printf(" ____________________________________\n");
printf(" |★★★★★★★(*^O^*)★★★★★★★ |\n");
printf(" |★★★恭喜你,排雷成功!太棒了!★★★ |\n");
printf("  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n");
print(board, row, col, 1);
system("pause");
printf("正在返回主菜单……\n");
Sleep(3000);
system("cls");
return;
}
else {
print(board, row, col, 0);
}
}
else {
printf("该坐标已被排查过,请重新输入……\n");
}
}
}
// 判断游戏是否胜利,判断是否胜利的思路很简单,棋盘中再也找不到值为-1的坐标时候,游戏就胜利
// 胜利返回1,否则返回0
int is_wins(int** board, int row, int col) {
assert(board);
int i = 0;
int j = 0;
for (i = 0; i < row; i++) {
for (j = 0; j < col; j++) {
if (-1 == board[i][j]) {
return 0;
}
}
}
return 1;
}
该文件用来真正运行扫雷游戏:
#include "Minesweeping.h"
// 先写一个简易的菜单
void menu() {
printf(" ________________________________________________________\n");
printf("| 欢迎来到扫雷游戏! |\n");
printf("|--------------------------------------------------------|\n");
printf("| 1. | 经典模式(8X8) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 2. | 简单模式(9X9) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 3. | 中等模式(16X16) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 4. | 专家模式(20X20) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 5. | 自定义模式(?X?) |\n");
printf("|--------------------------------------------------------|\n");
printf("| 0. | 退出游戏 |\n");
printf("  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n");
}
int main() {
int row = 0;
int col = 0;
int mines_count = 0;
int input = 0;
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input) {
case 1:
row = 8;
col = 8;
mines_count = 9;
break;
case 2:
row = 9;
col = 9;
mines_count = 10;
break;
case 3:
row = 16;
col = 16;
mines_count = 40;
break;
case 4:
row = 20;
col = 20;
mines_count = 80;
break;
case 5:
printf("请输入行数(7 <= row <= 20):>");
while (1) {
scanf("%d", &row);
if (row < 7 || row > 20) {
printf("输入行数超出范围,请重新输入:");
}
else {
break;
}
}
printf("请输入列数(7 <= col <= 20):>");
while (1) {
scanf("%d", &col);
if (col < 7 || col > 20) {
printf("输入列数超出范围,请重新输入:");
}
else {
break;
}
}
int max_count = (int)((row * col) * 0.7);
printf("请输入雷的个数(5 <= mines_count <= %d):>", max_count);
while (1) {
scanf("%d", &mines_count);
if (mines_count < 5 || mines_count > max_count) {
printf("输入雷数多少或过多,请重新输入:");
}
else {
break;
}
}
break;
case 0:
printf("已退出游戏……\n");
break;
default:
printf("输入有误,请重新输入……\n");
break;
}
if (input >= 1 && input <= 5) {
// 创建棋盘
int** board = (int**)malloc(row * sizeof(int*));
if (NULL == board) {
perror("main");
return -1;
}
int i = 0;
for (i = 0; i < row; i++) {
board[i] = (int*)malloc(col * sizeof(int));
if (NULL == board[i]) {
perror("main");
return -2;
}
}
// 初始化棋盘,初始化全为-1,表示坐标全都未被访问过
init_board(board, row, col);
// 布置雷到棋盘
set_mines(board, row, col, mines_count);
printf("游戏开始!\n");
// 打印棋盘
print(board, row, col, 0);
// 开始扫雷
sweep_mines(board, row, col);
// 游戏结束,释放空间
for (int i = 0; i < row; i++) {
free(board[i]);
board[i] = NULL;
}
free(board);
board = NULL;
}
} while (input);
}