前言
用写小游戏的方式,可以更快,更高效且有趣地学习C语言!!!
一、游戏的设计思路
1.游戏模式
2.设计模块
3.整体思路
(1)test.c:扫雷游戏的测试文件;game.c:游戏的函数的实现;game.h:游戏的函数声明。
(2)考虑游戏界面的展示问题,我们得如何实现呢?
参考了现有的扫雷游戏的界面,不难发现游戏界面的效果是一个几乘几的界面,所以我们就可以考虑用二维数组的来存储每个格子的数据。
(3)游戏的实现是可以分为两部分操作(埋地雷和选出没雷的坐标)
那么我们是否可以建立两个数组来分别存储地雷的地址和用户选出无雷的地址呢?答案是可以的。一个数组存放布置好的雷的信息,另一个数组存放排查出的雷的信息。
①一个数组存放布置好的雷的信息:雷用'1'表示,不是雷用'0'表示(为什么要这么设计呢,后面的代码会进行解释)
②一个数组存放排查出的雷的信息:先全部用'*'进行展示(增加了游戏的神秘感,让用户选择无雷的坐标),用户输入坐标,查看对应的坐标上是否有雷。有雷,通知用户您被炸死;无雷,统计该坐标周围雷的数目,并且展示给用户。
注:如果把两步操作都放在一个数组进行操作,那么当你判别的结果为1后,是雷?还是排除出雷的信息呢,这就产生了歧义!
(4)如果想实现9*9的棋盘,数组的大小就要设计成11*11比较好(为什么?)
图1:9*9数组
图2:11*11数组
如果没有用11*11的数组来存储数据,那么当计算机在判断一个坐标周围的8个坐标是否有炸弹时,就会出现数组越界的情况。但是要记住一点,虽然数组的大小是11*11,但是实际的操作大小只要9*9,所以在宏定义的时候就需要定义好大小。
通过观察也不难发现,其实就是左右,上下多了一行或者一列,也就是我们要操作的数组大小的行和列个加上2就是,存储的数组大小。
#define ROW 9//展示棋盘的行 #define COL 9//展示棋盘的列 #define ROWS ROW + 2//棋盘的总行数 #define COLS COL + 2//棋盘的总列数
(5)数组的初始化:
①一个数组存放布置好的雷的信息:char mine[11][11],初始化值为'0'
②一个数组存放排查出的雷的信息:char show[11][11],初始化值为'*'
二、分层说明
1.game.h文件
1.1 在此文件中,我们对其它需要在此游戏中实现的功能所对应的函数库进行了包含
//头文件包含 #include<stdio.h> #include<time.h> #include<stdlib.h>
这样做的好处在于,当我们需要同时使用stdio.h、stdlib.h和time.h时,不用再到每个文件中进行文件包含,我们只需写一句#include"game.h"就行,如在test.c文件中进行文件包含。
1.2 进行符号定义
//宏定义 #define EASY_COUNT 10//炸弹的数目 #define ROW 9//展示棋盘的行 #define COL 9//展示棋盘的列 #define ROWS ROW + 2//棋盘的总行数 #define COLS COL + 2//棋盘的总列数
为什么需要符号定义呢?这是因为我们这里的符号ROW,COL对应了二维字符数组的大小,如果当我们想改变该字符数组的大小时,只需在改动game.h文件中的ROW、COL、ROWS、COLS和EASY_COUNT的值,就可以改变在此游戏中,所对应的字符数组大小,不用再一个一个数字去修改。
1.3 函数声明
/函数定义 //二维数组初始化 void InitBoard(char board[ROWS][COLS], int row, int col, char set); //棋盘展示 void DisplayBoard(char board[][COLS], int row, int col); //布置雷 void SetMine(char mine[][COLS], int row, int col); //排查雷 void FindMine(char mine[][COLS], char show[][COLS], int row, int col);
在game.h文件中进行函数声明后,在其他.c文件中想用调用在game.h声明过的函数,只需要将game.h进行一个文件包含就行了。如:#include"game.h"
2.game.c文件
在此文件中,主要是编辑了一些关于雷盘的初始化,排查雷,棋盘展示等等的功能。
2.1 对二维数组的初始化
//对于两个棋盘的初始化 void InitBoard(char board[ROWS][COLS], int row, int col, char set) { int i, j; for (i = 0;i < row;i++) { for (j = 0;j < col;j++) { board[i][j] = set; } } }
2.2 展示游戏界面
//棋盘展示 void DisplayBoard(char board[][COLS], int row, int col) { int i, j; printf("--------------- 扫雷游戏 ---------------\n"); for (i = 0;i <= 9;i++) { printf("%d ", i); } printf("\n"); for (i = 1;i <= row;i++) { printf("%d ", i); for (j = 1;j <= col;j++) { printf("%c ", board[i][j]); } printf("\n"); } printf("--------------- 扫雷游戏 ---------------\n"); }
2.3 布置雷
//布置雷 void SetMine(char mine[][COLS], int row, int col) { //count为总雷数 int count = EASY_COUNT;//EASY_COUNT为10 int x , y;//对应的坐标 while (count) { x = rand() % row + 1; y = rand() % col + 1; if (mine[x][y] == '0') { mine[x][y] = '1'; //每当赋值一个雷,总雷数就减少一个 count--; } } }
2.4 排查雷
//排查雷 void FindMine(char mine[][COLS], char show[][COLS], int row, int col) { /* 首先输入坐标,查看对应的坐标上是否有雷 (1)有雷,通知用户您被炸死 (2)无雷,统计该坐标周围雷的数目,并且展示给用户 */ int x = 0, y = 0;//对应的坐标 int win = 0; while (win < ROW * COL - EASY_COUNT) { printf("请您输入要排查的坐标:> "); scanf("%d%d", &x, &y); //判断坐标的合法性 if (x >= 1 && x <= 9 && y >= 1 && y <= 9) { //判断是否进行了重复输入 if (show[x][y] == '*') { if (mine[x][y] == '1') { printf("这人多捞哇,这就被炸死了?\n"); DisplayBoard(mine, row, col);//展示给用户看炸弹棋盘,让他死得明明白白【doge】 break; } else { //不是雷的情况,用一个count变量来计算周围雷的数目 int count = GetMineCount(mine, x, y);//传入坐标,在mine棋盘中对应上这坐标位置 /* 此处用了隐式类型提升 count+'0',已知'0'的ASCII值为48,'1'的ASCII值为49,……'9'的ASCII值为57 所以count+'0'后先变成ASCII值,再变成对应的字符 */ show[x][y] = count + '0'; //显示排查的结果 DisplayBoard(show, row, col); win++; } } else { printf("这人多捞哇!别重复输入行不行!\n"); } } else { printf("您输入的坐标有误,请重新输入!\n"); } } if (win == ROW * COL - EASY_COUNT) { printf("这设计者有点东西,赢得不容易哇!\n"); DisplayBoard(mine, row, col); } }
在此代码中,还调用了定义在game.c文件中的static int GetMineCount(char mine[][COLS], int x, int y)来实现计算用户选择的坐标的周围8个坐标是否有炸弹,有多少个?记录下来并展示给用户
为什么要使用static修饰这个函数呢?
因为只是用在game.c源文件中,不需要向外暴露该函数,所以用static修饰
static的作用
①用于修饰函数
②用于修饰局部变量
③用于修饰全局变量
x,y是用户选择的坐标,那么该坐标周围的8个坐标就可以用x和y如上图表示出来。
那么我们该如何实现计算用户选择的坐标的周围8个坐标是否有炸弹,有多少个呢?总共有3中方法分别是直接计数法、循环法和最笨的条件选择法。
(1)最笨的条件选择法
//用最笨的办法多个if语句 //判断该位置周围的雷数 static int GetMineCount(char mine[][COLS], int x, int y) { int countBoom = 0; if (mine[x - 1][y - 1] == '1') { countBoom++; }if (mine[x - 1][y] == '1') { countBoom++; }if (mine[x - 1][y + 1] == '1') { countBoom++; }if (mine[x][y - 1] == '1') { countBoom++; }if (mine[x][y + 1] == '1') { countBoom++; }if (mine[x + 1][y - 1] == '1') { countBoom++; }if (mine[x + 1][y + 1] == '1') { countBoom++; } return countBoom; }
(2)循环法
第一种循环方式:
//用循环来解决问题 static int GetMineCount(char mine[][COLS], int x, int y) { //其中一种 int i, j; int countBoom = 0; for (i = x - 1; i <= x + 1;i++) { for (j = y - 1; j <= y + 1;j++) { if (mine[i][j] == '1') { countBoom++; } } } return countBoom; }
第二种循环方式:
//用循环来解决问题 static int GetMineCount(char mine[][COLS], int x, int y) { //另外一种 int i, j; int countBoom = 0; for (i = -1;i <= 1;i++) { for (j = -1;j <= 1;j++) { if (mine[x + i][y + j] == '1') { countBoom++; } } } return countBoom; }
(3)直接计数法
static int GetMineCount(char mine[][COLS], int x, int y) { 这里也就是为什么要把有炸弹的位置设置为'1' return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0'; }
3.test.c文件
3.1 菜单选择
void menu() { printf("扫雷游戏\n"); printf("************************\n"); printf("******* 请您选择 *******\n"); printf("******* 1 为开始 *******\n"); printf("******* 0 为结束 *******\n"); printf("************************\n"); }
3.2 执行游戏操作
void game() { char mine[ROWS][COLS] = { 0 };//存放布置的雷的信息 char show[ROWS][COLS] = { 0 };//存放排查出雷的信息 //初始化两个棋盘(二维数组) InitBoard(mine, ROWS, COLS, '0'); InitBoard(show, ROWS, COLS, '*'); /*DisplayBoard(show, ROW, COL); DisplayBoard(mine, ROW, COL);*/ //展示两个棋盘内容 //DisplayBoard(mine, ROW, COL); DisplayBoard(show, ROW, COL); //布置雷 SetMine(mine, ROW, COL); //DisplayBoard(mine, ROW, COL); //排查雷 FindMine(mine, show, ROW, COL); }
3.3 主函数
void main() { int input = 0; srand((unsigned int)time(NULL)); do { menu(); printf("请输入您的选择"); scanf("%d", &input); switch (input) { case 1: printf("游戏开始!\n"); game(); break; case 0: printf("游戏结束"); break; default: printf("您输入的数字有误,请重新输入!\n"); } } while (input); }
我们在game函数中,还调用了game.c文件中的函数,从而丰富了我们的游戏功能。这种分模块设计思想,也是以后在工作中设计一个项目的一个中心思想。
三、完整代码展示
1.test.c文件
#define _CRT_SECURE_NO_WARNINGS 1 //引用自己的头文件 #include"game.h" //菜单选择 void menu() { printf("扫雷游戏\n"); printf("************************\n"); printf("******* 请您选择 *******\n"); printf("******* 1 为开始 *******\n"); printf("******* 0 为结束 *******\n"); printf("************************\n"); } //执行游戏操作 void game() { char mine[ROWS][COLS] = { 0 };//存放布置的雷的信息 char show[ROWS][COLS] = { 0 };//存放排查出雷的信息 //初始化两个棋盘(二维数组) InitBoard(mine, ROWS, COLS, '0'); InitBoard(show, ROWS, COLS, '*'); /*DisplayBoard(show, ROW, COL); DisplayBoard(mine, ROW, COL);*/ //展示两个棋盘内容 //DisplayBoard(mine, ROW, COL); DisplayBoard(show, ROW, COL); //布置雷 SetMine(mine, ROW, COL); //DisplayBoard(mine, ROW, COL); //排查雷 FindMine(mine, show, ROW, COL); } void main() { int input = 0; srand((unsigned int)time(NULL)); do { menu(); printf("请输入您的选择"); scanf("%d", &input); switch (input) { case 1: printf("游戏开始!\n"); game(); break; case 0: printf("游戏结束"); break; default: printf("您输入的数字有误,请重新输入!\n"); } } while (input); }
2.game.h文件
#define _CRT_SECURE_NO_WARNINGS 1 //头文件包含 #include<stdio.h> #include<time.h> #include<stdlib.h> //宏定义 #define EASY_COUNT 10//炸弹的数目 #define ROW 9//展示棋盘的行 #define COL 9//展示棋盘的列 #define ROWS ROW + 2//棋盘的总行数 #define COLS COL + 2//棋盘的总列数 //函数定义 //二维数组初始化 void InitBoard(char board[ROWS][COLS], int row, int col, char set); //棋盘展示 void DisplayBoard(char board[][COLS], int row, int col); //布置雷 void SetMine(char mine[][COLS], int row, int col); //排查雷 void FindMine(char mine[][COLS], char show[][COLS], int row, int col);
3.game.c文件
#define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" //对于两个棋盘的初始化 void InitBoard(char board[ROWS][COLS], int row, int col, char set) { int i, j; for (i = 0;i < row;i++) { for (j = 0;j < col;j++) { board[i][j] = set; } } } //棋盘展示 void DisplayBoard(char board[][COLS], int row, int col) { int i, j; printf("--------------- 扫雷游戏 ---------------\n"); for (i = 0;i <= 9;i++) { printf("%d ", i); } printf("\n"); for (i = 1;i <= row;i++) { printf("%d ", i); for (j = 1;j <= col;j++) { printf("%c ", board[i][j]); } printf("\n"); } printf("--------------- 扫雷游戏 ---------------\n"); } //布置雷 void SetMine(char mine[][COLS], int row, int col) { //count为总雷数 int count = EASY_COUNT;//EASY_COUNT为10 int x , y;//对应的坐标 while (count) { x = rand() % row + 1; y = rand() % col + 1; if (mine[x][y] == '0') { mine[x][y] = '1'; //每当赋值一个雷,总雷数就减少一个 count--; } } } static int GetMineCount(char mine[][COLS], int x, int y) { //另外一种 int i, j; int countBoom = 0; for (i = -1;i <= 1;i++) { for (j = -1;j <= 1;j++) { if (mine[x + i][y + j] == '1') { countBoom++; } } } return countBoom; } //排查雷 void FindMine(char mine[][COLS], char show[][COLS], int row, int col) { /* 首先输入坐标,查看对应的坐标上是否有雷 (1)有雷,通知用户您被炸死 (2)无雷,统计该坐标周围雷的数目,并且展示给用户 */ int x = 0, y = 0;//对应的坐标 int win = 0; while (win < ROW * COL - EASY_COUNT) { printf("请您输入要排查的坐标:> "); scanf("%d%d", &x, &y); //判断坐标的合法性 if (x >= 1 && x <= 9 && y >= 1 && y <= 9) { //判断是否进行了重复输入 if (show[x][y] == '*') { if (mine[x][y] == '1') { printf("这人多捞哇,这就被炸死了?\n"); DisplayBoard(mine, row, col);//展示给用户看炸弹棋盘,让他死得明明白白【doge】 break; } else { //不是雷的情况,用一个count变量来计算周围雷的数目 //传入坐标,在mine棋盘中对应上坐标位置,然后通过计算周围8个坐标是否有炸弹 int count = GetMineCount(mine, x, y); /* 此处用了隐式类型提升 count+'0',已知'0'的ASCII值为48,'1'的ASCII值为49,……'9'的ASCII值为57 所以count+'0'后先变成ASCII值,再变成对应的字符 */ show[x][y] = count + '0'; //显示排查的结果 DisplayBoard(show, row, col); win++; } } else { printf("这人多捞哇!别重复输入行不行!\n"); } } else { printf("您输入的坐标有误,请重新输入!\n"); } } if (win == ROW * COL - EASY_COUNT) { printf("这设计者有点东西,赢得不容易哇!\n"); DisplayBoard(mine, row, col); } }
四、游戏效果图
1.重复输入同个地址
2.被炸死了
3.玩家胜利
炸弹数修改为80,容易测试取得胜利的结果。9*9==81,81-80==1,只要选一个位置就可以取得胜利,大大降低了测试时间。
哈哈哈,看到这里不容易哇!不足之处,请多多指教。大家觉得有趣,就点一波关注呗,赞了点一点吧,以后还会继续更新好玩的内容!