[特殊字符] C语言实战:数组与函数实践——扫雷游戏完整教程

📑 目录

  • [📖 1. 扫雷游戏分析和设计](#📖 1. 扫雷游戏分析和设计)
    • [🎯 1.1 扫雷游戏的功能说明](#🎯 1.1 扫雷游戏的功能说明)
    • [🧠 1.2 游戏的分析与设计](#🧠 1.2 游戏的分析与设计)
      • [🏗️ 1.2.1 数据结构的分析](#🏗️ 1.2.1 数据结构的分析)
      • [📁 1.2.2 文件结构设计](#📁 1.2.2 文件结构设计)
  • [💻 2. 扫雷游戏的代码实现](#💻 2. 扫雷游戏的代码实现)
    • [📄 game.h ------ 头文件与声明](#📄 game.h —— 头文件与声明)
    • [⚙️ game.c ------ 核心函数实现](#⚙️ game.c —— 核心函数实现)
    • [🚀 test.c ------ 游戏主逻辑](#🚀 test.c —— 游戏主逻辑)
  • [🚀 3. 扫雷游戏的扩展](#🚀 3. 扫雷游戏的扩展)
  • [📝 小总结](#📝 小总结)

📖 1. 扫雷游戏分析和设计

🎯 1.1 扫雷游戏的功能说明

扫雷是一款经典的单人益智游戏,我们将使用C语言在控制台中实现它。游戏具备以下核心功能:

  • 🕹️ 菜单交互:游戏启动时显示菜单,玩家可以选择"开始游戏"或"退出游戏"。
  • 🗺️ 标准棋盘 :游戏棋盘为 9×9 的格子。
  • 💣 随机布雷 :系统会默认随机布置10个雷
  • 🔍 排查雷机制
    • 如果玩家点击的位置不是雷,则显示该位置周围8个格子中雷的总数。
    • 如果点击的位置是雷,则游戏结束,玩家被"炸死"。
    • 如果玩家成功找出所有非雷格子(即排除了除10个雷之外的所有格子),则游戏胜利。

游戏界面示意:

  • 初始界面 :棋盘上所有格子均显示为 *,代表未知区域。
  • 排雷界面:玩家输入坐标后,该位置会显示周围雷的数量(数字1-8),或显示空白(周围无雷)。
  • 排雷失败界面:踩到雷后,游戏会展示所有雷的位置,并提示"很遗憾,你被炸死了"。

🧠 1.2 游戏的分析与设计

🏗️ 1.2.1 数据结构的分析

在扫雷过程中,我们需要存储两类信息:雷的布局信息玩家排查出的雷的数量信息 。因此,我们需要设计合适的数据结构。

1. 基础棋盘设计

最直观的想法是创建一个 9×9 的二维数组 来存储信息。我们规定:

  • 0:表示该位置没有雷。
  • 1:表示该位置有雷。

2. 边界问题与解决方案

当我们排查一个格子时,需要统计其周围8个格子的雷数。例如,排查坐标 (2,5) 时,访问其周围的黄色格子是安全的。但是,如果排查棋盘边缘的格子,比如 (8,6),访问其下方的格子时就会发生数组越界

排雷的假设:

周围加上一圈后的棋盘

为了解决这个问题,我们采用一个经典的技巧:将数组扩大一圈 。即创建一个 11×11 的数组 ,只在中间的 9×9 区域内布置雷,外围一圈全部初始化为 0(无雷)。这样,在统计任何中间格子的周围雷数时,都不会发生越界。

3. 信息分离:双数组策略

如果我们将雷的信息(0/1)和排查出的雷的数量信息(0-8)都放在同一个数组中,会造成数据混淆。例如,数组中的数字 1 到底代表"这里有1个雷"还是"周围有1个雷"?

为了解决这个问题,我们采用双数组方案:

  • mine 数组 :专门用来存放布置好的雷的信息。初始化全为 '0',布置雷的位置改为 '1'
  • show 数组 :专门用来存放排查出的雷的数量信息,并展示给玩家。初始化全为 '*'(神秘符号),排查后显示对应的数字。

这样做的好处是:

  • 职责单一,互不干扰。
  • 两个数组类型一致(都是 char 类型),可以使用同一套函数进行处理,提高代码复用性。
    mine数组布置雷后的状态:

    show输出初始化的状态:

对应的数组定义如下:

c 复制代码
char mine[11][11] = {0}; // 用来存放布置好的雷的信息
char show[11][11] = {0}; // 用来存放排查出的雷的个数信息
📁 1.2.2 文件结构设计

为了代码的模块化和清晰性,我们采用多文件组织方式,将游戏代码分为三个文件:

  • test.c :游戏的测试逻辑,包含 main 函数和游戏菜单。
  • game.c:游戏核心函数的实现,如初始化棋盘、打印棋盘、布雷、排雷等。
  • game.h:头文件,包含所需库的引入、常量的定义以及所有函数的声明。

💻 2. 扫雷游戏的代码实现

📄 game.h ------ 头文件与声明

c 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.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 rows, int cols, char set);
// 打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
// 布置雷
void SetMine(char board[ROWS][COLS], int row, int col);
// 排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

⚙️ game.c ------ 核心函数实现

c 复制代码
#include "game.h"

// 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;
        }
    }
}

// 2. 打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
    int i = 0;
    printf("-------- 扫雷游戏 -------\n");
    // 打印列号
    for (i = 0; i <= col; i++)
    {
        printf("%d ", i);
    }
    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");
    }
}

// 3. 布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
    int count = EASY_COUNT;
    while (count)
    {
        int x = rand() % row + 1;
        int y = rand() % col + 1;
        if (board[x][y] == '0')
        {
            board[x][y] = '1';
            count--;
        }
    }
}

// 4. 统计某坐标周围雷的数量
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
    return (mine[x-1][y] + mine[x-1][y-1] + mine[x][y-1] + mine[x+1][y-1] +
            mine[x+1][y] + mine[x+1][y+1] + mine[x][y+1] + mine[x-1][y+1] - 8 * '0');
}

// 5. 排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int x = 0;
    int y = 0;
    int win = 0; // 记录已排查的非雷格子数

    while (win < row * col - EASY_COUNT)
    {
        printf("请输入要排查的坐标:>");
        scanf("%d %d", &x, &y);
        if (x >= 1 && x <= row && y >= 1 && y <= col)
        {
            if (mine[x][y] == '1')
            {
                printf("💥 很遗憾,你被炸死了!\n");
                DisplayBoard(mine, ROW, COL);
                break;
            }
            else
            {
                // 该位置不是雷,统计周围雷数
                int count = GetMineCount(mine, x, y);
                show[x][y] = count + '0';
                DisplayBoard(show, ROW, COL);
                win++;
            }
        }
        else
        {
            printf("❌ 坐标非法,请重新输入!\n");
        }
    }
    if (win == row * col - EASY_COUNT)
    {
        printf("🎉 恭喜你,排雷成功!\n");
        DisplayBoard(mine, ROW, COL);
    }
}

🚀 test.c ------ 游戏主逻辑

c 复制代码
#include "game.h"

void menu()
{
    printf("***********************\n");
    printf("*****   1. play   *****\n");
    printf("*****   0. exit   *****\n");
    printf("***********************\n");
}

void game()
{
    char mine[ROWS][COLS]; // 存放布置好的雷
    char show[ROWS][COLS]; // 存放排查出的雷的信息

    // 1. 初始化棋盘
    InitBoard(mine, ROWS, COLS, '0');
    InitBoard(show, ROWS, COLS, '*');

    // 2. 打印棋盘
    DisplayBoard(show, ROW, COL);

    // 3. 布置雷
    SetMine(mine, ROW, COL);

    // 4. 排查雷
    FindMine(mine, show, ROW, COL);
}

int main()
{
    int input = 0;
    srand((unsigned int)time(NULL)); // 设置随机数种子
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            game();
            break;
        case 0:
            printf("👋 退出游戏\n");
            break;
        default:
            printf("❌ 选择错误,请重新选择!\n");
            break;
        }
    } while (input);
    return 0;
}

🚀 3. 扫雷游戏的扩展

以上实现了一个基础的扫雷游戏,但它还有很多可以优化的地方。以下是一些常见的扩展方向,也是面试中可能会被问到的问题:

  • 🎚️ 游戏难度选择
    • 简单:9×9 棋盘,10个雷。
    • 中等:16×16 棋盘,40个雷。
    • 困难:30×16 棋盘,99个雷。
  • 🌊 展开功能:如果排查的位置不是雷,且周围也没有雷,可以自动展开周围的一片区域,提升游戏体验。
  • 🚩 标记雷功能 :允许玩家对怀疑是雷的位置进行标记(例如用 !? 符号)。
  • ⏱️ 计时功能:显示玩家完成游戏所用的时间,增加挑战性。

在线扫雷游戏参考http://www.minesweeper.cn/

📝 小总结

通过本次扫雷游戏的实战,我们深入实践了C语言中的以下核心知识点:

  1. 数组:使用二维数组来模拟游戏棋盘,并学习了通过"扩大一圈"的技巧来解决数组越界问题。
  2. 函数:将游戏的不同功能(初始化、打印、布雷、排雷)封装成独立的函数,提高了代码的可读性和可维护性。
  3. 多文件编程:将声明、实现和测试逻辑分离到不同的文件中,是大型项目开发的基础。
  4. 模块化设计思想:通过"双数组"策略,将雷的信息和排查信息分离,避免了数据混淆,体现了良好的设计思路。