扫雷游戏的C语言实现

目标

用C语言实现扫雷游戏(非界面,在控制台进行交互),随机选择位置点击,若该位置是雷,则被炸死,游戏失败;否则在该位置显示与该位置相邻的所有格子的雷的总数,直到排除了所有非雷的可能,游戏胜利。

设计思路

为了知道ROW行,COL列哪些格子被设置了雷,我们需要用二维数组把其位置存储起来,例如雷的位置存为1,否则为0。如果与某个位置相邻的所有格子的雷的总数为1,这两个1有歧义,因此需要分开存储(事实上布置雷的数组不能被打印在屏幕上)。一个二维数组mine存储哪些位置设置了雷,另一个二维数组show存储与每个位置相邻的所有格子的雷的总数。但是ROW*COL数组下标不能越界,一种方式是让行数和列数增大2使得不可能越界,或加上条件判断以节约空间。show若用0表示该位置尚未被排查,可能和每个位置相邻的所有格子的雷的总数相冲突,故用'*'来初始化未被排查的位置。把这两个数组都设置为字符数组以便后面调用同一函数。

游戏步骤

首先初始化两个字符数组,然后布置雷,接着打印初始化的show数组,最后排查雷。

以下分别是函数声明的function.h和游戏源文件Minesweeper.c。

C 复制代码
#pragma once
#define COL 9
#define ROW 9
#define COLS (COL + 2)
#define ROWS (ROW + 2)
#define Mine_COUNT 10    //雷的个数

#include <stdio.h>
#include <stdlib.h>    //包含rand函数和srand函数
#include <time.h>    //包含time函数

void Init_Board(char board[][COLS], int, char);
void Display_Board(char board[][COLS], int);
void Set_Mine(char mine[][COLS], int);
void Find_Mine(char mine[][COLS], char show[][COLS], int);
C 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include "function.h"    注意function.h不是库函数对应的头文件,自己创建的需要加""

void menu()
{
	printf("--------   1. play  --------\n");
	printf("--------   0. exit  --------\n");
}
void game()
{
        char mine[ROWS][COLS] = { 0 };
        char show[ROWS][COLS] = { 0 };
        Init_Board(mine, ROWS, '0');
        Init_Board(show, ROWS, '*');
        Set_Mine(mine, ROW);
        Display_Board(show, ROW);
        Find_Mine(mine, show, ROW);
}

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
			case 1:
				game();
				break;
			case 0:
				break;
			default:
				printf("选择错误");
				break;
		}
	} 
	while (input);
	return 0;
}

具体函数实现

初始化数组函数Init_Board

设计思路:show和mine数组初始化不同,一个用'0'初始化,一 个用'*'初始化,为了让函数知道初始化的字符是什么,把初始化的字符作为函数参数传入函数。注意二维数组arr以一维数组arr0,arr1,...为元素连续存储,故arrij即*(*(arr+i)+j),*表示取对应地址的元素,arr+i即&arr0+i*sizeof(arr0),数组名arr是首元素arr0的地址,丢失了行数信息,故sizeof(arr0)即列数不可省略,行数是防止越界必须传入。

C 复制代码
void Init_Board(char board[][COLS], int rows,char init_char)
{
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < COLS; j++)
		{
			board[i][j] = init_char;
		}
	}
}

打印数组函数Display_Board

C 复制代码
void Display_Board(char board[][COLS], int row)
{
	int i = 0;
	for (i = 0; i <= COL; i++)
	{
		printf("%d ", i);    //打印列号,从0开始是因为第1列是行号
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);    //打印行号
		int j = 0;
		for (j = 1; j <= COL; j++)
		{
			printf("%c ",board[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}

注意这里必须用原始非扩大的ROW传入,我们不需要打印ROW之外的格子,但数组类型固定,不能更改。

设置雷函数Set_Mine

C 复制代码
void Set_Mine(char mine[][COLS], int row)
{
	int count = Mine_COUNT;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % COL  + 1;
		if (mine[x][y] == '0')    //防止重复设置地雷
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

这里要小心不能重复设置雷。

获取雷的数量Get_Mine_Count

C 复制代码
static int Get_Mine_Count(char mine[][COLS], int x, int y,int row)
{
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = -1; i <= 1; i++)
	{
		for (j = -1; j <= 1; j++)
		{
			int nx = x + i;
			int ny = y + j;
			if (nx > 0 && nx <= row && ny > 0 && ny <= COL)
				count += (mine[nx][ny] - '0');
		}
	}
	return count;
}

最好需要检查是否越界,当然外面我们已知初始化为'0',不检查不会有影响。注意任何数字字符-'0'是ASCII码值运算,返回数字本身。注意minexy='0',加上不影响结果。在想不到思路时,给行和列编号发现规律,采用循环可能解决问题。相加是因为只有0,1两种结果,有多少个1就说明有多少个雷。

排雷函数Find_Mine

C 复制代码
void Find_Mine(char mine[][COLS], char show[][COLS], int row)
{
	int x = 0;
	int y = 0;
	int win_frequency = 0;    //成功次数
	int first_click = 1;    //标记是否是第一次点击
	time_t start = time(NULL);
	while (win_frequency < row * COL - Mine_COUNT)
	{
		printf("输入坐标x y\n");
		scanf("%d%d", &x, &y);
		if (x > 0 && x <= row && y > 0 && y <= COL)
		{
			if (show[x][y] == '*')
			{
				if (first_click && mine[x][y] == '1')
				{
					mine[x][y] = '0';
					int nx = 0;
					int ny = 0;
					do
					{
						nx = rand() % row + 1;
						ny = rand() % COL + 1;
					}
					while (mine[nx][ny] == '1' || (nx == x && ny == y));
					mine[nx][ny] = '1';
				}    //防止第一次就碰到雷
				first_click = 0;
				if (mine[x][y] == '0')
				{
					Expand_Blank(mine, show, x, y, row, &win_frequency);
					Display_Board(show, row);
					time_t now = time(NULL);
					double cost = difftime(now, start);
					printf("成功,当前已耗时%.0lf秒\n", cost);
				}
				else
				{
					time_t end = time(NULL);
					double total = difftime(end, start);
					printf("游戏结束,总耗时%.0lf秒\n",total);
					Display_Board(mine, row);
					return;
				}
			}
			else
				printf("重复输入");
		}
		else
			printf("重新输入");
	}
	if (win_frequency == row * COL - Mine_COUNT)
	{
		time_t end = time(NULL);
		double total = difftime(end, start);
		printf("成功,总耗时%.0lf秒\n", total);
	}
}

功能:实现第一次不可能就输:若尚未排查且是第一次排雷,把该位置的雷取消,且随机生成一个新的非雷的位置,把其设为雷。注意必须是新的位置,否则跟不改没区别。若本次排查不是雷,则递归扩展空白,直到成功次数达到格子总数去掉雷的数量。

扩展空白函数Expand_Blank

这个函数的功能是扩展空白直到边界的某个格子周围有雷,采用递归实现

C 复制代码
void Expand_Blank(char mine[][COLS], char show[][COLS], int x, int y, int row, int* win)
{
	if (x < 1 || x > row || y < 1 || y > COL)
		return;
	if (show[x][y] != '*')    
		return;
	int cnt = Get_Mine_Count(mine, x, y,row);
	show[x][y] = (char)(cnt + '0');
	(*win)++;    
	if (cnt > 0)
		return;
	int i = 0;
	int j = 0;
	for (i = -1; i <= 1; i++)
	{
		for (j = -1; j <= 1; j++)
		{
			if (i || j)
				Expand_Blank(mine, show, x + i, y + j, row, win);    
		}
	}
}

注意我们需要mine数组获取每个格子周围雷的个数并且修改show数组,因此需要传递mine和show,这里要检查边界,因为递归范围无法预料!这里必须传址调用(把地址作为参数),尽管地址是值传递,但对应变量不是。因为需要修改win即成功次数的值,每次翻开格子加1。注意cnt+'0'是整型,需强制类型转换为char类型,因为show是字符数组。递归调用Expand_Blank时注意win是指针即地址,不能再取地址。

当然我们知道递归若是层次太深,速度很慢,且空间消耗极大,因为递归需要回到最初的起点,然后才能一步一步返回,每次递归都需要额外开辟空间。

相关推荐
渣男教父11 小时前
Python-lxml 与 XPath 实战
python·编程语言
似璟如你1 天前
Java 开发者的 Go 语法基础:从 0 开始快速上手 Go
java·开发语言·后端·golang·go·编程语言
302wanger3 天前
没有 Stack Overflow,没有自动补全:80 年代的编程体验
编程语言
yiqiefeimeng4 天前
Rust还是Go?编程语言的未来之争
rust·go·编程语言·技术社区·未来之争
计算机魔术师4 天前
传统基础架构 - 微服务
大数据·后端·微服务·架构·开发工具·编程语言
计算机魔术师6 天前
Replit Design 发布:AI 赋能设计愿景
人工智能·ai编程·编程语言
计算机魔术师8 天前
DeepSeek V4 Flash 0731 发布:Agent 能力反超 Pro Preview,开源模型前三易主
人工智能·开源·ai编程·编程语言