扫雷的游戏界面让我从一开始就想到了二维数组,事实上用二维数组来定义游戏数据确实是最符合人类思惟的方式。(Square类会在后面解释)git
//游戏数据 private readonly Square[,] _gameData;
有了这个开头,接下来就是填充二维数组的数据了,对于数据,我最初的想法是用int或枚举,固然,这是可行的,但涉及一个问题就是高耦合,全部操做将都在高层执行,难以维护。数组
因而咱们用一个Square类表示一个小方块区。dom
/// <summary> /// 表示游戏中一个方块区 /// </summary> public sealed class Square ...
以枚举表示方块区的状态:spa
/// <summary> /// 方块区状态 /// </summary> public enum SquareStatus { /// <summary> /// 闲置 /// </summary> Idle, /// <summary> /// 已打开 /// </summary> Opened, /// <summary> /// 已标记 /// </summary> Marked, /// <summary> /// 已质疑 /// </summary> Queried, /// <summary> /// 游戏结束 /// </summary> GameOver, /// <summary> /// 标记失误(仅在游戏结束时用于绘制) /// </summary> MarkMissed }
用Game类来表示一局游戏,其中包含游戏数据、游戏等级、雷区数、布雷方法等。.net
/// <summary> /// 游戏对象 /// </summary> public sealed class Game ...
游戏不大,涉及的难点也就很少,但对于刚接触GDI+的读者,一些地方仍是比较麻烦的。code
扫雷游戏有一个附加规则,就是第一次单击不论如何都不会踩到雷区,因为这个规则的存在,咱们不能将布雷操做作在第一次单击以前。因此咱们在游戏开局时假设全部方块区都没有雷。orm
/// <summary> /// 开始游戏 /// </summary> public void Start() { //假设全部方块区均非雷区 for (int i = 0; i < _gameData.GetLength(0); i++) for (int j = 0; j < _gameData.GetLength(1); j++) _gameData[i, j] = new Square(new Point(i, j), false, 0); }
随后,在开局后第一次单击时布雷。对象
/// <summary> /// 布雷 /// </summary> /// <param name="startPt">首次单击点</param> private void Mine(Point startPt) { Size area = new Size(_gameData.GetLength(0), _gameData.GetLength(1)); List<Point> excluded = new List<Point> { startPt }; //随机建立雷区 for (int i = 0; i < _minesCount; i++) { Point pt = GetRandomPoint(area, excluded); _gameData[pt.X, pt.Y] = new Square(pt, true, 0); excluded.Add(pt); } //建立非雷区 for (int i = 0; i < _gameData.GetLength(0); i++) for (int j = 0; j < _gameData.GetLength(1); j++) if (!_gameData[i, j].Mined)//非雷区 { int minesAround = EnumSquaresAround(new Point(i, j)).Cast<Square>().Count(square => square.Mined);//周围雷数 _gameData[i, j] = new Square(new Point(i, j), false, minesAround); } _gameStarted = true; }
先建立雷区,再建立非雷区,以便咱们在建立非雷区时能够计算出非雷区周围的雷数,枚举周围方块的方法咱们用yield建立一个枚举器。递归
/// <summary> /// 枚举周围全部方块区 /// </summary> /// <param name="squarePt">原方块区</param> /// <returns>枚举数</returns> private IEnumerable EnumSquaresAround(Point squarePt) { int i = squarePt.X, j = squarePt.Y; //周围全部方块区 for (int x = i - 1; x <= i + 1; ++x)//横向 { if (x < 0 || x >= _gameData.GetLength(0))//越界 continue; for (int y = j - 1; y <= j + 1; ++y)//纵向 { if (y < 0 || y >= _gameData.GetLength(1))//越界 continue; if (x == squarePt.X && y == squarePt.Y)//排除自身 continue; yield return _gameData[x, y]; } } }
//若是是空白区,则递归相邻的全部空白区 if (_gameData[logicalPt.X, logicalPt.Y].MinesAround == 0) AutoOpenAround(logicalPt);
/// <summary> /// 自动打开周围非雷区方块(递归) /// </summary> /// <param name="squarePt">原方块逻辑坐标</param> private void AutoOpenAround(Point squarePt) { //遍历周围方块 foreach (Square square in EnumSquaresAround(squarePt)) { if (square.Mined || square.Status == Square.SquareStatus.Marked || square.Status == Square.SquareStatus.Opened) continue; square.LeftClick();//打开 //周围无雷区 if (square.MinesAround == 0) AutoOpenAround(square.Location);//递归打开 } }
从二维数组的结构来看,咱们须要遍历整个二维数组,而后把每一个Square绘制到winform上,但这会形成强烈的闪烁效果。由于是实时绘图,绘制的每一步都会实时显示在窗口上,因此咱们看到的效果就是一个方块区一个方块区的出如今窗口上。游戏
为了克服这种不友好的闪烁,双缓冲出现了,思路就是建立一个缓冲区(一般是一个内存中的位图),先将全部方块区绘制到这张位图上,绘制完成后,将位图贴到窗体上,最终效果将再也不出现闪烁的状况。
//窗口图面 private readonly Graphics _wndGraphics; //缓冲区 private readonly Bitmap _buffer; //缓冲区图面 private readonly Graphics _bufferGraphics;
/// <summary> /// 绘制一帧 /// </summary> public void Draw() { for (int i = 0; i < _gameData.GetLength(0); i++) for (int j = 0; j < _gameData.GetLength(1); j++) _gameData[i, j].Draw(_bufferGraphics); _wndGraphics.DrawImage(_buffer, new Point(_gameFieldOffset.Width, _gameFieldOffset.Height)); }
至此,全部难点基本攻破,完整代码你们参考附件,代码基于Windows XP版扫雷作的模仿,笔者能力有限,不足之处请你们多多指点。
http://git.oschina.net/muxiangovo/Mine