直接开始吧! 多文件项目 扫雷项目内容较多,需要调用的函数 也较多,采用多文件的方式,可以使代码条理清晰 ,并且易于管理和维护 。文件如下
game.h
用于宏定义,函数声明,引入头文件等
game.c
用于函数的具体实现
front.c
用于实现程序的主干部分
other.c
用于实现其他杂项函数,这里我用于实现menu()
函数,主要内容太花了
注 .c
结尾的源文件均需加一句#include "game.h"
头文件 本次用到的头文件有stdio.h
stdlib.h
time.h
windows.h
和自己建的game.h
均在 文件game.h
中#include
define宏定义 为了便于阅读和维护 代码,在game.h
中的宏定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL + 2 #define Bomb '*' #define Blank ' ' #define EZ_RANK 10 #define HD_RANK 15 #define UN '' #define Flag '!'
为什么实际数组要大一圈? 如图,采用九宫格式访问时,大出来的一圈能有效防止越界访问
构建main函数 内容不多,主要是与菜单配合食用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int main () { srand((unsigned int )time(NULL )); while (1 ) { int input = 0 ; Menu(); printf ("请输入:>" ); scanf ("%d" , &input); switch (input) { case 1 : Sleep(250 ); game(); break ; case 2 : printf ("游戏结束\n" ); return 0 ; default : printf ("输入错误,请重试\n" ); break ; } } return 0 ; }
打印菜单 还在做静态菜单?弱爆了!来试试动态出现 的菜单!
原理很简单 ,就是打印空白数组
->向内逐个替换两侧元素
->清屏
->再打印
->再替换
->...
接下来的代码写在other.c
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void Menu () { char cover[] = "=======================" ; char option1[] = "====== play (1) ======" ; char option2[] = "====== exit (2) ======" ; char empty_c[] = " " ; char empty_1[] = " " ; char empty_2[] = " " ; int left = 0 ; int right = 22 ; while (left < right) { empty_c[left] = cover[left]; empty_c[right] = cover[right]; empty_1[left] = option1[left]; empty_1[right] = option1[right]; empty_2[left] = option2[left]; empty_2[right] = option2[right]; Sleep(50 ); system("cls" ); printf ("%s\n%s\n%s\n%s\n" ,empty_c,empty_1,empty_2,empty_c); left++; right--; } if (left == right) { Sleep(50 ); system("cls" ); empty_c[left] = cover[left]; empty_1[left] = option1[left]; empty_2[left] = option2[left]; printf ("%s\n%s\n%s\n%s\n" , empty_c, empty_1, empty_2, empty_c); } }
实现game()函数 游戏的主要逻辑在game()中实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void game () { char mine[ROWS][COLS] = { 0 }; char show[ROWS][COLS] = { 0 }; char check[ROWS][COLS] = { 0 }; InitBoard(mine, Blank); InitBoard(show, UN); InitCheck(check); SetMine(mine,EZ_RANK); SetNum(mine); OPMine(mine,show, check); printf ("敲击enter以继续\n" ); getchar(); getchar(); }
为什么用三个二维数组? 扫雷需要实现的功能较多,显然一个二维数组是不足以满足需求的,所以这里采用三个 数组相叠加的方式,各自实现功能,并整合到一起。
数组mine
用于存放雷
和雷周围的计数数字
数组show
用于储存给用户
看到的内容,可以是Unkown
,空白
,数字
,旗帜
数组check
用于记录棋盘的哪些地块被检查过了,防止后面用递归 打开成片的空白区时,出现无限递归。
规定 :检查过的坐标储存字符1
,没检查过的坐标储存字符0
,大出来的一圈 默认储存字符1
实现游戏用的函数 先看看有哪些要声明在game.h
里的 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void Menu () ;void InitBoard (char board[ROWS][COLS], char sign) ;void DisplayBoard (char board[ROWS][COLS]) ;void InitCheck (char check[ROWS][COLS]) ;void SetMine (char board[ROWS][COLS],int rank) ;void SetNum (char board[ROWS][COLS]) ;void OPMine (char mine[ROWS][COLS],char show[ROWS][COLS],char check[ROWS][COLS]) ;
好,有了目标,接下来就去一个一个实现
注 :以下代码均写在game.c
文件里
InitBoard() 1 2 3 4 5 6 7 8 9 10 void InitBoard (char board[ROWS][COLS],char sign) { for (int i = 0 ; i < ROWS; i++) { for (int j = 0 ; j < COLS; j++) { board[i][j] = sign; } } }
这里初始化的方式比较简单粗暴,就是用形参sign
填充整个二维数组
DisplayBoard()函数 这里采用的展示方式是带横纵坐标的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void DisplayBoard (char board[ROWS][COLS]) { for (int k = 0 ; k <= COL; k++) { printf ("%d " , k); } printf ("\n" ); for (int k = 0 ; k <= COL; k++) { printf ("--" ); } printf ("\n" ); for (int i = 1 ; i <= ROW; i++) { printf ("%d|" , i); for (int j = 1 ; j <= COL; j++) { printf ("%c " , board[i][j]); } printf ("\n" ); } }
InitCheck()函数 这里复用了InitBoard()
函数,是在其基础上增加了内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void InitCheck (char check[ROWS][COLS]) { InitBoard(check, '0' ); for (int k = 0 ; k < COLS; k++) { check[0 ][k] = '1' ; check[ROWS - 1 ][k] = '1' ; } for (int i = 1 ; i < ROWS -1 ; i++) { check[i][0 ] = '1' ; check[i][COLS - 1 ] = '1' ; } }
SetMine()函数 这里要使用rand()
函数搭配%
运算,来随机生成雷的坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void SetMine (char board[ROWS][COLS],int rank) { int x = 0 ; int y = 0 ; for (int count = 0 ;count < rank;) { x = rand() % ROW + 1 ; y = rand() % COL + 1 ; if (board[x][y] == Blank) { count++; board[x][y] = Bomb; } } }
SetNum()函数 这里遍历一遍数组并采用九宫格式
计数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 int CountMine (char board[ROWS][COLS], int x, int y) { int sum = 0 ; for (int i = x - 1 ; i <= x + 1 ; i++) { for (int j = y - 1 ; j <= y + 1 ; j++) { if (i != x || j != y) { if (board[i][j] == Bomb) { sum++; } } } } return sum; } void SetNum (char board[ROWS][COLS]) { for (int i = 1 ; i <= ROW; i++) { for (int j = 0 ; j <= COLS; j++) { if (board[i][j] == Blank) { if (CountMine(board, i, j)) { board[i][j] = '0' + CountMine(board, i, j); } } } } }
OPMine()函数–核心函数 该函数为游戏的核心函数
,有内置菜单,且多次调用其它函数,其中函数
的具体实现见四级标题
处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 void OPMine (char mine[ROWS][COLS],char show[ROWS][COLS],char check[ROWS][COLS]) { int x = 0 ; int y = 0 ; int flag = 1 ; int cont = 1 ; while (cont) { system("cls" ); DisplayBoard(show); printf ("排雷(1)\n插旗/拔旗(2)\n请输入:>" ); scanf ("%d" , &flag); switch (flag) { case 1 : { printf ("坐标格式,例>2(空格)2\n" ); printf ("请输入坐标:>" ); scanf ("%d %d" , &x, &y); if (show[x][y] == Flag) { printf ("此处为旗帜,不可排雷\n" ); break ; } else if (show[x][y] != UN) { printf ("不可重复排查\n" ); break ; } cont = FindMine(mine,show,check, x, y); if (cont) { cont = CheckWin(mine,show); } break ; } case 2 : { printf ("坐标格式,例>2(空格)2\n" ); printf ("请输入坐标:>" ); scanf ("%d %d" , &x, &y); SetFlag(show, x, y); break ; } default : { system("cls" ); printf ("\n输入错误(恼\n" ); } } } }
SetFlag()函数 先捏软柿子,插旗函数比较简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void SetFlag (char show[ROWS][COLS], int x, int y) { if (show[x][y] == UN) { show[x][y] = Flag; } else if (show[x][y] == Flag) { show[x][y] = UN; } else { printf ("报错\n" ); } }
ExpandBlank()函数 这个函数用于打开成片的空白区
,因为要从连着的空白连续开下去,所以要用到函数递归
,此时二维数组check
用于防止死递归
注 :这个函数一定要写在下一个函数(FindMine)前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void ExpandBlank (char mine[ROWS][COLS], char show[ROWS][COLS], char check[ROWS][COLS],int x,int y) { show[x][y] = mine[x][y]; check[x][y] = '1' ; if (mine[x][y] == Blank) { for (int i = x - 1 ; i <= x + 1 ; i++) { for (int j = y - 1 ; j <= y + 1 ; j++) { if (check[i][j] == '0' && mine[i][j] != Bomb && show[i][j] != Flag) { ExpandBlank(mine, show, check, i, j); } } } } }
FindMine()函数 排雷用的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int FindMine (char mine[ROWS][COLS], char show[ROW][COLS],char check[ROWS][COLS], int x, int y) { if (mine[x][y] == Bomb) { DisplayBoard(mine); printf ("炸死,游戏结束:)\n" ); return 0 ; } else if (mine[x][y] != Blank) { show[x][y] = mine[x][y]; return 1 ; } else { ExpandBlank(mine, show, check,x,y); return 1 ; } }
CheckWin()函数 用于检查玩家是否完全排雷,赢得游戏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int CheckWin (char mine[ROWS][COLS], char show[ROWS][COLS]) { int count = 0 ; for (int i = 1 ; i <= ROW; i++) { for (int j = 1 ; j <= COL; j++) { if (show[i][j] == UN || show[i][j] == Flag) { count++; } } } if (count == EZ_RANK) { printf ("恭喜排雷成功!\n" ); DisplayBoard(mine); return 0 ; } else { return 1 ; } }
总结 至此游戏所需的代码全部完成,已经可以编译出来玩耍啦。
该实践项目主要练习了二维数组
,函数
,函数递归
,宏定义
等内容,代码量在入门学习中算较大的,本人在初次编写的时候也写出了不少bug,debug的过程是相当快乐
建议多多画示意图,耐下性子 写代码和debug,哪怕是实现这样的小游戏项目,也是颇有意义的