初学C#窗体程序,想起了winXP上经典的扫雷小游戏,于是打算自己也写一个。
扫雷的规则就不赘述了。原版有三个难度:初级9*9,中级16*16,高级16*30。
关于资源文件,在网上找到了原版的DIB位图文件,效果如图(图为截图,dib文件貌似无法直接作为图片上传),文章最后会放完整项目的下载链接。
程序图标在网上随便找了一个ico
由于最开始在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是你导入的资源的名称
先添加一些简单的元素(请无视那个作弊按钮)
显然菜单栏的难度选择中同时只应该有一项被选中,将三个选项的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,暂时先不管了。最后附一张截图。
项目下载:
链接: https://pan.baidu.com/s/12F_azhECMINjzvBYYZ808w
提取码: y96k