前置
函数的声明与定义
单个文件
我们初学者平常写代码的时候需要一个(.c)文件就够用了
#include <stido.h>
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
return 1;
else
return 0;
}
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
printf("闰年\n");
else
printf("⾮闰年\n");
return 0;
}
比如判断是否为闰年的情况,函数的定义在函数调⽤之前,确实没啥问题,可以运行。但是一旦将判断闰年函数放在主函数的后面,那么编译器会报警告!
为什么?我们可以这样理解,程序的主函数是入口,如果主函数前面没有一个“说明”,当我们在主函数里突然发现这个函数,编译器不认识,太突然了!如果在主函数前面加一个说明(声明),那么相当于提前给编译器打好招呼,熟悉之后就方便办事,因此,上述判断闰年函数在声明后就可以使用!
多个文件
多个文件。多了什么,为什么要多?
⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中。
game.h
主要放头文件,函数的声明,以及一些宏定义。
game.c
game.c与game.h这两个文件是配套的!game.c里面放的是函数的核心内容
test.c
里面放主函数,在主函数中引用我们所写的函数。
这样一个判断闰年的功能就很轻松的写完了。
注意一个细节,由于我在game.h里包含了stdio.h这个头文件,所以我在test.c里直接用game.h就能顺带使用stdio.h这个头文件里的函数。
扫雷游戏game.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/*实际游戏要用的数组范围*/
#define ROW 9//行
#define COL 9//列
/*为了显示边界位置周围雷的个数而扩大的数组*/
#define ROWS 11//行
#define COLS 11//列
#define MineCount 10
/*初始化棋盘*/
void setmine(char board[ROWS][COLS], int rows, int lows, char set);
/*布置棋盘*/
void SetMine(char board[ROWS][COLS], int row, int low);
/*打印棋盘*/
void PrintBoard(char board[ROWS][COLS], int row, int low);
/*排查雷*/
void InvestigateMine(char show[ROWS][COLS], char mine[ROWS][COLS], int cow, int low);
扫雷游戏test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf(" 请输入数据 》:\n");
printf("**************\n");
printf("****1.play****\n");
printf("****0.play****\n");
printf("**************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
/*PrintBoard(mine, ROW, LOW);*/
PrintBoard(show, ROW, COL);
SetMine(mine, ROW, COL);
InvestigateMine(show, mine, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned)time(NULL));
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
break;
default:
printf(" 请重新输入数据!");
}
} while (input);
return 0;
}
扫雷游戏game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
/*初始化棋盘*/
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
/*布置棋盘*/
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = MineCount;
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
int x = rand() % row + 1;
int y = rand() % col + 1;
for (j = 0; j < col; j++)
{
if (board[x][y] == '0')
{
board[x][y] = '1';
}
count--;
}
if (count == 0)
{
break;
}
}
}
/*打印棋盘*/
void PrintBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("--------扫雷游戏-------\n");
for (i = 0; i <= col; i++)
{
printf("%-2d", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%-2d", i);
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%-2c", board[i][j]);
}
printf("\n");
}
}
int CountMine(char mine[ROWS][COLS], int x, int y)
{
int a = mine[x - 1][y] + mine[x][y - 1] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] + mine[x][y + 1] + mine[x + 1][y] - 8 * '0';
return a;
}
/*排查雷*/
void InvestigateMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int win = 0;
int x = 0;
int y = 0;
while (win < row * col - MineCount)
{
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("踩到雷了!");
PrintBoard(mine, ROW, COL);
break;
}
else
{
int count = CountMine(mine, x, y);
show[x][y] = count + '0';
PrintBoard(show, ROW, COL);
win++;
}
}
else
{
printf("请重新输入坐标!\n");
}
if (win == row * col - MineCount)
{
printf("恭喜,排雷成功!\n");
PrintBoard(show, ROW, COL);
}
}
}
多个文件的目的,是为了提升代码设计的效率,所以我们以后会经常涉及这个知识。
扫雷游戏分析和设计
• 使⽤控制台实现经典的扫雷游戏
• 游戏可以通过菜单实现继续玩或者退出游戏
• 扫雷的棋盘是9*9的格⼦ • 默认随机布置10个雷
• 可以排查雷 ◦ 如果位置不是雷,就显⽰周围有⼏个雷 ◦ 如果位置是雷,就炸死游戏结束 ◦ 把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束。
这是程序设计的要求!!
菜单打印
要求
1.玩家可以通过选择1进入游戏,0退出游戏。
2.选错的话提醒玩家,重新选择。
我们的要求是不管程序是否顺利执行都要显示菜单,所以我们很自然的想到用do~while循环
void menu()
{
printf(" 请输入数据 》:\n");
printf("**************\n");
printf("****1.play****\n");
printf("****0.play****\n");
printf("**************\n");
}
int main()
{
int input = 0;
srand((unsigned)time(NULL));
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
break;
default:
printf(" 请重新输入数据!");
}
} while (input);
return 0;
}
解释
将写好的menu函数放到主函数里的do~while循环里就好。注意:这里的scanf函数必须要放到menu函数后面(因为编译器读到scanf时就会停下,等待用户输入),因此就不会先打印菜单了!
在主函数里放入switch~case语句,输入1进入游戏,输入0退出游戏(在退出switch语句的同时退出循环语句 ,这一点设计思路很重要但不难想)
初始化棋盘
要求
1. mine棋盘初始化为全‘0’。
2. show棋盘初始化为全‘1’。
void InitBoard(char board[ROWS][COLS], int rows, int cols,char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
解释
这个代码就是通过两个for循环讲棋盘规模进行搭建,之后给set不同的值,来实现初始化
我们都明白,开始要有一个专门的展示棋盘,输入数据后,我们要知道这个地方是不是雷,以及这个输入数据的位置周围有几个雷。如果只用一个棋盘,是不是不可能实现?
因此我们想到用两个数组。
一个是show数组
我们用 '*' 来当作展示给玩家的棋盘,玩家输入对应的坐标后便能显示出所需信息。
另一个是mine数组
这个数组内部随机生成10个雷,其全部内容只有在玩家排雷不幸遇到雷,并退出游戏时,才会展现给大家。
看到game.h时,本来我们需要9*9的棋盘,但是为什么我们还宏定义一个ROWS和COLS?
这是因为如果我们排雷时,输入棋盘边上的坐标时,会导致一部分位置出界,如果我们在额外在上下各开辟一行,左右各开辟一列,那么就变成11*11,将11*11-9*9的部分都初始化成字符0,就不会影响9*9的棋盘某个坐标周围雷的个数的信息。
最后的效果
有朋友很好奇这个随机数是怎么实现的,首先我们是吧mine数组里先全初始化为字符0,然后在通过之后的布置雷函数来实现,下面会详细说明。
布置棋盘
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = MineCount;
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
int x = rand() % row + 1;
int y = rand() % col + 1;
for (j = 0; j < col; j++)
{
if (board[x][y] == '0')
{
board[x][y] = '1';
}
count--;
}
if (count == 0)
{
break;
}
}
}
解释
MineCount是我们宏定义雷的个数,可以后期方便修改。
因为是棋盘内随机值,每一个数组内的元素都有可能是雷,所以我们定义x和y,将随机值的结果给x和y,每一次成功排雷,就减一个count,当10个雷布置完后 结束布置雷函数,从而实现布置雷。
if语句里的意思是要不同的10个雷位置,万一有两次随机数相等的情况,就会少布置一个雷。
随机数的实现知识
rand
C语言提供了一个函数叫rand,这函数是可以生成随机数的,函数原型如下:
int rand (void);
rand函数会返回一个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的大小是依赖编译器上实现的,但是大部分编译器上是32767。 rand函数的使用需要包含⼀个头文件是:stdlib.h
正因为这种伪随机数是依据算法来实现的,因此我们需要真正的随机数。
怎么做?
rand函数是对⼀个叫“种子”的基准值进行运算生成的随机数,rand函数生成随机数的默认种子是1。如果要生成不同的随机数,就要让种子是变化的。
srand
C语言中又提供了一个函数叫srand,用来初始化随机数的生成器的,srand的原型如下:
void srand (unsigned int seed);
程序中在调用rand函数之前先调用srand函数,通过srand函数的参数seed来设置rand函数生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。 那也就是说给srand的种子是如果是随机的,rand就能生成随机数;在生成随机数的时候又需要⼀个随机数,这就矛盾了。
所以这个时候我们要借助一个time函数。
time
在程序中我们一般是使用程序运行的时间作为种子的,因为时间时刻在发生变化的。 在C语言中有一个函数叫time,就可以获得这个时间,time函数原型如下:
time_t time (time_t* timer);
time函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒到现在程序运行时间之间的差值,单位是秒。返回的类型是time_t类型的,time_t类型本质上其实就是32位或者64位的整型类型。 time函数的参数timer如果是非NULL的指针的话,函数也会将这个返回的差值放在timer指向的内存 中带回去。 如果timer是NULL,就只返回这个时间的差值。time函数返回的这个时间差也被叫做:时间戳。 time函数的时候需要包含头文件:time.h
如果只是让time函数返回时间戳,我们就可以这样写:
time(NULL);//调⽤time函数返回时间戳
这个也是比较简单的,我们了解到这里就好
所以这三个头文件在随机数中经常用到
打印棋盘
void PrintBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("--------扫雷游戏-------\n");
for (i = 0; i <= col; i++)
{
printf("%-2d", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%-2d", i);
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%-2c", board[i][j]);
}
printf("\n");
}
}
这个没什么好说的,可能有时候不能一次性打印出自己想要的结果,可以边打印边修改。
强调一点,我们初始化用的是字符,因此我们要用%c来打印棋盘。
这个是我想要的打印效果,先把横纵坐标打印出来,再打印棋盘。
排查雷
要求
1.保证玩家输入的值合法在9*9范围内,包括边界。
2.标记时,统计正确标记雷的个数。
3.排雷时,玩家输入要排除的坐标,如果是雷,炸死,则游戏结束;如果不是雷,则统计周围雷的个数,如果排了71(row*col-MineCount)次还没炸,那么游戏结束。
int CountMine(char mine[ROWS][COLS], int x, int y)
{
int a = mine[x - 1][y] + mine[x][y - 1] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] + mine[x][y + 1] + mine[x + 1][y] - 8 * '0';
return a;
}
void InvestigateMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int win = 0;
int x = 0;
int y = 0;
while (win < row * col - MineCount)
{
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("踩到雷了!");
PrintBoard(mine, ROW, COL);
break;
}
else
{
int count = CountMine(mine, x, y);
show[x][y] = count + '0';
PrintBoard(show, ROW, COL);
win++;
}
}
else
{
printf("请重新输入坐标!\n");
}
if (win == row * col - MineCount)
{
printf("恭喜,排雷成功!\n");
PrintBoard(show, ROW, COL);
}
}
}
解释
while循环里的条件:先定义一个win变量,表示成功排雷的次数,只要小于71且没踩到雷就可以继续排雷。
第一个if就是保证坐标合法性在9*9之间,如果是非法坐标则提示重新输入。
重新输入坐标后,此时仍然满足循环条件不变,所以还可以进入第一个if里嵌套程序,如果踩雷,直接炸死,退出游戏;如果不是雷,则win加一次统计次数,并打印出这个位置周围雷的情况。只要在循环条件内可以一直排下去!!!
如果71一次仍然安全说明剩余位置全是雷,直接游戏胜利,退出游戏!
重点
这个计算周围信息的特别重要
第一种方法
int CountMine(char mine[ROWS][COLS], int x, int y)
{
int a = mine[x - 1][y] + mine[x][y - 1] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] + mine[x][y + 1] + mine[x + 1][y] - 8 * '0';
return a;
}
定义一个CountMine函数。不妨设输入排查的坐标为 x y ,将其周围一圈的八个数都加起来再减去8个字符0,就是对应的数字,又因为x y处不可能是雷,如果是雷就已经结束游戏了。
第二种方法
int CountMine(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int con = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
con += (mine[i][j] - '0');
}
}
return con;
}
利用for循环遍历x y周围包括x y处的9个数,然后每一次这个被遍历的数都减去字符0,然后将九次的结果相加,就是周围雷的个数。
结语
总之这个扫雷游戏不算很简单但是只要好好理解,还是可以掌握的!