目标
用C语言实现扫雷游戏(非界面,在控制台进行交互),随机选择位置点击,若该位置是雷,则被炸死,游戏失败;否则在该位置显示与该位置相邻的所有格子的雷的总数,直到排除了所有非雷的可能,游戏胜利。
设计思路
为了知道ROW行,COL列哪些格子被设置了雷,我们需要用二维数组把其位置存储起来,例如雷的位置存为1,否则为0。如果与某个位置相邻的所有格子的雷的总数为1,这两个1有歧义,因此需要分开存储(事实上布置雷的数组不能被打印在屏幕上)。一个二维数组mine存储哪些位置设置了雷,另一个二维数组show存储与每个位置相邻的所有格子的雷的总数。但是ROW*COL数组下标不能越界,一种方式是让行数和列数增大2使得不可能越界,或加上条件判断以节约空间。show若用0表示该位置尚未被排查,可能和每个位置相邻的所有格子的雷的总数相冲突,故用'*'来初始化未被排查的位置。把这两个数组都设置为字符数组以便后面调用同一函数。
游戏步骤
首先初始化两个字符数组,然后布置雷,接着打印初始化的show数组,最后排查雷。
以下分别是函数声明的function.h和游戏源文件Minesweeper.c。
C
#pragma once
#define COL 9
#define ROW 9
#define COLS (COL + 2)
#define ROWS (ROW + 2)
#define Mine_COUNT 10 //雷的个数
#include <stdio.h>
#include <stdlib.h> //包含rand函数和srand函数
#include <time.h> //包含time函数
void Init_Board(char board[][COLS], int, char);
void Display_Board(char board[][COLS], int);
void Set_Mine(char mine[][COLS], int);
void Find_Mine(char mine[][COLS], char show[][COLS], int);
C
#define _CRT_SECURE_NO_WARNINGS
#include "function.h" 注意function.h不是库函数对应的头文件,自己创建的需要加""
void menu()
{
printf("-------- 1. play --------\n");
printf("-------- 0. exit --------\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
Init_Board(mine, ROWS, '0');
Init_Board(show, ROWS, '*');
Set_Mine(mine, ROW);
Display_Board(show, ROW);
Find_Mine(mine, show, ROW);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
break;
default:
printf("选择错误");
break;
}
}
while (input);
return 0;
}
具体函数实现
初始化数组函数Init_Board
设计思路:show和mine数组初始化不同,一个用'0'初始化,一 个用'*'初始化,为了让函数知道初始化的字符是什么,把初始化的字符作为函数参数传入函数。注意二维数组arr以一维数组arr0,arr1,...为元素连续存储,故arrij即*(*(arr+i)+j),*表示取对应地址的元素,arr+i即&arr0+i*sizeof(arr0),数组名arr是首元素arr0的地址,丢失了行数信息,故sizeof(arr0)即列数不可省略,行数是防止越界必须传入。
C
void Init_Board(char board[][COLS], int rows,char init_char)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < COLS; j++)
{
board[i][j] = init_char;
}
}
}
打印数组函数Display_Board
C
void Display_Board(char board[][COLS], int row)
{
int i = 0;
for (i = 0; i <= COL; i++)
{
printf("%d ", i); //打印列号,从0开始是因为第1列是行号
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i); //打印行号
int j = 0;
for (j = 1; j <= COL; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
printf("\n");
}
注意这里必须用原始非扩大的ROW传入,我们不需要打印ROW之外的格子,但数组类型固定,不能更改。
设置雷函数Set_Mine
C
void Set_Mine(char mine[][COLS], int row)
{
int count = Mine_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % COL + 1;
if (mine[x][y] == '0') //防止重复设置地雷
{
mine[x][y] = '1';
count--;
}
}
}
这里要小心不能重复设置雷。
获取雷的数量Get_Mine_Count
C
static int Get_Mine_Count(char mine[][COLS], int x, int y,int row)
{
int i = 0;
int j = 0;
int count = 0;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
int nx = x + i;
int ny = y + j;
if (nx > 0 && nx <= row && ny > 0 && ny <= COL)
count += (mine[nx][ny] - '0');
}
}
return count;
}
最好需要检查是否越界,当然外面我们已知初始化为'0',不检查不会有影响。注意任何数字字符-'0'是ASCII码值运算,返回数字本身。注意minexy='0',加上不影响结果。在想不到思路时,给行和列编号发现规律,采用循环可能解决问题。相加是因为只有0,1两种结果,有多少个1就说明有多少个雷。
排雷函数Find_Mine
C
void Find_Mine(char mine[][COLS], char show[][COLS], int row)
{
int x = 0;
int y = 0;
int win_frequency = 0; //成功次数
int first_click = 1; //标记是否是第一次点击
time_t start = time(NULL);
while (win_frequency < row * COL - Mine_COUNT)
{
printf("输入坐标x y\n");
scanf("%d%d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= COL)
{
if (show[x][y] == '*')
{
if (first_click && mine[x][y] == '1')
{
mine[x][y] = '0';
int nx = 0;
int ny = 0;
do
{
nx = rand() % row + 1;
ny = rand() % COL + 1;
}
while (mine[nx][ny] == '1' || (nx == x && ny == y));
mine[nx][ny] = '1';
} //防止第一次就碰到雷
first_click = 0;
if (mine[x][y] == '0')
{
Expand_Blank(mine, show, x, y, row, &win_frequency);
Display_Board(show, row);
time_t now = time(NULL);
double cost = difftime(now, start);
printf("成功,当前已耗时%.0lf秒\n", cost);
}
else
{
time_t end = time(NULL);
double total = difftime(end, start);
printf("游戏结束,总耗时%.0lf秒\n",total);
Display_Board(mine, row);
return;
}
}
else
printf("重复输入");
}
else
printf("重新输入");
}
if (win_frequency == row * COL - Mine_COUNT)
{
time_t end = time(NULL);
double total = difftime(end, start);
printf("成功,总耗时%.0lf秒\n", total);
}
}
功能:实现第一次不可能就输:若尚未排查且是第一次排雷,把该位置的雷取消,且随机生成一个新的非雷的位置,把其设为雷。注意必须是新的位置,否则跟不改没区别。若本次排查不是雷,则递归扩展空白,直到成功次数达到格子总数去掉雷的数量。
扩展空白函数Expand_Blank
这个函数的功能是扩展空白直到边界的某个格子周围有雷,采用递归实现
C
void Expand_Blank(char mine[][COLS], char show[][COLS], int x, int y, int row, int* win)
{
if (x < 1 || x > row || y < 1 || y > COL)
return;
if (show[x][y] != '*')
return;
int cnt = Get_Mine_Count(mine, x, y,row);
show[x][y] = (char)(cnt + '0');
(*win)++;
if (cnt > 0)
return;
int i = 0;
int j = 0;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (i || j)
Expand_Blank(mine, show, x + i, y + j, row, win);
}
}
}
注意我们需要mine数组获取每个格子周围雷的个数并且修改show数组,因此需要传递mine和show,这里要检查边界,因为递归范围无法预料!这里必须传址调用(把地址作为参数),尽管地址是值传递,但对应变量不是。因为需要修改win即成功次数的值,每次翻开格子加1。注意cnt+'0'是整型,需强制类型转换为char类型,因为show是字符数组。递归调用Expand_Blank时注意win是指针即地址,不能再取地址。
当然我们知道递归若是层次太深,速度很慢,且空间消耗极大,因为递归需要回到最初的起点,然后才能一步一步返回,每次递归都需要额外开辟空间。