引言
在这个数字化的时代,游戏已经成为我们生活中不可或缺的一部分。无论是复杂的3D大作,还是简单的桌面小游戏,它们都能带给我们无尽的乐趣和挑战。今天,我们要一起回到那个经典的桌面游戏时代,探索如何用C语言编写一个充满怀旧气息的扫雷小游戏。
一、游戏规则
游戏目标:
盘面上随机分布着一定数量的地雷。你的目标是避开这些地雷,打开其他所有格子。
地雷数量:
选择相应的难度后,电脑会在雷区随机布置一定数量的地雷。
排雷:输入需要排查的坐标。如果点击的是地雷,则游戏失败;如果点击的是非雷方格,会显示周围八个方格内地雷的数。
标记:在怀疑的方格上放置旗帜(本游戏中用$符号代替),标记为地雷。
游戏结束:
当所有非雷方格都被揭开,且所有地雷都被正确标记时,游戏胜利。
如果揭开了一个地雷,游戏失败。
二、设计思路
1. 游戏概述
首先,明确游戏的基本框架和玩法。扫雷游戏主要包括一个雷区、地雷的随机分布、玩家的点击操作以及游戏胜负的判定。
2. 数据结构设计
雷区表示:使用一个二维数组来表示雷区,每个元素对应一个方格。棋盘可操作的区域是9*9的二维字符数组,实际的棋盘要多出两行两列(防止越界,简化设计操作)。
棋盘有两个,一个用来埋雷,一个用来显示排查雷的情况
埋雷数组(逻辑层):这个数组存储了游戏的真实状态,即哪些位置埋有地雷,哪些位置是安全的。这个数组对玩家是不可见的,它用于游戏的内部逻辑处理。其中用‘1’表示有雷,‘0’表示无雷。
展示数组(视图层):这个数组用于向玩家展示游戏当前的状态。它包含了玩家已经点击的方格、标记的地雷以及显示的数字提示。其中‘*’表示待排查的地理,可进行标记操作和排雷操作,‘$’表示已标记的地理,盘面上的数字表示该位置周围一圈格子雷的数量。
3. 游戏流程设计
难度选择:开始游戏前可以选择难度,简单,一般,困难三个级别难度
初始化:初始化两个二维数组
生成雷区:随机布置地雷
显示盘面:打印展示数组供玩家操作
玩家操作:玩家可以选择排雷,标记或是删除标记
逻辑判断:根据玩家的点击,更新显示数组,并进行游戏胜负的判断。
游戏结束:当玩家触发地雷或成功标记所有地雷时,游戏结束。
4. 功能模块划分
难度模块:供玩家选择对应难度。
初始化模块:负责初始化雷区和显示数组。
布雷模块:随机在雷区布置地雷。
显示模块:根据玩家的操作更新显示数组,并打印当前雷区的状态。
标记模块:玩家可以在怀疑的地方做说标记。
胜负判定模块:判断游戏是否结束,并给出相应的提示。
5. 主要算法设计
布雷算法:使用随机数生成器来确定地雷的位置。
计算周围地雷数量:对于每个非雷方格,计算其周围八个方格内地雷的数量。
递归扫雷:当一个格子显示‘0’即周围没有雷时,进行递归扫雷,展开一片区域
三、游戏设计
1.菜单函数
首先,我们需要制作一个简易的游戏菜单,代码如下:
void Menu() { printf("****************************\n"); printf("******* 1.play *******\n"); printf("******* 0.exit *******\n"); printf("****************************\n"); //玩家按1开始游戏,按0则结束游戏 }
2.主函数
主函数实现代码框架,用来控制按1开始游戏/按0退出游戏,并且多次进行直到玩家退出。
这里我们用switch来实现玩家的选择,用do...while循环语句保证游戏的多次进行。代码如下:
其中的srand函数功能以及解释请看上一篇博客中的介绍。代码如下:
int main() { srand((unsigned int)time(NULL));//随机种子 int option; do { system("cls");//用于清除缓冲区,后一次玩的时候清除前面记录 Menu(); printf("请做出你的选择:"); scanf("%d", &option); switch (option) { case 1: system("cls");//清除缓冲区 game(); break; case 0: printf("游戏结束\n"); break; default: printf("输入有误,请重新输入\n"); system("pause");//用来暂停程序,按下后继续运行,即运行下面的清除缓冲区 break; } } while (option); }
现在完成了框架,接下来是各个模块代码的实现
3.选择难度函数
返回值为地理的数量,简单模式8个地雷,正常模式12个地雷,困难模式16个地雷。代码如下:
int SelectDiff() {//难度选择 int difficulty; printf("请选择难度:\n"); printf("1. 简单 8个雷\n"); printf("2. 正常(默认) 12个雷\n"); printf("3. 困难 16个雷\n"); scanf("%d", &difficulty); switch (difficulty) { case 1: return MINE - 4;//简单模式8个雷 break; case 2: return MINE;//正常模式12个雷 break; case 3: return MINE + 4;//困难模式16个雷 break; case 0: break; default: printf("输入有误,请重新选择\n"); break; } return MINE; }
4.初始化函数
该函数用来初始化mine数组和show数组。代码如下:
void InitBoard(char arr[ROWS][COLS], char set)//初始化棋盘 { int i, j; for (i = 0; i < ROWS; i++) { for (j = 0; j < ROWS; j++) { arr[i][j] = set;//初始化棋盘,mine数组中全部初始化为'0',show数组中全部初始化为'*' } } }
5.布置地雷函数
利用生成的随机数,在棋盘上随机位置布置地雷。代码如下:
void SetMine(char arr[ROWS][COLS], int count)//布置地雷 { while (count) { int x = rand() % ROW + 1;//产生1~9的随机数 int y = rand() % COL + 1;//产生1~9的随机数 if (arr[x][y] == '0') { arr[x][y] = '1'; count--; } } }
6.打印函数
用来打印show数组供玩家在盘面上进行操作以及游戏结束时打印mine数组供玩家查看雷区位。代码如下:
void PrintBoard(char arr[ROWS][COLS])//打印棋盘 { int i, j; printf("\n=====Minesweeper=====\n"); for (j = 0; j <= ROW; j++) { printf("%2d", j);//打印列标 } printf("\n"); for (i = 1; i <= ROW; i++) { printf("%2d", i);//打印行标 for (j = 1; j <= COL; j++) { printf("%2c", arr[i][j]); } printf("\n"); } printf("=====================\n"); }
7.计算雷数函数
玩家在该位置排雷后,若该位置没有雷,则计算周围地雷个数,展示在show数组的该位置上。代码如下:
int GetMineCount(char mine[ROWS][COLS], int x, int y)//计算周围雷的个数雷 { return (mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0'); }
8.递归排雷函数
当排查的地方周围无雷时,进行递归,自动深度排查周围区域。代码如下:
void DeepSweep(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)//递归连续排雷 { if (x == 0 || y == 0 || x == ROW + 1 || y == ROW + 1) return; if (show[x][y] != '*' && show[x][y] != '$') return; int count = GetMineCount(mine, x, y); if (count == 0) { show[x][y] = '0'; DeepSweep(mine, show, x - 1, y - 1); DeepSweep(mine, show, x - 1, y); DeepSweep(mine, show, x - 1, y + 1); DeepSweep(mine, show, x, y - 1); DeepSweep(mine, show, x, y + 1); DeepSweep(mine, show, x + 1, y - 1); DeepSweep(mine, show, x + 1, y); DeepSweep(mine, show, x + 1, y + 1); } else { show[x][y] = count + '0'; } }
9.标记(删除标记)函数
供玩家在怀疑的地方标记为地雷,或者删除某一位置的标记,其中标记符号为$。代码如下:
void MarkMine(char show[ROWS][COLS], int x, int y) { if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '*') { show[x][y] = '$'; } } } void UnmarkMine(char show[ROWS][COLS], int x, int y) { if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '$') { show[x][y] = '*'; } } }
10.操作函数
游戏中可供玩家进行排雷、标记、删除标记的选择,在玩家操作后更新展示show数组,并且根据玩家的一系列操作判断来玩家是胜利还是失败。代码如下:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS],int count) { int x, y; int win = 0; int flag = count; while (win < ROW * COL - MINE) {//排查的安全地方个数 // 显示菜单 printf("1. 排雷\n2. 标记雷(当前还可标记%d处)\n3. 删除标记\n0. 退出\n请选择操作:",flag); int choice; scanf("%d", &choice); switch (choice) { case 1: // 排雷 printf("输入你要排查的位置(输入坐标:行 列):"); scanf("%d %d", &x, &y); if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '*' || show[x][y] == '$') { if (mine[x][y] == '1') { printf("很遗憾,你踩到雷了!\n"); PrintBoard(mine); system("pause");//用来暂停程序,按下后继续运行 break; } else { DeepSweep(mine, show, x, y);//递归排雷 int count = GetMineCount(mine, x, y); show[x][y] = count + '0'; system("cls");//清除缓冲区 PrintBoard(show); win++; } } else { printf("该位置已被排除过,请重新输入!\n"); } } break; case 2: // 标记雷 if(flag>0) { printf("输入你要标记的位置(输入坐标:行 列):"); scanf("%d %d", &x, &y); if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '*') { system("cls");//清除缓冲区 MarkMine(show, x, y); PrintBoard(show); flag--; } else { printf("该位置已被排查,无法标记!\n"); } } else { printf("输入位置不合法,请重新输入\n"); } } else { printf("标记达上限,无法再标记!"); } break; case 3: // 删除标记 printf("输入你要删除标记的位置(输入坐标:行 列):"); scanf("%d %d", &x, &y); if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '$') { system("cls");//清除缓冲区 UnmarkMine(show, x, y); PrintBoard(show); } else { printf("该位置未被标记,无法删除!\n"); } } else { printf("输入位置不合法,请重新输入\n"); } break; case 0: // 退出 return; default: printf("输入有误,请重新输入\n"); break; } } if (win == ROW * COL - MINE) {//排查完所有非雷区游戏胜利 printf("我嘞个雷!\n"); printf("恭喜你已经排完了所有的雷!\n"); PrintBoard(mine); } }
11.游戏函数
即整合实现游戏运行的分模块。代码如下:
void game() { int minecount = SelectDiff();//难度选择 char mine[ROWS][COLS] = { 0 };//mine数组全部初始化为'0' char show[ROWS][COLS] = { 0 };//show数组中全部初始化为'*' InitBoard(mine, '0'); InitBoard(show, '*'); SetMine(mine, minecount); //PrintBoard(mine);雷区,取消注释可以作弊查看 PrintBoard(show); FindMine(mine, show,minecount); }
四、完整代码及运行效果图
完整源代码:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<time.h> #define ROW 9//9*9的棋盘 #define COL 9 #define ROWS ROW+2 #define COLS COL+2 #define MINE 12//正常难度雷的数量 void Menu() { printf("****************************\n"); printf("******* 1.play *******\n"); printf("******* 0.exit *******\n"); printf("****************************\n"); //玩家按1开始游戏,按0则结束游戏 } int SelectDiff() {//难度选择 int difficulty; printf("请选择难度:\n"); printf("1. 简单 8个雷\n"); printf("2. 正常(默认) 12个雷\n"); printf("3. 困难 16个雷\n"); scanf("%d", &difficulty); switch (difficulty) { case 1: return MINE - 4;//简单模式8个雷 break; case 2: return MINE;//正常模式12个雷 break; case 3: return MINE + 4;//困难模式16个雷 break; case 0: break; default: printf("输入有误,请重新选择\n"); break; } return MINE; } void InitBoard(char arr[ROWS][COLS], char set)//初始化棋盘 { int i, j; for (i = 0; i < ROWS; i++) { for (j = 0; j < ROWS; j++) { arr[i][j] = set;//初始化棋盘,mine数组中全部初始化为'0',show数组中全部初始化为'*' } } } void PrintBoard(char arr[ROWS][COLS])//打印棋盘 { int i, j; printf("\n=====Minesweeper=====\n"); for (j = 0; j <= ROW; j++) { printf("%2d", j);//打印列标 } printf("\n"); for (i = 1; i <= ROW; i++) { printf("%2d", i);//打印行标 for (j = 1; j <= COL; j++) { printf("%2c", arr[i][j]); } printf("\n"); } printf("=====================\n"); } void SetMine(char arr[ROWS][COLS], int count)//布置地雷 { while (count) { int x = rand() % ROW + 1;//产生1~9的随机数 int y = rand() % COL + 1;//产生1~9的随机数 if (arr[x][y] == '0') { arr[x][y] = '1'; count--; } } } int GetMineCount(char mine[ROWS][COLS], int x, int y)//计算周围雷的个数雷 { return (mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0'); } void DeepSweep(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)//递归连续排雷 { if (x == 0 || y == 0 || x == ROW + 1 || y == ROW + 1) return; if (show[x][y] != '*' && show[x][y] != '$') return; int count = GetMineCount(mine, x, y); if (count == 0) { show[x][y] = '0'; DeepSweep(mine, show, x - 1, y - 1); DeepSweep(mine, show, x - 1, y); DeepSweep(mine, show, x - 1, y + 1); DeepSweep(mine, show, x, y - 1); DeepSweep(mine, show, x, y + 1); DeepSweep(mine, show, x + 1, y - 1); DeepSweep(mine, show, x + 1, y); DeepSweep(mine, show, x + 1, y + 1); } else { show[x][y] = count + '0'; } } void MarkMine(char show[ROWS][COLS], int x, int y) { if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '*') { show[x][y] = '$'; } } } void UnmarkMine(char show[ROWS][COLS], int x, int y) { if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '$') { show[x][y] = '*'; } } } void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS],int count) { int x, y; int win = 0; int flag = count; while (win < ROW * COL - MINE) {//排查的安全地方个数 // 显示菜单 printf("1. 排雷\n2. 标记雷(当前还可标记%d处)\n3. 删除标记\n0. 退出\n请选择操作:",flag); int choice; scanf("%d", &choice); switch (choice) { case 1: // 排雷 printf("输入你要排查的位置(输入坐标:行 列):"); scanf("%d %d", &x, &y); if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '*' || show[x][y] == '$') { if (mine[x][y] == '1') { printf("很遗憾,你踩到雷了!\n"); PrintBoard(mine); system("pause");//用来暂停程序,按下后继续运行 break; } else { DeepSweep(mine, show, x, y);//递归排雷 int count = GetMineCount(mine, x, y); show[x][y] = count + '0'; system("cls");//清除缓冲区 PrintBoard(show); win++; } } else { printf("该位置已被排除过,请重新输入!\n"); } } break; case 2: // 标记雷 if(flag>0) { printf("输入你要标记的位置(输入坐标:行 列):"); scanf("%d %d", &x, &y); if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '*') { system("cls");//清除缓冲区 MarkMine(show, x, y); PrintBoard(show); flag--; } else { printf("该位置已被排查,无法标记!\n"); } } else { printf("输入位置不合法,请重新输入\n"); } } else { printf("标记达上限,无法再标记!"); } break; case 3: // 删除标记 printf("输入你要删除标记的位置(输入坐标:行 列):"); scanf("%d %d", &x, &y); if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (show[x][y] == '$') { system("cls");//清除缓冲区 UnmarkMine(show, x, y); PrintBoard(show); } else { printf("该位置未被标记,无法删除!\n"); } } else { printf("输入位置不合法,请重新输入\n"); } break; case 0: // 退出 return; default: printf("输入有误,请重新输入\n"); break; } } if (win == ROW * COL - MINE) {//排查完所有非雷区游戏胜利 printf("我嘞个雷!\n"); printf("恭喜你已经排完了所有的雷!\n"); PrintBoard(mine); } } void game() { int minecount = SelectDiff();//难度选择 char mine[ROWS][COLS] = { 0 };//mine数组全部初始化为'0' char show[ROWS][COLS] = { 0 };//show数组中全部初始化为'*' InitBoard(mine, '0'); InitBoard(show, '*'); SetMine(mine, minecount); //PrintBoard(mine);雷区,取消注释可以作弊查看 PrintBoard(show); FindMine(mine, show,minecount); } int main() { srand((unsigned int)time(NULL));//随机种子 int option; do { system("cls");//用于清除缓冲区,后一次玩的时候清除前面记录 Menu(); printf("请做出你的选择:"); scanf("%d", &option); switch (option) { case 1: system("cls");//清除缓冲区 game(); break; case 0: printf("游戏结束\n"); break; default: printf("输入有误,请重新输入\n"); system("pause");//用来暂停程序,按下后继续运行,即运行下面的清除缓冲区 break; } } while (option); }
运行效果图:
总结
通过这次C语言扫雷小游戏的开发,我们不仅重温了一个经典的桌面游戏,而且在实践中加深了对C语言编程的理解。从设计思路到具体实现,每一步都是对逻辑思维和编程技能的锻炼。在这个过程中,我们学到了如何利用二维数组管理复杂的游戏状态,如何处理用户输入,以及如何在游戏中实现递归和条件判断等高级功能。这次实践不仅让我们体验了从零到一构建游戏的成就感,也为未来的编程学习奠定了坚实的基础。扫雷游戏虽小,但它背后的编程智慧无穷,让我们继续探索,创造更多有趣的作品。