C#实现扫雷小游戏

初学C#窗体程序,想起了winXP上经典的扫雷小游戏,于是打算自己也写一个。

资源准备及基础思路

扫雷的规则就不赘述了。原版有三个难度:初级9*9,中级16*16,高级16*30。

关于资源文件,在网上找到了原版的DIB位图文件,效果如图(图为截图,dib文件貌似无法直接作为图片上传),文章最后会放完整项目的下载链接。
在这里插入图片描述 在这里插入图片描述
程序图标在网上随便找了一个ico
C#实现扫雷小游戏_第1张图片
由于最开始在MOOC上学习的是button控件,这次也习惯性地用了button,事后证明是一个严重的错误。因为要用图片做的话不如使用PictureBox控件,而不是像现在这样把资源图片设置为button的背景图片。

核心是用一个二维数组存储游戏区。在开始游戏时随机生成雷区,然后根据鼠标点击触发事件,思路是比较简单清晰的。雷的标记可以利用button类的Tag属性区分,0代表自身非雷周围8格有雷,1代表雷,2代表自身非雷但周围有雷。

后续还需要补充的功能是菜单栏的完善、界面的进一步模仿、积分器的添加、第二次右键变成问号功能以及计时器功能,看有没有心情填坑吧。

全局变量设置及资源导入

先做一些必要的变量声明。Coord是包含xy的坐标类,ButtonWithPos是继承自Button并加入的xy坐标的类。

        int level; //游戏难度,1:初级;2:中级;3:高级
        int mineNum; //雷总数,初级难度:9;中极难度:40;高级难度:99
        int mineMarked = 0; //已标记地雷数变量
        static int WIDTH, HEIGHT; //棋盘大小,初级:9*9,;中级:16*16;高级:16*30:
        ButtonWithPos[,] btns; //带XY坐标的拓展Button类
        Coord[] mines; //生成雷区所需标记雷的XY坐标的数组
        bool[,] visited; //Reveal()函数DFS递归算法所需visited数组

导入图片资源。在右下角解决方案资源管理器中双击Properties,在资源一栏中添加裁剪好的图片文件。在程序中将它们声明为Image类的变量便于后续引用,大致格式为

Image imgNum1 = Properties.Resources._1; //_1是你导入的资源的名称

界面设计及初始化

先添加一些简单的元素(请无视那个作弊按钮)
C#实现扫雷小游戏_第2张图片
显然菜单栏的难度选择中同时只应该有一项被选中,将三个选项的click的事件都注册到一个函数上就可以轻易实现:

		private void ToolStripMenuItem1_CheckedChanged(object sender, EventArgs e)
        {
            foreach (ToolStripMenuItem item in 难度ToolStripMenuItem1.DropDownItems)
            {
                item.Checked = false;
            }
            ToolStripMenuItem checkItem = sender as ToolStripMenuItem;
            checkItem.Checked = true;
        }

菜单栏中开始游戏项目的click事件是游戏的入口,需进行窗体大小的调整、提示信息刷新、雷区大小调整、雷区初始化、生成地雷。

        //游戏入口
        private void 开始游戏ToolStripMenuItem_Click(object sender, EventArgs e)
        {
            //先清空显示及btn数组
            if (btns != null)
            {
                foreach(ButtonWithPos btn in btns) Controls.Remove(btn);
                Array.Clear(btns, 0, WIDTH * HEIGHT);
            }

            //难度选择
            if (初级ToolStripMenuItem1.Checked)
            {
                level = 1;
                mineNum = 10;
                WIDTH = HEIGHT = 9;
            }
            else if (中级ToolStripMenuItem1.Checked)
            {
                level = 2;
                mineNum = 40;
                WIDTH = HEIGHT = 16;
            }
            else if (高级ToolStripMenuItem1.Checked)
            {
                level = 3;
                mineNum = 99;
                WIDTH = 30;
                HEIGHT = 16;
            }
            else Application.Exit();

            //对象初始化
            mines = new Coord[mineNum];
            for (int i = 0; i < mineNum; i++) mines[i] = new Coord();
            btns = new ButtonWithPos[HEIGHT, WIDTH];
            visited = new bool[HEIGHT, WIDTH];
            BoardInit();

            //Reveal()函数DFS递归算法所需visited数组初始化
            for (int i = 0; i < HEIGHT; ++i)
            {
                for (int j = 0; j < WIDTH; ++j)
                {
                    visited[i, j] = false;
                }
            }
            Generate();
        }

BoardInit函数用于窗口大小调整、提示信息刷新以及button数组(即雷区)的初始化:

		public void BoardInit()
        {
            int x0 = 20, y0 = 120;
            if (level == 1)
            {
                Height = 500;
                Width = 375;
            }
            else if (level == 2)
            {
                Height = 730;
                Width = 620;
            }
            else if (level == 3)
            {
                Height = 730;
                Width = 1110;
            }
            else Application.Exit();
            lblMineSum.Left = x0;
            lblMineChecked.Left = x0;
            btnShow.Left = Width / 2 - 20;

            int  d = 35;
            Console.WriteLine(WIDTH + " " + HEIGHT);
            for (int i = 0; i < HEIGHT; ++i)
            {
                for (int j = 0; j < WIDTH; ++j)
                {
                    ButtonWithPos button = new ButtonWithPos();
                    button.Width = d;
                    button.Height = d;
                    button.Top = y0 + i * d;
                    button.Left = x0 + j * d;
                    button.BackgroundImage = imgNormal;
                    button.BackgroundImageLayout = ImageLayout.Stretch;
                    button.X = j;
                    button.Y = i;
                    button.Tag = 0;//Tag = 0,周围无雷;1,地雷;2,周围有雷;
                    button.Visible = true;
                    button.Enabled = true;
                    button.MouseDown += Button_MouseDown;
                    btns[i, j] = button;
                    Controls.Add(button);
                }
            }
            btnShow.BackgroundImage = imgSmile;
            btnShow.BackgroundImageLayout = ImageLayout.Stretch;
            lblMineSum.Text = "地雷总数:" + mineNum.ToString();
            lblMineChecked.Text = "已标记地雷数:" + mineMarked.ToString();
        }

Generate函数使用随机数生成雷区:

		void Generate()
        {
            Random random = new Random();
            bool flag;
            for (int i = 0; i < mineNum; ++i)
            {
                int newX = random.Next(0, HEIGHT);
                int newY = random.Next(0, WIDTH);
                flag = true;
                while (true)
                {
                    flag = true;
                    for (int j = 0; j < i; ++j)
                    {
                        if (mines[j].X == newX && mines[j].Y == newY)
                        {
                            flag = false;
                            break;
                        }
                    }
                    if (flag)
                    {
                        mines[i].X = newX;
                        mines[i].Y = newY;
                        btns[newX, newY].Tag = 1;
                        int left = newX - 1, right = newX + 1;
                        int top = newY - 1, bottom = newY + 1;
                        for (int k = left; k <= right; ++k)
                        {
                            for (int l = top; l <= bottom; ++l)
                            {
                                if (k >= 0 && k < HEIGHT && l >= 0 && l < WIDTH)
                                {
                                    if (k == newX && l == newY) continue;
                                    if ((int)btns[k, l].Tag != 1) btns[k, l].Tag = 2;
                                }
                            }
                        }
                        break;
                    }
                    else
                    {
                        newX = random.Next(0, HEIGHT);
                        newY = random.Next(0, WIDTH);
                    }
                }

            }
        }

鼠标事件处理

左键是翻开,右键是标记地雷。右键只需要进行显示的更新、标记地雷数的刷新以及检查是否胜利。左键要分翻开的类型。如果踩雷直接输掉,打开周围有雷的格子显示对应周围的雷数。

        private void Button_MouseDown(object sender, MouseEventArgs e)
        {
            ButtonWithPos button = sender as ButtonWithPos;
            if (e.Button == MouseButtons.Right)
            {
                if (button.BackgroundImage != imgMarked)
                {
                    button.BackgroundImage = imgMarked;
                    button.BackgroundImageLayout = ImageLayout.Stretch;
                    ++mineMarked;
                }
                else
                {
                    button.BackgroundImage = imgNormal;
                    --mineMarked;
                }
                lblMineChecked.Text = "已标记地雷数:" + mineMarked.ToString();
                bool win = CheckWin();
                if (win) Win();
            }
            else if (e.Button == MouseButtons.Left)
            {
                int row = button.Y, col = button.X;
                Console.WriteLine("BUTTON DOWN: " + row + col);
                if ((int)button.Tag == 1)
                {
                    Lose(button);
                }
                else if ((int)button.Tag == 2)
                {
                    int mineAroundSum = CalcMineAround(row, col);
                    MineAroundShow(button, mineAroundSum);
                }
                else if ((int)button.Tag == 0)
                {
                    button.BackgroundImage = imgNull;
                    Reveal(row, col);
                }
            }
        }

打开周围无雷的格子要复杂一些。我们实际玩扫雷的时候有时候会一下翻开一片区域,这是因为当翻开了一个周围无雷的格子,程序会继续尝试翻周围的格子,直到翻开一个周围有雷的格子。这可以用DFS算法递归实现。

        void Reveal(int row, int col)
        {
            visited[row, col] = true;
            int mineAroundSum;
            int left = row - 1, right = row + 1;
            int top = col - 1, bottom = col + 1;
            if ((int)btns[row, col].Tag == 0) btns[row, col].BackgroundImage = imgNull;
            else if ((int)btns[row, col].Tag == 1) return;
            else if ((int)btns[row, col].Tag == 2)
            {
                mineAroundSum = CalcMineAround(row, col);
                MineAroundShow(btns[row, col], mineAroundSum);
                return;
            }
            for (int i = left; i <= right; ++i)
            {
                for (int j = top; j <= bottom; ++j)
                {
                    if (i >= 0 && i < HEIGHT && j >= 0 && j < WIDTH)
                    {
                        if (!visited[i, j])
                        {
                            //Console.WriteLine("REVEAL: " + i + j);
                            Reveal(i, j);
                        }
                    }
                }
            }
        }

最后一些判断胜利失败以及胜利失败的表示的函数比较简单,就不再贴代码了。

总结

这个项目代码一共400行左右,写得比较快,还有一些功能没实现,也可能有bug,暂时先不管了。最后附一张截图。
C#实现扫雷小游戏_第3张图片
项目下载:
链接: https://pan.baidu.com/s/12F_azhECMINjzvBYYZ808w
提取码: y96k

你可能感兴趣的:(C#实现扫雷小游戏)