目录
一、思路分析:
每个格子有三种状态: 未打开, 标记, 未知,已打开
每个格子可能存放雷或者周围雷信息
考虑创建一个棋盘结构体,进行存储,当格子的状态为已打开时,其状态不可修改,否则可以修改
初始化棋盘时可以将每个方格中的信息初始化为空格,表示其中没有任何东西
布置雷的时候,利用随机数进行随机摆放
每个格子周围雷的信息可以通过一个循环对以其为中心的周围8个格子进行遍历求得
在打印时,若其为已打开,在显示其内存储的信息,若为未打开则以空格代替,若为标记则用 M 表示, 若为未知则用 ? 表示, 其中 空格 和 ?可以直接打开, M 则不可以直接打开,但可以通过标记将其变为 ?
考虑到用户可以自己定义雷的数目,棋盘的规格,可以利用动态二维数组进行操作,使用参数传递,进行棋盘规模的控制
二、准备工作:
1、定义结构体
typedef struct Board
{
char sign; //存储信息
int state; //存储当前状态,0 为不显示, 1 为已显示, 2 为标记, 3为未知
}Board;
2、初始化结构体
Board** initBoard(Board** board, size_t row, size_t col)
{
//实际中为了简化问题,开辟行,列 均 比其显示部分大2的数组
size_t rows = row + 2;
size_t cols = col + 2;
board = (Board**)malloc(sizeof(Board*) * rows);
//内存开辟失败
if (!board)
{
return NULL;
}
for (int i = 0; i < rows; i++)
{
board[i] = (Board*)malloc(sizeof(Board) * cols);
//此时若内存开辟失败,则需要将之前开辟的空间释放掉
if (!board[i])
{
free(board);
return NULL;
}
for (int j = 0; j < cols; j++)
{
board[i][j].sign = ' ';
board[i][j].state = 0;
}
}
return board;
}
3、显示菜单
void showMenu()
{
printf("----欢迎使用扫雷游戏----\n\n");
printf("------1、开始游戏------\n");
printf("------2、设置参数------\n");
printf("------0、退出游戏------\n");
}
4、布置地雷
void set_mine(Board** board, size_t row, size_t col, size_t mine_count)
{
//循环放入地雷,因为mine_count是值传递,在此处修改不会影响原有的值,所以在此直接用其进行控制
while (mine_count)
{
//保证每个坐标的值均在 1 ~ row + 1之间
size_t x = rand() % row + 1;
size_t y = rand() % col + 1;
//如果可以放置地雷,将此处放为地雷,同时待布置地雷数目减一
if (board[x][y].sign == ' ')
{
board[x][y].sign = '*';
mine_count--;
}
}
}
5、获取每个方格周围雷的数目
对于每个方格其内存储的信息不同,考虑对单个方格进行查找即代码中的 get_num()函数模块
void get_around_mine(Board** board, size_t rows, size_t cols)
{
//展示的部分为 1 ~ row + 1 即 1 ~ rows - 1, 列同理
for (int i = 1; i < rows - 1; i++)
{
for (int j = 1; j < cols - 1; j++)
{
if (board[i][j].sign == '*') //如果当前格是地雷则跳过
{
continue;
}
board[i][j].sign = get_num(board, i, j); //将当前个元素修改为周围地雷数量
}
}
}
//char类型可以隐士转化为int类型,其相加是其ASCII码相加,可以利用这一原因在c上进行操作
char get_num(Board** board, size_t x, size_t y)
{
char c = '0';
for (int i = x - 1; i < x + 2; i++)
{
for (int j = y - 1; j < y + 2; j++)
{
if (board[i][j].sign == '*')
{
c++;
}
}
}
return c;
}
6、打印棋盘
void display(Board** board, size_t row, size_t col, int mode)
{
//打印表头,方便用户定位信息
for (int j = 0; j < col + 1; j++)
{
printf("| %2d ", j);
}
printf("|\n");
for (int j = 0; j < col + 1; j++)
{
printf("|----");
}
printf("|\n");
for (int i = 1; i < row + 1; i++)
{
for (int j = 0; j < col + 2; j++)
{
//如果是j == 0 则说明此时应该为第几行,而非棋盘信息
if (j == 0 && i != row + 1)
{
printf("| %2d ", i);
}
else {
int mark = board[i][j].state;
if (mark == 0)
{
printf("| %2c ", ' ');
}
else if (mark == 1)
{
printf("| %2c ", board[i][j].sign);
}
else if (mark == 2)
{
printf("| %2c ", 'M');
}
else
{
printf("| %2c ", '?');
}
}
}
printf("\n");
for (int j = 1; j < col + 2; j++)
{
printf("|----");
}
printf("|\n");
}
//说明游戏已经结束
if (mode == -1)
{
return;
}
//反映当前模式
printf("当前模式: ");
if (mode == 1)
{
printf("排雷模式\n");
}
else if (mode == 2)
{
printf("标记模式\n");
}
}
7、模式设置
void set_mode(int* mode)
{
printf("请选择你想设置的模式\n");
printf("2、标记模式\n");
printf("1、排雷模式\n");
do
{
scanf("%d", mode);
if (*mode == 1 || *mode == 2)
{
return;
}
printf("输入错误,请重新输入\n");
} while (1);
}
8、标记
void mark_mine(Board** board, int* n, int* m, int row, int col)
{
int x;
int y;
do
{
scanf("%d%d", &x, &y);
//清空缓冲区
while (getchar() != '\n');
if (x < 1 || x > row || y < 1 || y > col)
{
printf("输入错误,请重新输入\n");
}
else
{
//实现其从 'M' -> '?'-> ' ' -> 'M' 的转换
if (board[x][y].state == 2)
{
board[x][y].state = 3;
(*n)--;
(*m)++;
break;
}
else if (board[x][y].state == 3)
{
board[x][y].state = 0;
break;
}
else if (board[x][y].state == 0)
{
board[x][y].state = 2;
(*n)++;
(*m)--;
break;
}
else
{
printf("输入的值无效\n");
}
}
} while (1);
}
9、扫雷
因为一次可以打开多个格子,传入n,m,其中n是指剩余雷的个数,m指剩下未打开和未标记的格子数目,用于判断是否胜利
bool sweap_mine(Board** board, int* n, int* m, int row, int col)
{
int x;
int y;
do
{
scanf("%d%d", &x, &y);
while (getchar() != '\n');
if (x < 1 || x > row || y < 1 || y > col)
{
printf("输入错误,请重新输入\n");
}
else
{
//如果当前格子不是显示状态或标记状态,则可以打开
if (board[x][y].state != 1 && board[x][y].state != 2)
{
if (board[x][y].sign == '*')
{
board[x][y].state = 1;
(*m)--;
(*n)--;
// 失败,游戏终止
return false;
}
//每次方格可以炸开,使用该函数来使得一次打开多个格子
sweap_function(board, x, y, n, m, row, col);
break;
}
else
{
printf("输入值无效,请重新输入\n");
}
}
} while (1);
//未踩到雷,说明游戏继续
return true;
}
void sweap_function(Board** board, int x, int y, int* n, int* m, int row, int col)
{
//利用随机数,一次打开随机的几排
int t = rand() % row;
while (t < row)
{
int i = x;
while (i <= row && board[i][y].sign != '*' && board[i][y].sign != ' ' && (board[i][y].state != 1 || board[i][y].state != 2))
{
(*m)--;
board[i][y].state = 1;
i++;
}
y++;
t++;
}
}
10、参数设置
void setParam(int* p_row, int* p_col, int* p_mine_count)
{
printf("请输入行数\n");
scanf("%d", p_row);
printf("请输入列数\n");
scanf("%d", p_col);
printf("请输入地雷数\n");
scanf("%d", p_mine_count);
system("cls");
}
三、流程控制
此处可以封装一个game模块对游戏的每个流程进行控制
void game(Board** board, size_t row, size_t col, size_t mine_count)
{
//实际行列数比显示部分大2
size_t rows = row + 2;
size_t cols = col + 2;
//当前的模式,1为扫雷,2为标记
int mode = 1;
int n = 0; //存储标记的雷的数目
int m = row * col; //未打开和未标记的格子数
board = initBoard(board, rows, cols);
bool game_continue = true;
int select; //用于与用户进行交互,修改模式
//此时地图创建失败,无法游戏,直接提示用户并返回
if (!board)
{
printf("地图创建失败\n");
system("pause");
system("cls");
return;
}
set_mine(board, row, col, mine_count); //设置地雷
get_around_mine(board, rows, cols); //获取周围地雷的数目
do
{
display(board, row, col, mode);
printf("是否修改模式或回到主菜单\n");
printf("1、修改模式\n2、继续游戏\n3、回到主菜单\n");
scanf("%d", &select);
if (select == 1)
{
system("cls");
set_mode(&mode);
system("cls");
display(board, row, col, mode);
}
else if (select == 3)
{
break;
}
printf("请输入坐标\n");
if (mode == 1)
{
game_continue = sweap_mine(board, &n, &m, row, col);
}
else if (mode == 2)
{
mark_mine(board, &n, &m, row, col);
}
if (game_continue == false)
{
break;
}
} while (!(n == mine_count && m == 0));
if (game_continue == false)
{
printf("失败\n");
display(board, row, col, -1);
}
else if(n == mine_count && m == 0)
{
printf("恭喜获胜\n");
display(board, row, col, -1);
}
//释放内存
free(board);
system("pause");
system("cls");
}
主函数
int main()
{
srand((unsigned int)time(NULL));
int row = 10; //行数
int col = 10; //列数
int mine_count = 10; //地雷数目
Board** board = NULL; //棋盘对象
int select; //接收用户的选择
do
{
showMenu(); //展示菜单
scanf("%d", &select);
while (getchar() != '\n');
switch (select)
{
case 1:
game(board, row, col, mine_count); //游戏进程
break;
case 2:
setParam(&row, &col, &mine_count); //设置参数
break;
case 0:
printf("感谢您的使用\n");
break;
}
} while (select);
return 0;
}
四、整体代码
game.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct Board
{
char sign; //存储信息
int state; //存储当前状态,0 为不显示, 1 为已显示, 2 为标记, 3为未知
}Board;
void showMenu();
Board** initBoard(Board** board, size_t row, size_t col);
void game(Board** board, size_t row, size_t col, size_t mine_count);
void setParam(int* p_row, int* p_col, int* p_mine_count);
void sweap_function(Board** board, int x, int y, int* n, int* m, int row, int col);
bool sweap_mine(Board** board, int* n, int* m, int row, int col);
void mark_mine(Board** board, int* n, int* m, int row, int col);
void set_mode(int* mode);
void display(Board** board, size_t row, size_t col, int mode);
char get_num(Board** board, size_t x, size_t y);
void get_around_mine(Board** board, size_t rows, size_t cols);
void set_mine(Board** board, size_t row, size_t col, size_t mine_count);
game.c
#include "game.h"
void game(Board** board, size_t row, size_t col, size_t mine_count)
{
//实际行列数比显示部分大2
size_t rows = row + 2;
size_t cols = col + 2;
//当前的模式,1为扫雷,2为标记
int mode = 1;
int n = 0; //存储标记的雷的数目
int m = row * col; //未打开和未标记的格子数
board = initBoard(board, rows, cols);
bool game_continue = true;
int select; //用于与用户进行交互,修改模式
//此时地图创建失败,无法游戏,直接提示用户并返回
if (!board)
{
printf("地图创建失败\n");
system("pause");
system("cls");
return;
}
set_mine(board, row, col, mine_count); //设置地雷
get_around_mine(board, rows, cols); //获取周围地雷的数目
do
{
display(board, row, col, mode);
printf("是否修改模式或回到主菜单\n");
printf("1、修改模式\n2、继续游戏\n3、回到主菜单\n");
scanf("%d", &select);
if (select == 1)
{
system("cls");
set_mode(&mode);
system("cls");
display(board, row, col, mode);
}
else if (select == 3)
{
break;
}
printf("请输入坐标\n");
if (mode == 1)
{
game_continue = sweap_mine(board, &n, &m, row, col);
}
else if (mode == 2)
{
mark_mine(board, &n, &m, row, col);
}
if (game_continue == false)
{
break;
}
} while (!(n == mine_count && m == 0));
if (game_continue == false)
{
printf("失败\n");
display(board, row, col, -1);
}
else if(n == mine_count && m == 0)
{
printf("恭喜获胜\n");
display(board, row, col, -1);
}
//释放内存
free(board);
system("pause");
system("cls");
}
void setParam(int* p_row, int* p_col, int* p_mine_count)
{
printf("请输入行数\n");
scanf("%d", p_row);
printf("请输入列数\n");
scanf("%d", p_col);
printf("请输入地雷数\n");
scanf("%d", p_mine_count);
system("cls");
}
void sweap_function(Board** board, int x, int y, int* n, int* m, int row, int col)
{
//利用随机数,一次打开随机的几排
int t = rand() % row;
while (t < row)
{
int i = x;
while (i <= row && board[i][y].sign != '*' && board[i][y].sign != ' ' && (board[i][y].state != 1 || board[i][y].state != 2))
{
(*m)--;
board[i][y].state = 1;
i++;
}
y++;
t++;
}
}
bool sweap_mine(Board** board, int* n, int* m, int row, int col)
{
int x;
int y;
do
{
scanf("%d%d", &x, &y);
while (getchar() != '\n');
if (x < 1 || x > row || y < 1 || y > col)
{
printf("输入错误,请重新输入\n");
}
else
{
//如果当前格子不是显示状态或标记状态,则可以打开
if (board[x][y].state != 1 && board[x][y].state != 2)
{
if (board[x][y].sign == '*')
{
board[x][y].state = 1;
(*m)--;
(*n)--;
// 失败,游戏终止
return false;
}
//每次方格可以炸开,使用该函数来使得一次打开多个格子
sweap_function(board, x, y, n, m, row, col);
break;
}
else
{
printf("输入值无效,请重新输入\n");
}
}
} while (1);
//未踩到雷,说明游戏继续
return true;
}
void mark_mine(Board** board, int* n, int* m, int row, int col)
{
int x;
int y;
do
{
scanf("%d%d", &x, &y);
//清空缓冲区
while (getchar() != '\n');
if (x < 1 || x > row || y < 1 || y > col)
{
printf("输入错误,请重新输入\n");
}
else
{
//实现其从 'M' -> '?'-> ' ' -> 'M' 的转换
if (board[x][y].state == 2)
{
board[x][y].state = 3;
(*n)--;
(*m)++;
break;
}
else if (board[x][y].state == 3)
{
board[x][y].state = 0;
break;
}
else if (board[x][y].state == 0)
{
board[x][y].state = 2;
(*n)++;
(*m)--;
break;
}
else
{
printf("输入的值无效\n");
}
}
} while (1);
}
void set_mode(int* mode)
{
printf("请选择你想设置的模式\n");
printf("2、标记模式\n");
printf("1、排雷模式\n");
do
{
scanf("%d", mode);
if (*mode == 1 || *mode == 2)
{
return;
}
printf("输入错误,请重新输入\n");
} while (1);
}
void display(Board** board, size_t row, size_t col, int mode)
{
//打印表头,方便用户定位信息
for (int j = 0; j < col + 1; j++)
{
printf("| %2d ", j);
}
printf("|\n");
for (int j = 0; j < col + 1; j++)
{
printf("|----");
}
printf("|\n");
for (int i = 1; i < row + 1; i++)
{
for (int j = 0; j < col + 2; j++)
{
//如果是j == 0 则说明此时应该为第几行,而非棋盘信息
if (j == 0 && i != row + 1)
{
printf("| %2d ", i);
}
else {
int mark = board[i][j].state;
if (mark == 0)
{
printf("| %2c ", ' ');
}
else if (mark == 1)
{
printf("| %2c ", board[i][j].sign);
}
else if (mark == 2)
{
printf("| %2c ", 'M');
}
else
{
printf("| %2c ", '?');
}
}
}
printf("\n");
for (int j = 1; j < col + 2; j++)
{
printf("|----");
}
printf("|\n");
}
//说明游戏已经结束
if (mode == -1)
{
return;
}
//反映当前模式
printf("当前模式: ");
if (mode == 1)
{
printf("排雷模式\n");
}
else if (mode == 2)
{
printf("标记模式\n");
}
}
//char类型可以隐士转化为int类型,其相加是其ASCII码相加,可以利用这一原因在c上进行操作
char get_num(Board** board, size_t x, size_t y)
{
char c = '0';
for (int i = x - 1; i < x + 2; i++)
{
for (int j = y - 1; j < y + 2; j++)
{
if (board[i][j].sign == '*')
{
c++;
}
}
}
return c;
}
void get_around_mine(Board** board, size_t rows, size_t cols)
{
//展示的部分为 1 ~ row + 1 即 1 ~ rows - 1, 列同理
for (int i = 1; i < rows - 1; i++)
{
for (int j = 1; j < cols - 1; j++)
{
if (board[i][j].sign == '*') //如果当前格是地雷则跳过
{
continue;
}
board[i][j].sign = get_num(board, i, j); //将当前个元素修改为周围地雷数量
}
}
}
void set_mine(Board** board, size_t row, size_t col, size_t mine_count)
{
//循环放入地雷,因为mine_count是值传递,在此处修改不会影响原有的值,所以在此直接用其进行控制
while (mine_count)
{
//保证每个坐标的值均在 1 ~ row + 1之间
size_t x = rand() % row + 1;
size_t y = rand() % col + 1;
//如果可以放置地雷,将此处放为地雷,同时待布置地雷数目减一
if (board[x][y].sign == ' ')
{
board[x][y].sign = '*';
mine_count--;
}
}
}
void showMenu()
{
printf("----欢迎使用扫雷游戏----\n\n");
printf("------1、开始游戏------\n");
printf("------2、设置参数------\n");
printf("------0、退出游戏------\n");
}
Board** initBoard(Board** board, size_t row, size_t col)
{
//实际中为了简化问题,开辟行,列 均 比其显示部分大2的数组
size_t rows = row + 2;
size_t cols = col + 2;
board = (Board**)malloc(sizeof(Board*) * rows);
//内存开辟失败
if (!board)
{
return NULL;
}
for (int i = 0; i < rows; i++)
{
board[i] = (Board*)malloc(sizeof(Board) * cols);
//此时若内存开辟失败,则需要将之前开辟的空间释放掉
if (!board[i])
{
free(board);
return NULL;
}
for (int j = 0; j < cols; j++)
{
board[i][j].sign = ' ';
board[i][j].state = 0;
}
}
return board;
}
main.c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "game.h"
#include <time.h>
int main()
{
srand((unsigned int)time(NULL));
int row = 10; //行数
int col = 10; //列数
int mine_count = 10; //地雷数目
Board** board = NULL; //棋盘对象
int select; //接收用户的选择
do
{
showMenu(); //展示菜单
scanf("%d", &select);
while (getchar() != '\n');
switch (select)
{
case 1:
game(board, row, col, mine_count); //游戏进程
break;
case 2:
setParam(&row, &col, &mine_count); //设置参数
break;
case 0:
printf("感谢您的使用\n");
break;
}
} while (select);
return 0;
}