C#编写的扫雷游戏

我突发奇想,想用C#编写了一个扫雷游戏,在网上搜了半天,结果一无所获。

在图书馆翻了半天的书,硬着头皮把一本很厚很厚的书看完了,总之,还是觉得收获挺大。

为了能够学以致用,我开始通过C#来编写扫雷游戏。

废话少说,我们先来了解一下扫雷游戏的一些功能:

1.首先是界面设计:通过不同颜色大小的画刷画笔以及在窗体上绘制直线,将窗体分成两个区域。一个用来显示时间和雷数还有复位按钮,一个用来装载棋盘。


哎!完成这个东西其实很不容易,需要一些图片资源,要分析它们的尺寸,等等,图片上所有的资源都来自这个:
                                                      

就是将它们的一部分截下来放在窗体的某一位置,呵呵,而你的工作就是以一种可视化有趣的方式不断的变换变量之间的次序。

想要代码吗?我觉得其实用处不大,关键得看懂,我也知道,一般在短时间内是不易看懂的,因为我做这个也花了不少时间。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 扫雷
{
    class frame
    {
        //定义框架的尺寸
        private int TitleHeight = 30;//标题栏高度
        private int MenuBarHeight = 25;//主菜单高度
        private int LineWidth_1 = 1;//横向边沿宽度
        private int LineWidth_2 = 2;//纵向边沿宽度
        private int GrayWidth_1 = 3;//横向灰框宽度
        private int GrayWidth_2 = 4;//纵向灰框宽度
        private int SmallShellHeight = 37;//放置显示信息栏的宽度
        private int MineWidth = 20;//雷的宽度*
        //readonly int FaceWidth = 24;//笑脸的宽度
        //readonly int DigitWidth = 13;//显示时间数字的宽度
        //readonly int DigitHeight = 23;//显示时间数字的高度
        //定义框架的颜色
        private Color GRAY = Color.FromArgb(192, 192, 192);
        private Color DARK_GRAY = Color.FromArgb(128, 128, 128);
        private Color WHITE = Color.FromArgb(255, 255, 255);
        private int WindowWidth, WindowHeight;
        public int Window_Width
        {
            get
            {
                return WindowWidth;
            }
            set
            {
                WindowWidth=value;
            }
        }
        public int Window_Height
        {
            get
            {
                return WindowHeight;
            }
            set
            {
                WindowHeight = value;
            }
        }
        public  int Form1WidthNum(int ColNum)
        {
            return LineWidth_2 *9 + GrayWidth_2*5 + ColNum  *MineWidth;
        }
        public int Form1HeightNum(int RowNum)
        {
            return TitleHeight + MenuBarHeight + LineWidth_1 * 2 + LineWidth_1 * 3 + GrayWidth_2*6 + SmallShellHeight + RowNum * MineWidth;
        }
        public void DrawFrame(Graphics graphic, out Rectangle FaceAndDigitArea, out Rectangle MinesArea)
        {
            Pen penDarkShadow_Narrow = new Pen(DARK_GRAY, LineWidth_1);//横向边沿暗灰色细线
            Pen penDarkShadow_Wide = new Pen(DARK_GRAY, LineWidth_2);//纵向边沿暗灰色粗线
            Pen penLightShadow_Narrow = new Pen(WHITE, LineWidth_1);//横向边沿亮白色细线
            Pen penLightShadow_Wide = new Pen(WHITE, LineWidth_2);//纵向边沿亮白色粗线
            Point p_LeftTop = new Point(LineWidth_2*2 + GrayWidth_2, MenuBarHeight + LineWidth_1*2 + GrayWidth_1);
            Point p_RightBottom = new Point(WindowWidth - LineWidth_2 * 2 - GrayWidth_2, p_LeftTop.Y + SmallShellHeight);
            FaceAndDigitArea = new Rectangle(p_LeftTop.X, p_LeftTop.Y, p_RightBottom.X - p_LeftTop.X, p_RightBottom.Y - p_LeftTop.Y);//显示信息框
            //绘制外边框
            graphic.DrawLine(penLightShadow_Narrow, 1, MenuBarHeight + 1, WindowWidth, MenuBarHeight + 1);
            graphic.DrawLine(penLightShadow_Wide, 1, MenuBarHeight + 1, 1, WindowHeight);
            graphic.DrawLine(penDarkShadow_Narrow, 1, WindowHeight - 2, WindowWidth, WindowHeight - 2);
            graphic.DrawLine(penDarkShadow_Wide, WindowWidth - 1, WindowHeight, WindowWidth - 1, MenuBarHeight + 1);
            //绘制信息框
            graphic.DrawLine(penDarkShadow_Narrow, p_LeftTop.X, p_LeftTop.Y, p_RightBottom.X, p_LeftTop.Y);
            graphic.DrawLine(penLightShadow_Wide, p_RightBottom.X, p_LeftTop.Y, p_RightBottom.X, p_RightBottom.Y);
            graphic.DrawLine(penLightShadow_Narrow, p_RightBottom.X, p_RightBottom.Y, p_LeftTop.X, p_RightBottom.Y);
            graphic.DrawLine(penDarkShadow_Wide, p_LeftTop.X, p_RightBottom.Y, p_LeftTop.X, p_LeftTop.Y);
            p_LeftTop.Y += SmallShellHeight + 2*LineWidth_1 + GrayWidth_1;
            p_RightBottom.Y = WindowHeight - 2 * LineWidth_1 - GrayWidth_1-1;
            MinesArea = new Rectangle(p_LeftTop.X + 3, p_LeftTop.Y + 2, p_RightBottom.X - p_LeftTop.X - 7, p_RightBottom.Y - p_LeftTop.Y - 5);
            //画主界面框
            graphic.DrawLine(penDarkShadow_Wide, p_LeftTop.X, p_LeftTop.Y, p_RightBottom.X, p_LeftTop.Y);
            graphic.DrawLine(penLightShadow_Wide, p_RightBottom.X, p_LeftTop.Y, p_RightBottom.X, p_RightBottom.Y);
            graphic.DrawLine(penLightShadow_Wide, p_RightBottom.X, p_RightBottom.Y, p_LeftTop.X, p_RightBottom.Y);
            graphic.DrawLine(penDarkShadow_Wide, p_LeftTop.X, p_RightBottom.Y, p_LeftTop.X, p_LeftTop.Y);
        }

    }
}
不管是什么游戏,产生随机数,这个少不了!我们需要编写一个类,内容如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 扫雷
{
    class GetRandom
    {
        public static Random globalRandomGenerator= GenerateNewRandomGenerator();
        public static Random GenerateNewRandomGenerator()
        {
            globalRandomGenerator= new Random((int)DateTime.Now.Ticks);
            return globalRandomGenerator;
        }
        public static int GetRandomInt()//随机产生0至9的一个整数
        {
            return globalRandomGenerator.Next(10);
        }
        public static int GetRandomInt(int max)
        {
            return globalRandomGenerator.Next(max);
        }
        //产生一个随机浮点数组
        public static float[] GenerateRandomOrder(int n)
        {
            float[] RandomOrder=new float[n];
            for (int i = 0; i < n; i++)
            {
                RandomOrder[i] =GetRandom.GetRandomFloat(0.0f,1.0f);
            }
            return RandomOrder;
        }
        public static int GetRandomInt(int min, int max)
        {
            return globalRandomGenerator.Next(max - min) + min;
        }
        public static float GetRandomFloat(float min, float max)
        {
            return (float)globalRandomGenerator.NextDouble() * (max - min) + min;
        }
        //将一串随机数顺序打乱
        public static int[] ArrRandom(int[] Arr)
        {
            int k = 0;
            int strtmp ;
            for (int i = 0; i < Arr.Length; i++)
            {
                k = GetRandomInt(Arr.Length);
                if (k != i)
                {
                    strtmp = Arr[i];
                    Arr[i] = Arr[k];
                    Arr[k] = strtmp;
                }
            }
            return Arr;
        }
    }
}



当然这只是完成了边框的绘制,为了使游戏更有趣,更具可玩性,我们不妨来让地雷的布局由用户自己定义,呵呵,这就好玩多了!


允许用户自己定义地雷的布局,行列数,当然,一些简单的菜单控件的布置我就不说了,只需一拖。上图:

自定义布雷:


这个游戏的核心部分就是递归算法。还有许多辅助函数,由于内容太多,工作量比较大,我只好一股脑粘贴啦!

#region 几个重要的自定义函数
        //放置雷的函数
        private void LayMines()
        {
            if (自定义布雷toolStripMenuItem2.Text == "自定义布雷")
            {
                for (int LayedNums = 0; LayedNums < MinesNum; )
                {
                    int row = GetRandom.GetRandomInt(RowsNum);
                    int col = GetRandom.GetRandomInt(ColsNum);
                    if (row == NewIndex / ColsNum && col == NewIndex % ColsNum)
                        continue;
                    if (!mineGrids[row, col].IsMine)
                    {
                        mineGrids[row, col].IsMine = true;
                        LayedNums++;
                    }
                }
            }
        }
        //获得某个方格周围雷的个数的函数
        private int GetAroundNums(MineGrid mineGrid)
        {
            int row = mineGrid.Row;
            int col = mineGrid.Col;
            int aroundNums = 0;
            int minRow = row == 0 ? 0 : row - 1;
            int maxRow = row == RowsNum - 1 ? row : row + 1;
            int minCol = col == 0 ? 0 : col - 1;
            int maxCol = col == ColsNum - 1 ? col : col + 1;
            for (int i = minRow; i <= maxRow; i++)
            {
                for (int j = minCol; j <= maxCol; j++)
                {
                    if (!(i == row && j == col) && mineGrids[i, j].IsMine)
                        aroundNums++;
                }
            }
            return aroundNums;
        }
        /// <summary>
        /// 当由于某种操作(可能是左键点击了一个恰好周围没有一个雷的方格,也可能是鼠标左右键点
        /// 击了一个状态为数字的方格而触发了某个周围没有一个雷的方格被显示),一个周围没有一个
        /// 雷的方格被显示,此时必须显示这个方格周围方格的周围雷的个数,而这可能发生一系列的连
        /// 锁反应,导致一片方格被展开,这个过程的实现依靠递归函数Expand(MineGrid mineGrid)来实现:
        /// </summary>
        /// <param name="mineGrid"></param>
        private void Expand(MineGrid mineGrid)
        {
            ExpandNums++;
            int row = mineGrid.Row;
            int col = mineGrid.Col;
            int minRow = row == 0 ? 0 : row - 1;
            int maxRow = row == RowsNum - 1 ? row : row + 1;
            int minCol = col == 0 ? 0 : col - 1;
            int maxCol = col == ColsNum - 1 ? col : col + 1;
            int aroundNums = GetAroundNums(mineGrid);
            mineGrid.State = 15 - aroundNums;
            mineGrid.OldState = 15 - aroundNums;
            if (aroundNums == 0)
            {
                for (int i = minRow; i <= maxRow; i++)
                {
                    for (int j = minCol; j <= maxCol; j++)
                    {
                        if (!(i == row && j == col) &&mineGrids[i, j].State == (int)MineGridState.Normal&& (!mineGrids[i, j].IsMine))
                        {
                            Expand(mineGrids[i, j]);
                        }
                    }
                }
            }
        }
        /// <summary>
        /// 当游戏失败时,必须显示还有哪些为雷的方格没有被确认,void Dead()函数:
        /// </summary>
        private void Dead()
        {
           gameState = (int)GameState.Dead;
           int flagsNum = 0;
           for (int row = 0; row < RowsNum; row++)
                for (int col = 0; col < ColsNum; col++)
                {
                    if (row == NewIndex / ColsNum && col == NewIndex % ColsNum) continue;
                    if (mineGrids[row, col].State == (int)MineGridState.Empty && mineGrids[row, col].OldState ==(int)MineGridState.Normal)
                    {
                        mineGrids[row, col].State = (int)MineGridState.Normal;
                    }
                    if (mineGrids[row, col].IsMine &&
                     mineGrids[row, col].State ==
                     (int)MineGridState.Normal)
                    {
                        mineGrids[row, col].State = (int)MineGridState.Mine;
                        mineGrids[row, col].OldState = (int)MineGridState.Mine;
                    }
                    if (mineGrids[row, col].State == (int)MineGridState.Flag)
                        flagsNum++;
                }
           MinesNum = RowsColsNums[Difficulty * 3 + 2] - flagsNum;
           FaceIndex = (int)FaceStyle.Dead;
           if (bSoundful)
               Play("dead");
           this.GameTimer.Stop();
        }
        //游戏胜利的判断函数:
        private bool Victory()
        {
            if (gameState == (int)GameState.Dead)
                return false;
            for (int row = 0; row < RowsNum; row++)
                for (int col = 0; col < ColsNum; col++)
                    if (!(mineGrids[row, col].State ==(int)MineGridState.Flag ||mineGrids[row, col].State >=(int)MineGridState.Num8))
                        return false;
            return true;
        }
        #endregion

现在就可以开玩啦!


不好意思,把我的个人信息粘上来了。

再来一张,我玩过的:


哈哈!有意思吧!我居然用了942秒就通过了!

偶!忘了说了,还需要加载音乐,不然就太无趣了!

添加一个命名空间:

using System.Runtime.InteropServices;

向定义全局变量一样放入以下语句(具体是什么原理,?我也不太清楚)

[DllImport("winmm")]
        public static extern bool PlaySound(string szSound, int hMod, int i);
为了使编程更简单,还需要编写一些播放声音的函数。当然如果你比较牛逼,可以不写,不过你要让你多年后还能看得懂。

private void Play(string waveName)
        {
            PlaySound(Application.StartupPath + "\\GameSounds\\" + waveName + ".wav", 0, 1);
        }

在可执行目录下放置几个音乐文件,命名规则遵循见名知意,当然其他变量和函数的命名都应该遵循这个潜规则(难道你老湿没告诉你吗?)。


还有,我们一时兴奋,完成了一项伟大的扫雷任务,想要把它保存起来,该怎么办!和其他游戏一样,我们可以来个截图,留下精彩瞬间,也可以保存一下游戏记录,以便以后翻给学弟学妹门看,看,这是我创下了丰碑!当然你也有了装逼的资本,虐小狗还是挺容易的,呵呵!


保存截图这项功能是我通过网络了解到的,其实对于编程这种数据量繁重的工作对人脑的载荷能力是很有挑战性的。一个很好的应对办法是通过网络不断的学习和发展自我。分享自己的智慧,也吸收别人的精华。我不可能手把手的帮助到你,只能交给你怎样学习的方法。 这些对你在今后的学习工作中都是有用的。

private void 保存截图ToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Bitmap bit = new Bitmap(this.Width - 16, this.Height - 7);//实例化一个和窗体一样大的bitmap
            Graphics g = Graphics.FromImage(bit);
            g.CompositingQuality = CompositingQuality.HighQuality;//质量设为最高
            //g.CopyFromScreen(this.Left+8, this.Top, 0, 0, new Size(this.Width-12, this.Height-7));//保存整个窗体为图片
            g.CopyFromScreen(new Point(this.Left + 8, this.Top), new Point(0, 0), new Size(this.Width - 16, this.Height - 7));
            //g.CopyFromScreen(panel游戏区 .PointToScreen(Point.Empty), Point.Empty, panel游戏区.Size);//只保存某个控件(这里是panel游戏区)
            bit.Save(Application.StartupPath + "\\扫雷游戏"+DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Year.ToString()
                + DateTime.Now.Day.ToString() + DateTime.Now.Hour.ToString() + DateTime.Now.Minute.ToString()+
                DateTime.Now.Second.ToString()+DateTime.Now.Millisecond.ToString()+".png");//默认保存格式为PNG,保存成jpg格式质量不是很好
        }

图片保存的名字以“扫雷游戏+日期+时间”命名
如:


保存记录需要按照一定的格式,方便其他程序调用。


private void 保存记录ToolStripMenuItem_Click(object sender, EventArgs e)
        {
            GameTimer.Stop();
            FormGetName Fgname = new FormGetName();
            if (Fgname.ShowDialog()==DialogResult.OK)
            {
                string LevelInfo = "";
                for (int i = 0; i < RowsNum; i++)
                {
                    for (int j = 0; j < ColsNum; j++)
                    {

                        LevelInfo += (mineGrids[i, j].IsMine == true) ? "1" : "0";
                    }
                }
                LevelInfo = LevelInfo + "," + RowsNum.ToString() + "," + ColsNum.ToString() + "," + RowsColsNums[3 * Difficulty + 2].ToString()
                    + "," + SpendSeconds.ToString() + "," + System.Enum.GetName(typeof(GameState), gameState) + ","+DateTime.Now.ToLongDateString()
                    + "," + DateTime.Now.ToLongTimeString() + ",";
                GameConfig.AddRecord(LevelInfo, Fgname.GetUserName);
            }
            if (gameState==(int)GameState.Run)
            {
                GameTimer.Start();
            }
        }

这里加载了一个计时器,这些次要的东西我都忽略不讲了。

当然,游戏保存了以后还需要浏览哈!需要再新建一个子窗体。


它的程序如下:

在主类中调用这个类

private void 浏览记录ToolStripMenuItem_Click(object sender, EventArgs e)
        {
            FormScanRecord FSR = new FormScanRecord();
            FSR.Show();
        }
窗体类中构造显示函数,读取保存的信息:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace 扫雷
{
    public partial class FormScanRecord : Form
    {
        string showInfo;
        public FormScanRecord()
        {
            InitializeComponent();
        }

        private void FormScanRecord_Load(object sender, EventArgs e)
        {
            string[] RecordInfo=GameConfig.GetFileLines();
           
            if (RecordInfo!=null)
            {
                if (RecordInfo[0]=="0")
                {
                    MessageBox.Show("还没有游戏记录");
                    this.Close();
                }
                else
                {
                    showInfo = "一共有" + RecordInfo[0] + "次记录,按时间先后顺序分别是:\r\n";
                    for (int i = 1; i < int.Parse(RecordInfo[0])+1; i++)
                    {
                        string[] strSplit = RecordInfo[i].Split(',');
                        showInfo += "\r\n第" + i.ToString() + "位扫雷员:\r\n";
                        showInfo += "    姓名:" + strSplit[8] + "\r\n";
                        showInfo += "    扫雷行数:" + strSplit[1] + "\r\n";
                        showInfo += "    扫雷列数:" + strSplit[2] + "\r\n";
                        showInfo += "    雷数:" + strSplit[3] + "\r\n";
                        showInfo += "    地雷分布密度:" + (int.Parse(strSplit[3]) / (float)((int.Parse(strSplit[1]) * int.Parse(strSplit[2])))).ToString() + "\r\n";
                        showInfo += "    扫雷用时:" + (int.Parse(strSplit[4]) / 60).ToString() + "分" + (int.Parse(strSplit[4]) % 60).ToString() + "秒\r\n";
                        showInfo += "    扫雷记录状态:" + strSplit[5] + "\r\n";
                        showInfo += "    扫雷记录日期:" + strSplit[6] + "\r\n";
                        showInfo += "    扫雷记录时间:" + strSplit[7] + "\r\n";
                    }
                    this.RecordtextBox.Text = showInfo;
                }
            }
        }

        private void close_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}
最后,也是你最迫不及待想要的东西,我还是把可执行文件发给你们吧。
点击下面的链接:

http://pan.baidu.com/s/1c0uhFCO

需要源代码的专业人士,可以向我发送QQ邮件,我会不定时查看

[email protected]


为了保证用户能够很快浏览截图记录,可以添加一个图片浏览器,可以很方便的浏览历史截图。


与此同时,我还开发了其他类型的小游戏


新版游戏下载链接

http://t.cn/RIMeL9f


猜你喜欢

转载自blog.csdn.net/qq_16635325/article/details/49561341