小游戏:扫雷 (C语言实现扫雷的基本功能)
该程序分为三个文件:
1.game.h :包含头文件的引用、函数的声明和宏定义
2.game.c :包含各功能函数的具体实现
3.test.c :各功能函数的调用(程序的流程)
功能介绍:
1.初始化雷盘
2.打印雷盘
3.随机设置雷的分布
4.统计坐标位置周围的雷数
5.扩展式排雷
6.给所选坐标位置做标记
7.取消标记
8.第一次排雷不会被炸死
读者可以自己额外增加新功能,比如计时功能等。
作者提醒:
具体的内容都在代码注释里详细解释(我觉得是非常详细了)
当然,本人并不推荐这个程序中的注释风格,以后编代码千万别这么写注释
这样注释纯粹是为了初学者能轻松理解所有代码的作用
game.h
#ifndef __GAME_H__
#define __GAME_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define ROW 10//雷盘行数
#define COL 10//雷盘列数
#define ROWS ROW+2//数组(实际)行数
#define COLS COL+2//数组(实际)列数
#define EASY_COUNT 10//初级难度的雷数
void InitBoard(char board[ROWS][COLS], int row, int col, char set);//初始化雷盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);//打印雷盘
void SetBoard(char board[ROWS][COLS], int row, int col, int count);//布雷
int GetCount(char board[ROWS][COLS], int x, int y);//统计雷数
void expend(char board1[ROWS][COLS], char board[ROWS][COLS], int x, int y,int *num);//扩展式排雷
void sign(char board[ROWS][COLS], int x, int y);//标记猴头(雷)
void unsign(char board[ROWS][COLS], int x, int y);//取消标记
#endif//__GAME_H__
game.c
#include "game.h"
void InitBoard(char board[ROWS][COLS], int row, int col, char set)//初始化雷盘
{
memset(board,set,row*col*sizeof(board[0][0]));//利用memset初始化,memset用法自行看书学习
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)//打印雷盘
{
system("CLS");//每次打印雷盘之前清屏一次
int i = 0;
int j = 0;
printf(" ");//为了打印列坐标时对齐
for (i = 1; i <= row; i++)
printf("%d ", i);//打印列坐标1 2 3 4 5 6 7 8 9 10
printf("\n");
for (i = 1; i <=row; i++)
{
printf("%2d ", i);//打印行坐标1 2 3 4 5 6 7 8 9 10
for (j = 1; j <=row; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
void SetBoard(char board[ROWS][COLS], int row, int col,int count)//布雷
{
int x = 0;
int y = 0;
while (count)
{
x = rand() % row + 1;//保证行坐标在1到10之间
y = rand() % col + 1;//保证列坐标在1到10之间
if (board[x][y] == '0')//判断该位置是否布过雷
{
board[x][y] = '1';//‘1’代表有雷
count--;//布一次雷,雷数减一
}
}
}
int GetCount(char board[ROWS][COLS], int x, int y)//统计雷数
{
return board[x - 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1] +
board[x + 1][y] +
board[x + 1][y + 1] +
board[x][y + 1] +
board[x - 1][y + 1] - 8 * '0';//将该位置周围八个位置的情况(是否有雷)计数
}
void expend(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y, int *num)//扩展式排雷(递归)
{
int i = 0;
int j = 0;
if (board2[x][y]=='*')//如果该位置字符为'*',则该位置是未排过的,进行排雷
{
(*num)++;//排雷次数加一
int count = GetCount(board1,x,y);//统计该位置周围的雷数
if (count != 0)//如果该位置周围的雷数不为0
{
board2[x][y] = count + '0';//显示雷数,则该位置的字符不为'*',可避免下次排雷重复排到该位置
}
else//如果该位置的雷数为0,则向它周围八个位置扩展排雷
{
board2[x][y] = '0';//该位置的字符显示为‘0’
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (x + i >= 1 && x + i <=ROW && y + j >= 1 && y + j <=COL)//耗费我3个小时调试时间的不起眼的小错误!!!!!!!
{
if (i != 0 || j != 0)//避免重复排到自己
expend(board1, board2, x + i, y + j, num);
}
}
}
}
}
}
void sign(char board[ROWS][COLS], int x, int y)//用‘@’标记雷
{
if (board[x][y] == '*')
{
board[x][y] = '@';
}
}
void unsign(char board[ROWS][COLS], int x, int y)//取消标记
{
if (board[x][y] == '@')
{
board[x][y] = '*';
}
}
test.c
#include "game.h"
void game()
{
int x = 0;
int y = 0;
int win = 0;//判定输赢的排雷次数
int select = 0;//决定游戏功能的变量
char mine[ROWS][COLS] = { 0 };//存雷的数组(雷盘)
char show[ROWS][COLS] = { 0 };//展示的数组(雷盘)
InitBoard(show, ROWS, COLS, '*');//展示的数组(雷盘)初始化为'*'
InitBoard(mine, ROWS, COLS, '0');//存雷的数字(雷盘)初始化为'0'
SetBoard(mine, ROW, COL, EASY_COUNT);//布雷
DisplayBoard(show, ROW, COL);//打印雷盘
while (win<(ROW*COL-EASY_COUNT))//当排雷的次数不少于无雷格数(雷盘格数 减 雷数)时,停止排雷
{
printf("请选择功能:\n1 for 排雷;2 for 标记 3 for 取消标记 4 for 再来一局\n");
scanf("%d", &select);
fflush(stdin);//清空输入缓冲区,避免多输造成的影响
if (select == 1)//1 for 排雷
{
printf("请输入坐标:\n");
scanf("%d%d", &x, &y);
fflush(stdin);//清空输入缓冲区,避免多输造成的影响
if (x >= 1 && x <= ROW&&y >= 1 && y <= COL)//检验坐标是否合法
{
if (mine[x][y] == '1')//如果所选位置有雷,判断是否为第一次排雷
{
if (win != 0)//如果不是第一次排雷,宣布游戏结束
{
DisplayBoard(mine, ROW, COL);//排雷失败后打印一下雷的分布
printf("你被炸死了!\n");
return;
}
else//如果是第一次排雷,将这颗雷转移到其他位置,保证第一次不会排到雷
{
mine[x][y] = '0';
SetBoard(mine, ROW, COL, 1);
expend(mine, show, x, y, &win);
}
}
else//如果所选位置没有雷,进行扩展式排雷
{
expend(mine, show, x, y, &win);
}
DisplayBoard(show, ROW, COL);//打印排雷后的雷盘
}
else
{
printf("错误坐标:\n");
}
}
else if (select==2)//2 for 标记
{
printf("请输入坐标:\n");
scanf("%d%d", &x, &y);
fflush(stdin);//清空输入缓冲区,避免多输造成的影响
sign(show,x,y);
DisplayBoard(show,ROW,COL);
}
else if (select==3)//3 for 取消标记
{
printf("请输入坐标:\n");
scanf("%d%d", &x, &y);
fflush(stdin);//清空输入缓冲区,避免多输造成的影响
unsign(show, x, y);
DisplayBoard(show, ROW, COL);
}
else if (select==4)//4 for 结束游戏
{
return;
}
}
printf("排雷成功!\n");
}
void menu()
{
int num = 0;
srand((unsigned int)time(NULL));//产生随机种子,用于随机布雷
do
{
printf("*********1.play 0.exit**********");
printf("请选择:\n");
scanf("%d", &num);
switch (num)
{
case 1:
game();
break;
case 0:
break;
default:
printf("选择错误!\n");
break;
}
} while (num);
}
void test()
{
menu();
}
int main()
{
test();
return 0;
}
截图:
程序开始运行
排雷过程
标记雷
取消标记
扫雷失败
重新开始游戏
扫雷成功
心得体会:
这个扫雷可以算是个小小小小的项目,对现阶段的我来说还不算难,但过程并不轻松。
我针对其中一个bug进行解释说明,为什么说过程并不轻松?
该程序判定扫雷成功的方式是:
在expend函数中传形参win计数(每排一次雷,win自加一次,当win不小于无雷格数时游戏结束)
代码如下:
//调用排雷的功能函数,将win传给形参num
expend(mine, show, x, y, &win);
//功能函数的具体内容
void expend(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y, int *num)//扩展式排雷(递归)
{
int i = 0;
int j = 0;
if (board2[x][y]=='*')//如果该位置字符为'*',则该位置是未排过的,进行排雷
{
(*num)++;//排雷次数加一
int count = GetCount(board1,x,y);//统计该位置周围的雷数
if (count != 0)//如果该位置周围的雷数不为0
{
board2[x][y] = count + '0';//显示雷数,则该位置的字符不为'*',可避免下次排雷重复排到该位置
}
else//如果该位置的雷数为0,则向它周围八个位置扩展排雷
{
board2[x][y] = '0';//该位置的字符显示为‘0’
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (x + i >= 1 && x + i <=ROW && y + j >= 1 && y + j <=COL)//耗费我3个小时调试时间的不起眼的小错误!!!!!!!
{
if (i != 0 || j != 0)//避免重复排到自己
expend(board1, board2, x + i, y + j, num);
}
}
}
}
}
}
我程序中出现的bug是win计数出错(win总是大于实际排雷次数),
这个bug的具体错误耗费了我3个小时才找到,其实就是我把
if (x + i >= 1 && x + i <=ROW && y + j >= 1 && y + j <=COL)
写成了:
if (x + i >= 1 && x + i <ROWS && y + j >= 1 && y + j <COLS)
这样就导致排雷排到雷盘行和列的最边缘的位置时会出错
如图:
排雷应该在1到10坐标之间(即黑框里)进行,当我们定义i和j小于ROWS的时候,在这个程序中就是小于12,则坐标为0到11(即红框里).
简单来说,就是本应该只在黑框内排雷,但是却把最外层(非雷区)也排了,这样就会增加排雷次数,从而导致win的数值不正常,总是大于90(雷格数10x10-雷数10=90)
在这里看来,您可能会想,“这么简单的bug你竟然花了3个小时!啧啧啧。。。”
其次,我觉得,真的是“当局者迷”,有时候自己真的很难发现自己的微不足道的错误=》小bug
唉。。。
正应了那句话,“程序员20%的时间花在编代码上,80%的时间花在调试上”。
OK!在此,我建议和我一样初学者们,在对一个程序编写之前,首先分析整个程序的流程和功能, 做到有模有块,如果脑海里实在理不出来步骤,不妨在纸上慢慢画出来或者写出来,
莫急于上手编代码!
最后,送给大家一句陈正冲先生的话,“调试代码才是最长水平的!”