文章目录
一、前言
对于编程初学者而言,掌握指针与数组的基础知识是构建复杂程序不可或缺的基石。若您对这些核心概念尚存疑虑,强烈推荐您通过以下链接进行深入学习,以巩固基础:传送门。
若您已对指针与数组有了较好的理解,但对于如何生成随机数仍感陌生,那么探索以下猜数字游戏的实现过程将是极佳的学习机会:传送门。
二、游戏思路
回溯至我们童年时期的经典游戏 —— 扫雷:游戏原型。
- 游戏的核心目标在于找出并准确标记所有不含地雷的格子,同时确保自身安全,避免触雷。
- 游戏区域内的每个格子均藏有玄机,其显示的数字代表该格子周围8个相邻格子中地雷的总数。
- 特别规则:玩家的首次点击将确保安全,绝不会遭遇地雷。
- 若不幸选中了地雷格子,则宣告游戏立即结束。
- 若玩家选中的是无地雷且未显示数字的格子,则系统会根据规则自动展开其周围的安全区域,直至遇到含数字的格子为止。
三、游戏方法
1、初始化游戏区域:
- 创建一个二维数组作为游戏区域的模型,初始时,所有格子均处于未探索状态。
- 等待玩家首次操作完成后,系统将在游戏区域内随机布置地雷,增添游戏挑战性。
2、用户交互:
- 当玩家在游戏区域内选择某个格子时,系统将即时响应其操作。
- 若所选格子暗藏地雷,则立即显示“游戏结束”的提示信息,并高亮显示所有地雷位置,让玩家一目了然。
- 若所选格子安全无虞,则显示其周围地雷的数量,并依据游戏规则自动展开周围的安全区域,提升游戏体验。
3、输赢条件:
- 胜利条件:当游戏区域内所有非地雷格子均被玩家成功翻开时,即宣布玩家取得胜利。
- 失败条件:一旦玩家不慎翻开地雷格子,则游戏立即结束,宣告玩家失败。
四、核心方法说明
1、初始化游戏
/*
* 初始化扫雷游戏
* @param mine 指向存储游戏棋盘的指针的指针
* @param row 棋盘的行数(不包括边界)
* @param col 棋盘的列数(不包括边界)
*/
void init_mine(char*** mine, int row, int col)
{
//分配内存
*mine = (char**)malloc((row + 2) * sizeof(char*));//多开两个,防止越界
if (*mine == NULL)
{
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < (row + 2); i++)//多开两个,防止越界
{
(*mine)[i] = (char*)malloc((col + 2) * sizeof(char));
if ((*mine)[i] == NULL)
{
fprintf(stderr, "内存分配失败\n");
//释放之前已分配的内存
for (int j = 0; j < i; j++)
{
free((*mine)[j]);
}
free(*mine);
exit(EXIT_FAILURE);
}
}
//初始化
for (int i = 0; i < (row + 2); i++)
{
for (int j = 0; j < (col + 2); j++)
{
if (i == 0 || i == row + 1 || j == 0 || j == col + 1)
(*mine)[i][j] = '0';
else
(*mine)[i][j] = '#';
}
}
}
2、销毁棋盘
/*
* 销毁扫雷游戏棋盘
* @param mine 指向存储游戏棋盘的指针的指针
* @param row 棋盘的行数(不包括边界)
* @param col 棋盘的列数(不包括边界)
*/
void destory_mine(char*** mine, int row, int col)
{
assert(mine != NULL);
for (int i = 0; i < (row + 2); i++)
{
free((*mine)[i]);
}
free(*mine);
*mine = NULL;
}
3、展示游戏
/*
* 显示扫雷游戏棋盘
* @param mine 游戏棋盘
* @param row 棋盘的行数(不包括边界)
* @param col 棋盘的列数(不包括边界)
* @param flag 游戏当前状态(0表示游戏未结束,非0表示游戏已结束)
*/
void show_mine(char** mine, int row, int col, int flag)
{
assert(mine != NULL);
for (int i = 0; i <= col; i++)//列号
{
printf("%02d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%02d ", i);//行号
for (int j = 1; j <= col; j++)
{
if (mine[i][j] == '*' && flag == 0)//雷
{
printf("## ");
}
else if (mine[i][j] == '*' && flag != 0)//雷
{
printf("\033[31m** \033[0m");
}
else if (mine[i][j] == '0')//已点击周围无雷
{
printf("\033[30;1m## \033[0m");
}
else if (mine[i][j] >= '1' && mine[i][j] <= '8')//已点击周围有雷
{
printf("\033[30;1m%02c \033[0m", mine[i][j]);
}
else//未点击
{
printf("## ");
}
}
printf("\n\033[0m");
}
printf("\n");
}
4、扩展非雷区
/*
* 计算指定位置周围的地雷数量
* @param mine 游戏棋盘
* @param row 棋盘行数(不包括边界)
* @param col 棋盘列数(不包括边界)
* @param x 当前位置的行号
* @param y 当前位置的列号
* @return 指定位置周围的地雷数量
*/
int count_mine(char** mine, int row, int col, int x, int y)
{
assert(mine != NULL);
assert(x >= 1 && x <= row && y >= 1 && y <= col);
return (mine[x-1][y] == '*') +
(mine[x+1][y] == '*') +
(mine[x][y+1] == '*') +
(mine[x][y-1] == '*') +
(mine[x-1][y-1] == '*') +
(mine[x+1][y+1] == '*') +
(mine[x+1][y-1] == '*') +
(mine[x-1][y+1] == '*');
}
/*
* 扩展安全区域(递归地显示周围无雷的格子)
* @param mine 游戏棋盘
* @param row 棋盘行数(不包括边界)
* @param col 棋盘列数(不包括边界)
* @param x 当前位置的行号
* @param y 当前位置的列号
*/
void expand_safe(char** mine, int row, int col, int x, int y)
{
assert(mine != NULL);
assert(x >= 1 && x <= row && y >= 1 && y <= col);
int count = count_mine(mine, row, col, x, y);
if (mine[x][y] != '#')
{
return;
}
else if (count != 0)
{
mine[x][y] = count + '0';
return;
}
else if (count == 0)
{
mine[x][y] = '0';
}
if (x - 1 >= 1)
expand_safe(mine, row, col, x - 1, y);
if (y - 1 >= 1)
expand_safe(mine, row, col, x, y - 1);
if (x - 1 >= 1 && y - 1 >= 1)
expand_safe(mine, row, col, x - 1, y - 1);
if (x + 1 <= row && y - 1 >= 1)
expand_safe(mine, row, col, x + 1, y - 1);
if (x + 1 <= row)
expand_safe(mine, row, col, x + 1, y);
if (y + 1 <= col)
expand_safe(mine, row, col, x, y + 1);
if (x + 1 <= row && y + 1 <= col)
expand_safe(mine, row, col, x + 1, y + 1);
if (x - 1 >= 1 && y + 1 <= col)
expand_safe(mine, row, col, x - 1, y + 1);
return;
}
5、判断游戏结束
/*
* 判断游戏是否结束
* @param mine 游戏棋盘
* @param row 棋盘行数(不包括边界)
* @param col 棋盘列数(不包括边界)
* @param x 当前选择的行号
* @param y 当前选择的列号
* @return -1 表示玩家踩雷,游戏结束;0 表示还有未探索的格子,游戏继续;1 表示所有无雷格子都已探索,玩家获胜。
*/
int is_win(char** mine, int row, int col, int x, int y)
{
assert(mine != NULL);
assert(x >= 1 && x <= row && y >= 1 && y <= col);
if (mine[x][y] == '*')
return -1;
else
expand_safe(mine, row, col, x, y);
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (mine[i][j] == '#')
return 0;
}
}
return 1;
}
6、游戏交互
/*
* 清除标准输入缓冲区中直到换行符的所有字符,并返回一个标志位。
* @return 如果缓冲区中有字符被清除,则返回1;否则返回0。
*/
int clear_stdin(void)
{
int flag = 0;
while (getchar() != '\n')
flag = 1;
return flag;
}
/*
* 运行扫雷游戏的主逻辑。
* @param mine 指向游戏棋盘的指针的指针。
* @param row 棋盘行数(不包括边界)。
* @param col 棋盘列数(不包括边界)。
* @param mine_num 雷的个数。
*/
void game(char*** mine, int row, int col, int mine_num)
{
assert(mine_num > 0 && mine_num < (row * col));
srand(time(NULL));
init_mine(mine, row, col);
int win = 0;
int count = 1;
while (1)
{
system("cls");
show_mine(*mine, row, col, win);
int x = 0, y = 0;
printf("请输入排雷点(x, y):");
scanf("%d,%d", &x, &y);
if (clear_stdin() != 0)
x = y = -1;
if (x < 1 || x > row || y < 1 || y > col)
{
system("cls");
printf("输入错误!\n");
clear_stdin();
system("cls");
continue;
}
if (count == 1)
set_mine(*mine, mine_num, row, col, x, y);
count++;
win = is_win(*mine, row, col, x, y);
if (win == -1)
{
system("cls");
printf("游戏结束!\n");
show_mine(*mine, row, col, win);
destory_mine(mine, row, col);
clear_stdin();
system("cls");
break;
}
else if (win == 1)
{
system("cls");
printf("游戏胜利!\n");
show_mine(*mine, row, col, win);
destory_mine(mine, row, col);
clear_stdin();
system("cls");
break;
}
}
}
五、游戏成果
1、效果展示
以下是我精心打造的扫雷游戏的效果展示:
从图中可以看到,游戏的基本功能已经全部实现,玩家可以愉快地进行扫雷挑战啦!🎮
2、源代码
想要更深入地了解这款扫雷游戏的实现细节吗?没问题!我已经将游戏的源代码完整地开源在了Gitee上:传送门。
欢迎大家前来查看和学习,你可以在这里找到游戏的完整实现代码,包括棋盘初始化、地雷布置、游戏交互逻辑等各个模块的代码实现。无论你是对扫雷游戏的算法感兴趣,还是想学习如何用C语言实现一个完整的游戏项目,这里都是你不容错过的好去处。快来一起探索和学习吧!📚👨💻