[C语言初阶]扫雷小游戏

目录

在上一篇文章中,我们实现了我们的第二个游戏------三子棋小游戏。这次我们继续结合我们之前所学的所有内容,制作出我们的第三个项目------扫雷小游戏。

一、原理及问题分析

说起扫雷,这是一个非常经典的小游戏。扫雷游戏的核心逻辑是通过玩家输入的坐标排查雷的位置,若踩雷则游戏结束,否则显示周围雷的数量。接下来我们先以结构化的方式来宏观分析整个扫雷游戏中的关键要点:

  1. 双棋盘设计

    • mine数组:存储雷的位置(1为雷,0为非雷)。
    • show数组:存储玩家可见的信息(初始为*,排查后显示周围雷数,因为是字符,所以两个数组都是char类型)。
    • 设计意义:若只创建一个数组,雷为1,不是雷为0,若这个坐标周围只有1个雷,则分不清是雷,还是排查出的雷的信息,有歧义,所以再创建一个额外数组,专门用来存放排查出的雷的信息,只打印这个数组即可,用%c打印。
  2. 边界处理

    • 实际使用11x11的数组(通过ROWSCOLS定义),但只操作中间的9x9区域(通过ROWCOL定义)。
    • 目的 :排查雷时边界容易越界,所以要实现9x9棋盘实际是创建11x11的数组才不会越界,。 但不要直接写数字,而是定义行和列的符号,方便后期修改。
  3. 模块化设计

    • test.c:处理菜单、循环流程和用户输入。
    • game.c:实现游戏核心逻辑(初始化、布置雷、排查雷)。
    • game.h:声明函数和定义常量。
  4. 游戏流程(在三子棋和之前的猜数字小游戏中已实现过)

    • 使用do-while循环支持重复游玩。
    • 玩家输入坐标后,通过递归展开无雷区域(进阶功能需自行实现)。

二、代码实现

2.1 分文件结构设计

文件分工与核心函数

  • game.h:定义常量、声明函数。
c 复制代码
// game.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 常量定义:实际操作的棋盘大小为9x9,扩展为11x11避免越界
#define ROW 9    
#define COL 9    
#define ROWS ROW+2
#define COLS COL+2
#define EASY 10  // 默认雷的数量

// 函数声明
void Start(char arr[ROWS][COLS], int rows, int cols, char get); // 初始化棋盘
void Display(char arr[ROWS][COLS], int row, int col);           // 打印棋盘
void Set(char arr[ROWS][COLS], int row, int col);               // 布置雷
int Choose(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col); // 排雷逻辑
  • test.c:主流程和菜单逻辑。
  • game.c:核心功能实现。

2.2 棋盘初始化与打印

1. 初始化函数 Start

  • 功能:将棋盘所有位置初始化为指定字符(mine初始为'0'show初始为'*')因为一个函数要实现两种不同内容初始化,所以多加一个参数。
c 复制代码
// game.c
void Start(char arr[ROWS][COLS], int rows, int cols, char get) 
{
    for (int i = 0; i < rows; i++) 
    {
        for (int j = 0; j < cols; j++) 
        {
            arr[i][j] = get; // 将每个元素设置为传入的字符('0'或'*')
        }
    }
}

2. 打印函数 Display

  • 功能:打印棋盘,为了美化棋盘显示,优化玩家体验,我们添加了行列号和分隔线。
c 复制代码
// game.c
void Display(char arr[ROWS][COLS], int row, int col)
 {
    printf("--------扫雷游戏--------\n");
    // 打印列号(顶部标签)
    printf("   "); // 对齐行号
    for (int i = 1; i <= col; i++)
     {
        printf("%d ", i);
    }
    printf("\n");

    // 打印分隔线
    printf("   ");
    for (int i = 1; i <= col; i++) 
    {
        printf("--");
    }
    printf("\n");

    // 打印棋盘内容(带行号)
    for (int i = 1; i <= row; i++) 
    {
        printf("%d |", i); // 行号+左侧竖线
        for (int j = 1; j <= col; j++) 
        {
            printf("%c ", arr[i][j]); // 打印棋盘元素
        }
        printf("\n");
    }
    printf("--------扫雷游戏--------\n");
}

运行效果

复制代码
--------扫雷游戏--------
   1 2 3 4 5 6 7 8 9 
   -------------------
1 |* * * * * * * * * 
2 |* * * * * * * * * 
...(略)

2.3 布置雷与排查雷

1. 布置雷函数 Set

  • 功能:在9x9区域内随机生成雷。(关于rand函数的用法以及取余的技巧在之前的三子棋和猜数字小游戏中已经详细介绍过,这里不再赘述)
c 复制代码
// game.c
void Set(char arr[ROWS][COLS], int row, int col)
 {
    int count = EASY; // 雷的数量
    while (count) 
    {
        int x = rand() % row + 1; // 生成1~9的随机坐标
        int y = rand() % col + 1;
        if (arr[x][y] == '0') {   // 仅当该位置无雷时布置
            arr[x][y] = '1';      // 标记为雷
            count--;
        }
    }
}

2. 计算周围雷数 get_mine

  • 功能:计算坐标(x,y)周围8个位置的雷数总和。 这个函数可以不用在game.h中声明,因为只是为了在排查雷的函数中临时用的,不会用在其他地方。
c 复制代码
// game.c
int get_mine(char arr[ROWS][COLS], int x, int y) 
{
    // 周围8个坐标的字符值相加('0'或'1'),再减去8*'0'得到实际数字
    return arr[x-1][y-1] + arr[x-1][y] + arr[x-1][y+1] +
           arr[x][y-1]   +               arr[x][y+1]   +
           arr[x+1][y-1] + arr[x+1][y] + arr[x+1][y+1] - 8 * '0';
}

3. 排雷逻辑 Choose

  • 功能:处理玩家输入的坐标,判断是否踩雷或显示周围雷数。
c 复制代码
// game.c
int Choose(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
 {
    int x = 0, y = 0;
    int Win = 0; // 记录已排查的非雷区域数量
    while (Win < ROW * COL - EASY) // 胜利条件:所有非雷区域均被排查
    { 
        printf("请选择排雷坐标(如2 3表示第二行第三列):>");
        scanf("%d %d", &x, &y);
        if (x >= 1 && y >= 1 && x <= row && y <= col)// 坐标合法性检查
         { 
            if (mine[x][y] == '1') // 踩雷
            { 
                printf("很遗憾,你被炸死了!\n");
                Display(mine, ROW, COL); // 展示雷的位置
                break; // 游戏结束
            } 
            else  // 安全坐标
            { 
                int count = get_mine(mine, x, y); // 计算周围雷数
                show[x][y] = count + '0'; // 转换为字符存储(例如3 -> '3')
                Display(show, ROW, COL);
                Win++;
            }
        } 
        else 
        {
            printf("坐标非法!\n");
        }
    }
    if (Win == ROW * COL - EASY) // 胜利条件达成
    { 
        printf("恭喜通关!\n");
        Display(mine, ROW, COL); // 展示雷的位置
    }
}

2.4 游戏主流程实现

test.c:菜单和主循环逻辑。

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

void menu() {
    printf("*********************************\n");
	printf("*********************************\n");
	printf("***********扫雷小游戏************\n");
	printf("*********************************\n");
	printf("***********1.开始游戏************\n");
	printf("***********0.退出游戏************\n");
	printf("*********************************\n");
	printf("**********版本:Beta1.0***********\n");
	printf("**********作者:Yang210***********\n");
	printf("*********************************\n");
	printf("*********************************\n");
	printf("*********************************\n");
}

void game() {
    char mine[ROWS][COLS]; // 存储雷的棋盘
    char show[ROWS][COLS]; // 显示给玩家的棋盘
    Start(mine, ROWS, COLS, '0'); // 初始化雷棋盘为全'0'
    Start(show, ROWS, COLS, '*'); // 初始化显示棋盘为全'*'
    Set(mine, ROW, COL);          // 布置雷
    Display(show, ROW, COL);      // 打印初始棋盘
    Choose(mine, show, ROW, COL); // 进入排雷逻辑
}

int main() {
    srand((unsigned int)time(NULL)); // 设置随机数种子
    int input = 0;
    int add = 0; // 记录游戏次数
    do {
        menu();
        if (add == 0) {
            printf("请选择(输入1开始游戏,输入0退出游戏):>");
        } else {
            printf("是否继续游玩(输入1继续游玩,输入0退出游戏):>");
        }
        scanf("%d", &input);
        switch (input) {
            case 1:
                game();
                add++;
                break;
            case 0:
                printf("已退出游戏\n");
                break;
            default:
                printf("输入错误!\n");
        }
    } while (input); // input为0时退出循环
    return 0;
}

三、后期优化方向

  1. 递归展开:若排查坐标周围无雷(即雷数为0),自动展开相邻区域。
  2. 标记雷 :允许玩家输入特殊指令(如m 3 4)标记可能为雷的位置。
  3. 难度调整 :通过修改EASY的值实现不同难度的雷数设置。
  4. 界面优化:使用Windows API或第三方库(如EasyX)添加图形界面。

简单地总结一下,在这篇文章中,我们通过分文件设计和模块化的运用,将扫雷游戏的逻辑逐一地理清并且给出了后期扩展的方向。我们在代码中通过定义符号常量(如ROWEASY)提高可维护性,方便后期修改,这是一种很重要的编程习惯。我们通过我们现阶段所学的知识,做出了这个经典的游戏,这是对我们所学知识的肯定,也是我们对经典跨越时间的致敬。最后,我想送给大家一句话:"人生没有白走的路,每一步都算数。 "我们的决定,决定了我们。在介绍完了两个C语言的项目之后,下一章,我们将回归C语言的知识学习,介绍一下操作符的相关知识,敬请期待。

作者其他文章链接:
[C语言初阶]三子棋小游戏
[C语言初阶]数组
[C语言初阶]递归
Gitee详细使用教程

相关推荐
网硕互联的小客服1 小时前
如何安全配置数据库(MySQL/PostgreSQL/MongoDB)
linux·运维·服务器·网络·windows
TDengine (老段)3 小时前
TDengine 中集群维护
大数据·运维·数据库·时序数据库·tdengine·涛思数据·物联
weixin_508821653 小时前
STM32H7系列USART驱动区别解析 stm32h7xx_hal_usart.c与stm32h7xx_ll_usart.c的区别?
c语言·stm32·嵌入式硬件
开挖掘机上班3 小时前
linux自有服务
linux·运维·服务器
小黄人软件4 小时前
OpenSSL 与 C++ 搭建一个支持 TLS 1.3 的服务器
服务器·开发语言·c++
wanhengidc4 小时前
算力服务器和GPU服务器之间的联系
运维·服务器
大海里的番茄4 小时前
Wave Terminal + Cpolar:SSH远程访问的跨平台实战+内网穿透配置全解析
运维·ssh
Elastic 中国社区官方博客5 小时前
Elasticsearch Synthetic _source
大数据·运维·elasticsearch·搜索引擎·全文检索·可用性测试
长流小哥6 小时前
STM32:Modbus通信协议核心解析:关键通信技术
服务器·网络·stm32·单片机·嵌入式硬件·信息与通信·modbus
路过的一个普通人6 小时前
C 语言学习笔记二
c语言·笔记·学习