C# - 带你玩扫雷

扫雷游戏,大家都应该玩过吧!其实规则也很简单,可是我们想自己实现一个扫雷,我们应该怎么做呢?

TIM截图20171010162223.png

Step1: 知晓游戏原理

扫雷就是要把所有非地雷的格子揭开即胜利;踩到地雷格子就算失败。游戏主区域由很多个方格组成。使用鼠标左键随机点击一个方格,方格即被打开并显示出方格中的数字;方格中数字则表示其周围的8个方格隐藏了几颗雷;如果点开的格子为空白格,即其周围有0颗雷,则其周围格子自动打开;如果其周围还有空白格,则会引发连锁反应;在你认为有雷的格子上,点击右键即可标记雷;如果一个已打开格子周围所有的雷已经正确标出,则可以在此格上同时点击鼠标左右键以打开其周围剩余的无雷格。
1代表1的上下左右及斜角合计有一颗雷,依次轮推,2则有2颗,3则有3颗..
在确实是炸弹的方格上点了旗子,就安全了,不是炸弹的被点了旗子,后面会被炸死的..问号就先不确定这里有没有炸弹,不会存在点错了被炸死的状况..

Step2: 由step1可知,游戏由格子组成,翻译成代码语言就叫做数组,也就是游戏地图就是一个二维数组。格子对象,格子的值即当前雷的数量,那么此时我们暂定雷的数字标识为-1。除此之外,格子对象还有是否被显示,显示当前雷数量等属性,那么我们大概可以定义这样一个类:

 public class CellBlockRole
    {
        /// <summary>
        /// 位于游戏地图中的坐标点X
        /// </summary>
        public int X { get; set; }

        /// <summary>
        /// 位于游戏地图中的坐标点Y
        /// </summary>
        public int Y { get; set; }

        /// <summary>
        /// 是否展示最后格子所代表的结果
        /// </summary>
        public bool IsShowResult { get; set; } = false;

        /// <summary>
        /// 是否计算数字结果
        /// </summary>
        public bool IsComputeResult { get; set; } = false;

        /// <summary>
        /// 是否已经展示过计算结果了
        /// </summary>
        public bool IsHasShowComputed { get; set; } = false;

        /// <summary>
        /// 当前的格子的角色数字, -1:地雷,其他当前雷的数量
        /// </summary>
        public int Number { set; get; } = 0;

        /// <summary>
        /// 是否被Flag标识
        /// </summary>
        public bool IsFlag { get; set; } = false;

        /// <summary>
        /// 是否是雷
        /// </summary>
        public bool IsBoom => Number == -1;

    }

绘制游戏UI画面,见代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using SweeperLibrary.Properties;
using Timer = System.Threading.Timer;

namespace SweeperLibrary
{
    public delegate void OnGameOverDelegate();

    public delegate void OnShowANumberDelegate();

    public delegate void OnPublishGameTimeDelegate(string timeDescription);

    public partial class GameView : UserControl
    {

        /// <summary>
        /// 游戏结束事件
        /// </summary>
        public event OnGameOverDelegate OnGameOverEvent;

        /// <summary>
        /// 当一个格子被点击时,显示当前数字的事件
        /// </summary>
        public event OnShowANumberDelegate OnShowANumberEvent;

        /// <summary>
        /// 发布当前游戏的时间
        /// </summary>
        public event OnPublishGameTimeDelegate OnPublishGameTimeEvent;

        /// <summary>
        /// 游戏绘制地图的每个格子的大小
        /// </summary>
        public static readonly int CellSize = 40;

        /// <summary>
        /// 游戏规模N*N
        /// </summary>
        public static readonly int GameCellCount = 10;

        /// <summary>
        /// 移动方向坐标点改变的数组
        /// </summary>
        public static readonly int[][] MoveDirectionPoints = {
            new[]{-1, -1},
            new[] {0, -1},
            new[] {1, -1},
            new[] {1, 0},
            new[] {1, 1},
            new[] {0, 1},
            new[] {-1, 1},
            new[] {-1, 0}
        };

        /// <summary>
        /// 随机数雷生成对象
        /// </summary>
        private static readonly Random random = new Random(Guid.NewGuid().GetHashCode());
        /// <summary>
        /// 游戏地图标识数组
        /// </summary>
        private CellBlockRole[][] gameMap = new CellBlockRole[GameCellCount][];

        /// <summary>
        /// 雷的数量,默认为10
        /// </summary>
        public int BoomCount { get; set; } = 10;

        /// <summary>
        /// 游戏开始时间
        /// </summary>
        private DateTime gameStartTime;

        /// <summary>
        /// 计时定时器
        /// </summary>
        private System.Windows.Forms.Timer gameTimer = new System.Windows.Forms.Timer();

        public GameView()
        {
            InitializeComponent();
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            InitGame(); //默认游戏已经开始
            SetGameTimer(); //设置游戏定时器
        }

        private void GameView_Paint(object sender, PaintEventArgs e)
        {
            Width = GameCellCount + 1 + GameCellCount * CellSize;
            Height = GameCellCount + 1 + GameCellCount * CellSize;
            //绘制游戏界面
            Graphics graphics = e.Graphics;
            graphics.Clear(Color.WhiteSmoke);
            if (gameMap != null && gameMap.Length > 0 && gameMap[0] != null && gameMap[0].Length > 0)
            {
                for (int y = 0; y < GameCellCount; y++)
                {
                    for (int x = 0; x < GameCellCount; x++)
                    {
                        int dx = x + 1 + x * CellSize,
                            dy = y + 1 + y * CellSize;
                        CellBlockRole cellBlockRole = gameMap[y][x];
                        graphics.FillRectangle(new SolidBrush(cellBlockRole.IsShowResult ? Color.LightSlateGray : Color.WhiteSmoke),
                            dx, dy, CellSize, CellSize);
                        graphics.DrawRectangle(new Pen(Color.LightGray), dx, dy, CellSize, CellSize);
                        if (cellBlockRole.IsShowResult && cellBlockRole.Number != 0)
                        {
                            switch (cellBlockRole.Number)
                            {
                                case -1: //雷
                                    graphics.DrawImage(Image.FromHbitmap(Resources.boom.GetHbitmap()), new RectangleF(dx, dy, CellSize, CellSize));
                                    break;
                                default: //数字
                                    string drawText = cellBlockRole.Number.ToString();
                                    Font textFont = new Font(FontFamily.GenericSansSerif, 12, FontStyle.Bold);
                                    SizeF textSize = graphics.MeasureString(drawText, textFont);
                                    graphics.DrawString(drawText, textFont, new SolidBrush(Color.White),
                                        dx + (CellSize - textSize.Width) / 2, dy + (CellSize - textSize.Height) / 2);
                                    break;
                            }
                        }
                    }
                }
            }
        }

        private void GameView_MouseDown(object sender, MouseEventArgs e)
        {
            int px = (e.X - 1) / (CellSize + 1),
                py = (e.Y - 1) / (CellSize + 1);
            switch (e.Button)
            {
                case MouseButtons.Left: //鼠标左键
                    if (!gameMap[py][px].IsShowResult)
                    {
                        if (gameMap[py][px].IsBoom)
                        {
                            new Thread(() =>
                            {
                                ShowAllCellBlockRoleNumber();
                                if (this.InvokeRequired)
                                {
                                    MethodInvoker del = Invalidate;
                                    this.Invoke(del);
                                } else
                                {
                                    Invalidate();
                                }
                            }).Start();
                            gameTimer.Stop();
                            OnGameOverEvent?.Invoke();
                        } else
                        {
                            new Thread(() =>
                            {
                                ShowNeiborhoodCellRolesByPosi(px, py);
                                if (this.InvokeRequired)
                                {
                                    MethodInvoker del = Invalidate;
                                    this.Invoke(del);
                                } else
                                {
                                    Invalidate();
                                }
                            }).Start();
                            OnShowANumberEvent?.Invoke();
                        }
                    }
                    break;
                case MouseButtons.Right: //鼠标右键
                    break;
            }
        }

        /// <summary>
        /// 初始化游戏
        /// </summary>
        private void InitGame()
        {
            new Thread(() =>
            {
                InitGameMap();
                GenerateBooms();
                if (this.InvokeRequired)
                {
                    MethodInvoker del = Invalidate;
                    this.Invoke(del);
                } else
                {
                    Invalidate();
                }
            }).Start();
        }

        /// <summary>
        /// 设置游戏定时器
        /// </summary>
        private void SetGameTimer()
        {
            gameTimer.Interval = 1000;
            gameTimer.Enabled = true;
            gameTimer.Tick += (sender, args) =>
            {
                long dMillisecond = DateTime.Now.Millisecond - gameStartTime.Millisecond;
                long hour = dMillisecond / 60 / 60 / 1000;
                long minute = (dMillisecond - hour * (60 * 60 * 1000)) / (60 * 1000);
                long second = ((dMillisecond - hour * (60 * 60 * 1000)) % (60 * 1000)) / 1000;
                OnPublishGameTimeEvent?.Invoke((hour > 0 ? (hour > 9 ? hour.ToString() : "0" + hour) + ":" : "")
                                               + (minute > 9 ? minute.ToString() : "0" + minute) + ":" + (second > 9 ? second.ToString() : "0" + second));
            };
        }

        /// <summary>
        /// 初始化游戏地图
        /// </summary>
        private void InitGameMap()
        {
            for (int i = 0; i < GameCellCount; i++)
            {
                gameMap[i] = new CellBlockRole[GameCellCount];
                for (int j = 0; j < GameCellCount; j++)
                {
                    gameMap[i][j] = new CellBlockRole
                    {
                        X = j,
                        Y = i
                    };
                }
            }
            gameStartTime = DateTime.Now;
            gameTimer.Start();
        }

        /// <summary>
        /// 重置游戏地图
        /// </summary>
        public void ResetGameMap()
        {
            new Thread(() =>
            {
                for (int i = 0; i < GameCellCount; i++)
                {
                    for (int j = 0; j < GameCellCount; j++)
                    {
                        gameMap[i][j].X = j;
                        gameMap[i][j].Y = i;
                        gameMap[i][j].Number = 0;
                        gameMap[i][j].IsShowResult = false;
                        gameMap[i][j].IsComputeResult = false;
                        gameMap[i][j].IsHasShowComputed = false;

                    }
                }
                GenerateBooms(); //生成一些雷
                if (this.InvokeRequired)
                {
                    MethodInvoker del = Invalidate;
                    this.Invoke(del);
                } else
                {
                    Invalidate();
                }
            }).Start();
            gameStartTime = DateTime.Now;
            gameTimer.Start();
        }

        /// <summary>
        /// 随机生成一些地雷
        /// </summary>
        public void GenerateBooms()
        {
            for (int i = 0; i < BoomCount; i++)
            {
                int boomNumberIndex = random.Next(0, GameCellCount * GameCellCount - 1); //生成随机数的范围
                int boomX = boomNumberIndex % GameCellCount,
                    boomY = boomNumberIndex / GameCellCount;
                if (gameMap[boomY][boomX].Number == 0)
                    gameMap[boomY][boomX].Number = -1; //-1表示雷
                else // 已经存在雷了,所以要重新处理
                    i--;
            }
            MakeAllNumberComputeInCellRole(0, 0); //默认从坐标(0,0)开始
        }

        /// <summary>
        /// 显示所有的格子的信息
        /// </summary>
        private void ShowAllCellBlockRoleNumber()
        {
            for (int i = 0; i < GameCellCount; i++)
            {
                for (int j = 0; j < GameCellCount; j++)
                {
                    gameMap[i][j].IsShowResult = true;
                }
            }
        }

        /// <summary>
        /// 显示某点周边所有格子的数字
        /// </summary>
        /// <param name="posiX">X轴坐标</param>
        /// <param name="posiY">Y轴坐标</param>
        private void ShowNeiborhoodCellRolesByPosi(int posiX, int posiY)
        {
            gameMap[posiY][posiX].IsShowResult = true;
            gameMap[posiY][posiX].IsHasShowComputed = true;
            int boomCount = GetBoomCountInNeiborhood(posiX, posiY);
            if (boomCount == 0) //如果周围没有雷,则翻开所有8个方向的相关数字
            {
                for (int i = 0; i < MoveDirectionPoints.Length; i++)
                {
                    int[] itemPosi = MoveDirectionPoints[i];
                    int rx = posiX + itemPosi[0],
                        ry = posiY + itemPosi[1];
                    bool isNotOutIndexRange = rx >= 0 && rx < GameCellCount && ry >= 0 && ry < GameCellCount;
                    if (isNotOutIndexRange) //防止坐标溢出
                    {
                        gameMap[ry][rx].IsShowResult = true;
                        if (!gameMap[ry][rx].IsHasShowComputed && gameMap[ry][rx].Number == 0)
                            ShowNeiborhoodCellRolesByPosi(rx, ry);
                    }
                }
            }
        }

        /// <summary>
        /// 获取某点附近的雷数量
        /// </summary>
        /// <param name="posiX">X轴坐标点</param>
        /// <param name="posiY">Y轴坐标点</param>
        /// <returns></returns>
        private int GetBoomCountInNeiborhood(int posiX, int posiY)
        {
            int boomCount = 0;
            for (int i = 0; i < MoveDirectionPoints.Length; i++)
            {
                int[] itemPosi = MoveDirectionPoints[i];
                int rx = posiX + itemPosi[0],
                    ry = posiY + itemPosi[1];
                bool isNotOutIndexRange = rx >= 0 && rx < GameCellCount && ry >= 0 && ry < GameCellCount;
                if (isNotOutIndexRange && gameMap[ry][rx].IsBoom) //防止坐标溢出
                {
                    boomCount++;
                }
            }
            return boomCount;
        }

        /// <summary>
        /// 计算每个格子的数字标识
        /// </summary>
        /// <param name="posiX">X轴坐标</param>
        /// <param name="posiY">Y轴坐标</param>
        private void MakeAllNumberComputeInCellRole(int posiX, int posiY)
        {
            int boomCount = GetBoomCountInNeiborhood(posiX, posiY);
            if (boomCount != 0) //如果周围没有雷,则计算周围的8个方向的格子
            {
                gameMap[posiY][posiX].Number = boomCount;
            } else
            {
                if (!gameMap[posiY][posiX].IsBoom)
                    gameMap[posiY][posiX].Number = 0;
            }
            gameMap[posiY][posiX].IsComputeResult = true;
            for (int i = 0; i < MoveDirectionPoints.Length; i++)
            {
                int[] itemPosi = MoveDirectionPoints[i];
                int rx = posiX + itemPosi[0],
                    ry = posiY + itemPosi[1];
                bool isNotOutIndexRange = rx >= 0 && rx < GameCellCount && ry >= 0 && ry < GameCellCount;
                if (isNotOutIndexRange && !gameMap[ry][rx].IsComputeResult && !gameMap[ry][rx].IsBoom) //防止坐标溢出
                {
                    MakeAllNumberComputeInCellRole(rx, ry);
                }
            }
        }

    }

}

主要代码已经实现,现已知现有代码的定时器由问题,暂时不支持Flag(旗子标识)。当然代码中还有其他不足的地方,游戏持续优化中。。。

源代码Git地址:https://gitee.com/yugecse/MineSweeper-CShape

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容