天天看点

写一个简单的C++控制台游戏——扫雷

扫雷 MineSweeper

用过Windows XP的用户一定不会忘记那一款有趣、烧脑(maybe)的扫雷游戏。(反正我是没有怎么玩过)这里将用C++控制台程序简单实现这个扫雷游戏。

Win7下的扫雷就是下面这个画风的:

写一个简单的C++控制台游戏——扫雷

首先,我们分析以下这个游戏:

  • 地图由正方形小方块组成的矩形,每个小方块内要么有雷,要么没有雷
  • 随意点击一个方块开始,如果有雷,爆炸了,则游戏结束,没有爆炸则继续
  • 没有爆炸的方块会显示出以它为中心的九宫格内的雷的数量,为空则表示没有
  • 一次点击后,如果没有爆炸,且周围有雷,则不会显示出多余方块,如果周围没有雷,则会显示出周围所有没有雷的方块,并且显示出数字的方块会作为边界
  • 为什么要这样呢?因为为空白的话,可以直接将周围剩余8个方块依次点击出来(都没有雷),所以电脑帮我们做了这一步,让我们进行接下来的分析

可以看到,上面的过程中最重要的就是:点击的块周围没有雷时,显示出整个可以确定没有雷的一块区域和边界。这很显然要用到广度优先搜索,用队列实现,并不复杂。

实现效果图:

写一个简单的C++控制台游戏——扫雷

因为是纯粹的C++控制台游戏,所以输入坐标进行挖雷。

游戏逻辑就是:每一次接受输入,然后挖雷,刷新状态,清屏,输出,挖到雷就结束。

雷的数量作为输入,自己设置。然后程序随机生成雷的位置。

实现

IDE:VS 2017 Community

文件结构

|
|----block.h        //block类以及Map类的声明
|----stdafx.h       //VS必须有这个头文件,可以将一些所有源文件都能用到的头文件在其中声明
|----block.cpp      //Map类的方法实现
|____game-MineSweeper.cpp       //主函数,逻辑主体
           
  • block.h

    ——

    block

    Map

    类定义
// 2018.4.5 In XDU 
// block class and Map class definition
// IDE: VS 2017 Community

#pragma once
#include <cstdlib>
#include <iostream>
#include <queue>
#include <ctime>
using std::cout;
using std::cin;
using std::endl;
using std::queue;
using std::pair;

const int max_X = ;       //也可以将block改造为模板类,将这两个参数作为模板参数传入接口
const int max_Y = ;       //如果要实现输入控制地图大小,可在堆上分配数组,作为参数传入构造函数,一维数组模拟二维数组即可
const int offset_X = ;     //打印地图时相对纵轴偏移量,下为横轴
const int offset_Y = ;

template<typename T>
T max(T a, T b)
{
    return a > b ? a : b;
}
template<typename T>
T min(T a, T b)
{
    return a < b ? a : b;
}

struct block
{
    bool has_mine;          //是否有雷
    int mine_around_num;    //周围的雷的数量
    bool digged;            //是否已经被挖开,挖开有雷则直接返回,没有则显示周围的雷的数量
};
class Map
{
private:
    block Cube[max_X][max_Y];
public:
    //传入雷的数量,将Cube初始化
    Map(int n);
    //应该要采用广度优先搜索,将所有位置显示出来,挖到雷返回false
    bool dig(int x, int y);
    //打印地图,若碰到有雷炸了的情况,最后由下一个函数清屏打印最终结果
    void print();
    //雷挖炸了之后,将所有位置情况打印出来
    void print_end();
};
           
  • block.cpp

    ——

    Map

    的成员函数实现

其中广搜很容易实现。

print

函数很多都是处理输出格式的内容。

#include "stdafx.h"
#include "block.h"

传入雷的数量,对其进行初始化
Map::Map(int n)
{
    memset(Cube, , sizeof(Cube));
    srand(time());                     //随机数种子
    for (int i = ; i < n; i++)         //随机埋上n个雷
    {
        int x = rand() % max_X;
        int y = rand() % max_Y;
        Cube[x][y].has_mine = true;
        //cout << "(" << x << "," << y << ")" << endl;
    }
    for (int i = ; i < max_X; i++)     //对地图进行初始化
        for (int j = ; j < max_Y; j++)
            for (int x = max(i - , ); x <= min(i + , max_X - ); x++)
                for (int y = max(j - , ); y <= min(j + , max_Y - ); y++)
                    if (Cube[x][y].has_mine)
                        Cube[i][j].mine_around_num++;
}

//应该要采用广度优先搜索,将所有位置显示出来,挖到雷返回false
bool Map::dig(int x, int y)
{
    Cube[x][y].digged = true;
    if (Cube[x][y].has_mine)
        return false;
    else
    {
        if (Cube[x][y].mine_around_num != )
            return true;
        else
        {
            queue<pair<int, int>> Q;            //采用队列进行广度优先搜索
            Q.push({ x,y });
            while (!Q.empty())
            {
                int curx = Q.front().first;     //最初挖出来的位置入队
                int cury = Q.front().second;
                Q.pop();
                for (int i = ; i <= ; i++)    //表示方向,1~4分别为上下左右
                {
                    int tmpx = curx;
                    int tmpy = cury;
                    switch (i)                  //不会出现default的情况
                    {
                    case : tmpx = max(curx - , );            break;
                    case : tmpx = min(curx + , max_X - );    break;
                    case : tmpy = max(cury - , );            break;
                    case : tmpy = min(cury + , max_Y - );    break;
                    }
                    if (!Cube[tmpx][tmpy].digged && !Cube[tmpx][tmpy].has_mine) //没有被dig且没有雷且周围没有雷则入队
                    {
                        if (Cube[tmpx][tmpy].mine_around_num == )  //挖出来的区域的中间位置,即周围没有雷的位置
                        {
                            Q.push({ tmpx,tmpy });
                            Cube[tmpx][tmpy].digged = true;
                        }
                        else
                            Cube[tmpx][tmpy].digged = true;         //被挖出来区域的边界,周围有雷,置其状态但不入队
                    }
                }
            }
        }
        cout << endl;
        return true;
    }
}


//打印地图,就是这样
void Map::print()
{
    for (int oy = ; oy < offset_Y; oy++)
        cout << endl;
    for (int ox = ; ox < offset_X +  + max_X / ; ox++)
        cout << "  ";
    cout << "Y" << endl;
    cout << " ";
    for (int oy = ; oy < offset_X + ; oy++)
        cout << "  ";
    for (int ox = ; ox < max_Y; ox++)
    {
        cout << ox;
        if (ox < )
            cout << " ";
    }
    cout << endl;
    //以上均为控制横向偏移
    for (int i = ; i < max_X; i++)
    {
        for (int ox = ; ox < offset_X; ox++)
            cout << "  ";
        if (i == (max_Y) / )
            cout << "X";
        else
            cout << " ";
        if (i < )
            cout << " ";
        cout << i;
        //以上为纵向偏移控制
        for (int j = ; j < max_Y; j++)
        {
            if (!Cube[i][j].digged)         //没有被挖
                cout << "■";
            else if (Cube[i][j].has_mine)   //被挖了且有雷
                cout << "×";

            else if (Cube[i][j].mine_around_num != )   //被挖了且没有雷且周围有雷
                cout << " " << Cube[i][j].mine_around_num;
            else
                cout << "  ";
        }
        cout << endl;
    }
}


//雷挖炸了之后,将所有位置情况打印出来,也可以选择只用print()打印出踩中的位置
void Map::print_end()
{
    for (int oy = ; oy < offset_Y; oy++)
        cout << endl;
    for (int ox = ; ox < offset_X +  + max_X / ; ox++)
        cout << "  ";
    cout << "Y" << endl;
    cout << " ";
    for (int oy = ; oy < offset_X + ; oy++)
        cout << "  ";
    for (int ox = ; ox < max_Y; ox++)
    {
        cout << ox;
        if (ox < )
            cout << " ";
    }
    cout << endl;
    //以上均为控制横向偏移
    for (int i = ; i < max_X; i++)
    {
        for (int ox = ; ox < offset_X; ox++)
            cout << "  ";
        if (i == (max_Y) / )
            cout << "X";
        else
            cout << " ";
        if (i < )
            cout << " ";
        cout << i;
        //以上为纵向偏移控制
        for (int j = ; j < max_Y; j++)
        {
            if (Cube[i][j].has_mine)        //有雷
                cout << "×";

            else if (Cube[i][j].mine_around_num != )   //没有雷且周围有雷
                cout << " " << Cube[i][j].mine_around_num;
            else
                cout << "  ";
        }
        cout << endl;
    }
}
           
  • game-MIneSweeper.cpp

    ——主函数
// 2018.4.5 In XDU 
// MineSwepper, classic game in Windows XP, a simple implementation with WIN32 console.
// IDE: VS 2017 Community
// author : Tiko
// Github : aojueliuyun
// I'm a student , happy to study annd sahre, welcome to communicate with me !

#include "stdafx.h"
#include "block.h"

int main(void)
{
    int n;
    cout << "一个简单的扫雷游戏,向 Windows XP 的扫雷致敬!" << endl;
    cout << "核心算法:广度优先搜索 " << endl;
    cout << "默认地图大小:20 * 20 , 可在头文件block.h中修改。" << endl;
    cout << "另外横向纵向的偏移量也可在block.h中修改。" << endl;
    cout << "规则:" << endl << "使用■表示未挖开的位置,×表示挖开炸掉了的雷," << endl;
    cout << "数字表示以该格点为中心的九宫格内的雷的数量,空白为没有雷!" << endl;
    cout << "玩法:" << endl << "因为是纯控制台,所以采用做标输入的方式挖雷。" << endl;
    cout << "请输入地雷的数量(20左右为宜):";
    cin >> n;
    Map mymap(n);
    int x, y;
    system("cls");
    while (true)
    {
        mymap.print();
        cout << "\t  请输入坐标(x,y),用空格隔开! ps: x为纵,y为横" << endl;
        cout << "\t\t\t";
        cin >> x >> y;
        while (cin.fail() || x< || x >=  || x >= max_X || y >= max_Y)    //读取输入失败或者输入不在指定范围
        {
            system("cls");
            mymap.print();
            cout << "\t  请输入坐标(x,y),用空格隔开! ps: x为纵,y为横" << endl;
            cout << "\t\t输入有误,请重新输入!" << endl;
            cin.clear();        //更改cin状态标示符
            cin.ignore();       //清空输入缓冲区
            cout << "\t\t\t";
            cin >> x >> y;
        }
        if (!mymap.dig(x, y))
        {
            system("cls");
            mymap.print();      //亦可选择print_end()函数,将所有位置打印出来
            cout << "\t\t你踩中了(" << x << "," << y << ")" << endl;
            cout << "\t\t恭喜客官中雷,慢走不送了,您吶!" << endl;
            break;
        }
        system("cls");      //清屏,很好用的感觉
    }
    cout << "\t\t查看所有雷的位置 y/n ? ";
    char c;
    cin.get();      //读取回车
    if (cin.get(c) && c == 'y')
    {
        system("cls");
        mymap.print_end();
    }
    cout << "\t\t";
    return ;
}
           

另外VS中的

stdafx.h

等文件请自行添加。

结语

本来想写一个控制台的RPG游戏,有等级、经验、地图、道具、材料、装备、NPC等元素的勇士屠龙然后变成龙的RPG游戏的。但是大概很多个小时之后,发现很多地方的设计有问题。所有暂时搁置,等到先把 Bjarne Stroustrup 大佬的 The C++ programming language 看完了之后再来写。所以今天先写了一个扫雷,大概写了两个小时,调Bug又花了两个小时,然后控制格式逻辑什么的又花了大概两个小时。一天就这么就过去了,其实说到底就是一个广度优先搜索而已。以后要提高效率。

源码

Github,直接下载源码。

继续阅读