C# 扫雷游戏 纯控制台 附源码
盆友在学C#+U3D,作为一枚java开发,我也研究了一下,估计以后也会更这方面的内容。百度能搜到的内容,我从来不写。
先贴两张效果图:
废话不多说,先贴代码,再盘出逻辑。
扫雷游戏的类
using System;
using System.Threading;
namespace ConsoleGame
{
class FindBomb
{
Square[,] table ;
//起始坐标
int x;
int y;
//选中的坐标
int chooseX = 1;
int chooseY = 1;
//背景颜色
ConsoleColor foreColor;
ConsoleColor backColor;
//布雷密度
int bombPersent;
//DEBUG模式
bool debug = false;
//开始时间
DateTime dt;
//重绘标志位
bool needPaint = true;
//时间重绘标志位
string timeLastPaint;
public FindBomb(int x, int y, ConsoleColor foreColor, ConsoleColor backColor,int size, int bombPersent)
{
this.x = x;
this.y = y;
this.foreColor = foreColor;
this.backColor = backColor;
this.table = new Square[size + 2, size + 2];
this.bombPersent = bombPersent;
}
public void Start()
{
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 38, 5, "\t\t扫雷游戏", 50);
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 38, 7, "地图边长"+(table.GetLength(0)-2)+ "格 布雷密度"+bombPersent+"%", 50);
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Gray, 38, 8, "移动光标:↑↓←→ 标记:SPACE 翻开:ENTER", 50);
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 38, 10, "玩法说明:", 50);
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 38, 11, "翻开所有非地雷的方格即可完成游戏", 50);
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 38, 12, "不小心翻到地雷则游戏失败", 50);
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 38, 13, "临近地雷的方格,会显示附近的地雷数量", 50);
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 38, 28, "F1:开启/关闭DEBUG作弊模式 Esc:退出游戏", 50);
PutBombs(bombPersent);
PrintTable();
dt = DateTime.Now;
//开启计时线程
ThreadStart clock = new ThreadStart(Timer);
Thread thread = new Thread(clock);
thread.Start();
if (ReadKeyBoard())
{
thread.Abort();
//success
AfterAll(true);
}
else
{
thread.Abort();
//lose
AfterAll(false);
}
}
public void Timer()
{
while (true)
{
PrintTable();
}
}
public bool ReadKeyBoard()
{
while (true)
{
ConsoleKey ck = Console.ReadKey(true).Key;
switch (ck)
{
case ConsoleKey.Backspace:
break;
case ConsoleKey.Spacebar://标记
if (!table[chooseX, chooseY].isOpen)
{
table[chooseX, chooseY].isMarked = !table[chooseX, chooseY].isMarked;//标志位取反
}
break;
case ConsoleKey.Enter:
if (TryOpen(chooseX, chooseY))//打开
{
table[chooseX, chooseY].isMarked = false;
return false;//如果打开了Bomb则返回
}//如果没打开Bomb,检测是否胜利
if (IsSuccess())
{
return true;
}
break;
case ConsoleKey.LeftArrow:
if (chooseX > 1)
chooseX--;
break;
case ConsoleKey.UpArrow:
if (chooseY > 1)
chooseY--;
break;
case ConsoleKey.RightArrow:
if (chooseX < table.GetLength(0) - 2)
chooseX++;
break;
case ConsoleKey.DownArrow:
if (chooseY < table.GetLength(1) - 2)
chooseY++;
break;
case ConsoleKey.F1:
debug = !debug;
break;
case ConsoleKey.Escape:
return false;
default:
break;
}
needPaint = true;
}
}
/// <summary>
/// 判断是否已经完成
/// </summary>
public bool IsSuccess()
{
for (int i = 1; i < table.GetLength(0) - 1; i++)
{
for (int j = 1; j < table.GetLength(1) - 1; j++)
{
if (table[i,j].isBomb==false && table[i, j].isOpen==false)
{
return false;
}
}
}
return true;
}
/// <summary>
/// 填入或重置所有方块
/// </summary>
/// <param name="isBombPersent">标记为true的几率</param>
public void PutBombs(int isBombPersent)
{
Random random = new Random();
for (int i = 1; i < table.GetLength(0) - 1; i++)//忽略外围
{
for (int j = 1; j < table.GetLength(1) - 1; j++)
{
if (random.Next() % 100 < isBombPersent)
{
table[i, j] = new Square(true);
}
else
{
table[i, j] = new Square(false);
}
}
}//设置好了Bomb,设置count
for (int i = 1; i < table.GetLength(0) - 1; i++)
{
for (int j = 1; j < table.GetLength(1) - 1; j++)
{
if (!table[i, j].isBomb)//如果不是bomb,计算count
{
table[i, j].count = CountNumb(i, j);
}
}
}
}
/// <summary>
/// 计算某坐标周边Bomb数
/// </summary>
/// <returns>周边Bomb的数量</returns>
private int CountNumb(int x, int y)
{
int result = 0;
if (table[x - 1, y - 1].isBomb)
result++;
if (table[x, y - 1].isBomb)
result++;
if (table[x + 1, y - 1].isBomb)
result++;
if (table[x - 1, y].isBomb)
result++;
if (table[x + 1, y].isBomb)
result++;
if (table[x - 1, y + 1].isBomb)
result++;
if (table[x, y + 1].isBomb)
result++;
if (table[x + 1, y + 1].isBomb)
result++;
return result;
}
/// <summary>
/// 绘制排查中的Table
/// </summary>
public void PrintTable()
{
string timePass = "用时: " + (DateTime.Now - dt).Minutes + "分 " + (DateTime.Now - dt).Seconds + "秒 ";
if (!timePass.Equals(timeLastPaint))//如果绘制的时间想同,则不会绘制,避免闪烁
{
timeLastPaint = timePass;
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 40, 29, timePass, 0);
}
if (needPaint)
{
needPaint = false;
Console.ResetColor();
Console.SetCursorPosition(2, 1);
if (debug)
{
Console.WriteLine("当前选择{2},{3} 计数:{4} {5} ", x, y, chooseX, chooseY, table[chooseX, chooseY].count, table[chooseX, chooseY].isBomb ? "炸弹" : "空地");
}
else
{
Console.WriteLine(" ");
}
for (int x = 1; x < table.GetLength(0) - 1; x++)//外围不绘制
{
for (int y = 1; y < table.GetLength(1) - 1; y++)
{
Console.SetCursorPosition(this.x + x * 2, this.y + y);
if (x == chooseX && y == chooseY)//如果正被选中
{
Console.ForegroundColor = foreColor;//不交换
Console.BackgroundColor = backColor;
}
else
{
Console.ForegroundColor = backColor;//交换前景色和背景色
Console.BackgroundColor = foreColor;
}
if (table[x, y].isOpen == false)//如果没有被开启
{
if (table[x, y].isMarked == false)
{
Console.Write('□');
}
else
{
Console.Write('※');
}
}
else//如果已经开启
{
if (table[x, y].count == 0)
{
Console.Write(" ");
}
else
{
Console.Write(table[x, y].count + " ");
}
}
}
}
}
}
public void AfterAll(bool isSuccess)
{
string timePass = (DateTime.Now - dt).Minutes + "分 " + (DateTime.Now - dt).Seconds + "秒";
for (int x = 1; x < table.GetLength(0) - 1; x++)//外围不绘制
{
for (int y = 1; y < table.GetLength(1) - 1; y++)
{
Console.SetCursorPosition(this.x + x * 2, this.y + y);
if (x == chooseX && y == chooseY)//如果正被选中
{
Console.ForegroundColor = foreColor;//不交换
Console.BackgroundColor = backColor;
}
else
{
Console.ForegroundColor = backColor;//交换前景色和背景色
Console.BackgroundColor = foreColor;
}
if (table[x, y].isBomb && table[x, y].isMarked)//如果是正确标记的地雷
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write('√');
}else if (table[x, y].isBomb == false && table[x, y].isMarked)//标错的
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Write('×');
}else if (table[x, y].isBomb)//未标记的地雷
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.Write('○');
}
else if (table[x, y].isOpen)
{
if (table[x, y].count == 0)
{
Console.Write(" ");
}
else
{
Console.Write(table[x, y].count + " ");
}
}
else
{
Console.Write("□");
}
Console.ResetColor();
}
}
if (isSuccess)
{
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 15, 3, "你成功了,用时" + timePass + " 请按任意键继续", 50);
}
else
{
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 15, 3, "游戏结束,耗时" + timePass + " 请按任意键继续", 50);
}
Console.ReadKey();
}
/// <summary>
/// 设置开启状态,并返回isBomb属性
/// </summary>
/// <returns></returns>
public bool TryOpen(int x, int y)
{
//如果是空的 则向四周传递
if (x>0 && y>0 && x < table.GetLength(0) - 1 && y < table.GetLength(1) - 1)
{
if (table[x, y].isOpen == false && table[x, y].count == 0 && table[x, y].isBomb == false)
{
table[x, y].isOpen = true;
TryOpen(x - 1, y - 1);//↖
TryOpen(x, y - 1); //↑
TryOpen(x + 1, y - 1);//↗
TryOpen(x - 1, y); //←
TryOpen(x + 1, y); //→
TryOpen(x - 1, y + 1);//↙
TryOpen(x, y + 1); //↓
TryOpen(x + 1, y + 1);//↘
}
table[x, y].isOpen = true;
}
return table[x, y].isBomb;
} }
/// <summary>
/// 单个方块结构体
/// </summary>
public struct Square
{
public Square(bool isBomb)
{
this.isBomb = isBomb;
isMarked = false;
isOpen = false;
count = 0;
}
public bool isBomb;
public bool isMarked;
public bool isOpen;
public int count;
}
}
调用方式:
/// <summary>
/// 扫雷调用
/// </summary>
public static bool PlayFindBombs()
{
BoxUtil.CoverBackendColor(ConsoleColor.Black, 60, 31);//擦除内容
BoxUtil.TypeWords(ConsoleColor.DarkRed, ConsoleColor.Black, 14, 2, StrConst.MINE_CLEARANCE, 0);//绘制暗色LOGO
//选择大小
Thread.Sleep(500);
BoxUtil.PrintBox(baseColor, baseColor, 20, 10, 40, 20, ' ', 20);//绘制内边框
BoxUtil.TypeWords(ConsoleColor.Red, ConsoleColor.Black, 28, 12, "请选择大小", 0);
BoxUtil.TypeWords(ConsoleColor.Red, ConsoleColor.Black, 14, 2, StrConst.MINE_CLEARANCE, 0);//绘制亮色LOGO
ChooseList FindBombSizeMod = new ChooseList(new ListView[] {
new ListView(6 ,"6 X 6 -适合学龄前儿童"),
new ListView(10,"10 X 10 -大小和厕所接近"),
new ListView(14,"14 X 14 -标准面积的擂台"),
new ListView(22,"22 X 22 -谁选谁是大*")}, 23, 14);
int sizeFlag = FindBombSizeMod.RefreshAndChoose();
BoxUtil.CoverBackendColor(ConsoleColor.Black, 21, 11, 40, 21);//擦除内容
//选择密度
BoxUtil.TypeWords(ConsoleColor.Red, ConsoleColor.Black, 28, 12, "请选择密度", 0);
ChooseList FindBombPersentMod = new ChooseList(new ListView[] {
new ListView(10,"10% -工兵有我方间谍"),
new ListView(20,"20% -军费被领导贪污"),
new ListView(30,"30% -训练有素的敌军"),
new ListView(40,"45% -敌军地雷不要钱")}, 24, 14);
int persentFlag = FindBombPersentMod.RefreshAndChoose();//显示选择列表
BoxUtil.CoverBackendColor(ConsoleColor.Black, 60, 31);//擦除内容
BoxUtil.TypeWords(ConsoleColor.Black, ConsoleColor.Black, 14, 2, StrConst.MINE_CLEARANCE, 0);//绘制黑色LOGO
FindBomb fb = new FindBomb(10, 5, ConsoleColor.White, ConsoleColor.Black, sizeFlag, persentFlag);//按大小创建地图
fb.Start();//按密度排布地雷,并开始
return ShowRetry();
}
这里有点长,BoxUtil和ChooseList是我自己封装的绘图和选框工具类
反正java是这么玩的
缩略一下,就是这样即可
FindBomb fb = new FindBomb(10, 5, ConsoleColor.White, ConsoleColor.Black, 18/*边长18*18*/, 10/*密度10%*/);
fb.Start();
工具类我在最下面贴出吧,其实可以用Console.Write和Console.SetCursorPosition函数替换掉
本游戏的特点:
1.完全使用原生和控制台实现。
2.布雷密度和地图大小可调节。
3.操作使用上下左右,并且基本复刻了原版扫雷。
4.F1开启debug模式,可以作弊。
5.多线程显示游戏进行时间。
6.缺点,没有保存、暂停、排行榜功能(因为懒)
简单说明一下逻辑和用到的方法:
1.显示扫雷的框框:由于右下角实时显示游戏时间,所以使用了一个标志位来判断是否进行绘图,绘图的条件是:1.如果秒数变了,绘制。2.如果按下了上下左右等按键,绘制。3.如果秒数没变,上下左右也没按,不绘制。
2.扫雷框框的结构:一个二维数组,java的小伙伴们注意了,是C#的二维数组喔,C#还有个交错数组。每一个数组元素是一个Square结构体。
那说说Square结构体的设计:
public struct Square
{
public Square(bool isBomb)
{
this.isBomb = isBomb;
isMarked = false;
isOpen = false;
count = 0;
}
public bool isBomb;
public bool isMarked;
public bool isOpen;
public int count;
}
通过一系列布尔值,标注了是否为炸弹isBomb,是否被玩家标记isMarked,是否被翻开isOpen,以及如果不是炸弹,那么数字应该是几count。他的构造方法是为二维矩阵布置地雷时调用。
那么地雷如何布置的呢,使用了Random,根据创建类对象时提供的密度作为比例布置地雷
诶呦,忘记说了,这里用了一个巧妙的设计,来避免数组下标越界异常(称呼来自java,c#是不是叫这个不清楚)
简单的来理解一下,首先熟悉一下扫雷的规则
如图,如果一个黑色的小方块不是炸弹,那么我们需要在其中显示他周围炸弹的数量,如2表示他周边8个格子中有2个炸弹
那么就会出现数组下标越界的麻烦事
对,当你在处理边缘上的点的时候,比如当前选择x,y这个点,判断x-1,y-1这个点时,C#会告诉你x数组没那么长,你这个x-1不在数组下标范围里。
那如何化解这个尴尬,显然,可以通过if来判断,如果这个点在边缘上,我们就使用另一套判别计算方法
那么你需要写几种计算方法呢,9种,分别是黄色的-正常,绿色的-不去找他的y-1,等等等,这显然非常麻烦。
这个办法虽然繁琐,但是是可行的,他可以简单一些,我们加上if判断,如果这个点的x-1已经小于0了,不处理,如果大于length了也不处理。
那么每个点的8个周边点,请你都套上最少带一个&&的if语句
这样是不是简单了一些,至少我不用写9种判别方法了,但是他可以更容易,比如我如果只处理黄色部分呢
于是我想到了这种设计,如图,黄色既是布雷区域,也是显示区域,相当于对于一个长度为[x,y]的二维数组,只显示 1→x-1,1→y-1的部分,外围部分设置为空地。这样在计数的时候,也只处理黄色部分的方块,看看他周围有几个雷,而不用担心数组下标越界的问题。
所以我的显示区,布雷区,计数区,都是黄色区域,通过加宽加高这个二维数组,绕开了下标越界。
于是乎一切都变得简单了,剩下的内容主要也只剩显示了
显示我只写了一个方法,在AfterAll(bool isScuess)中,这个方法通过一个布尔来区分是游戏状态还是游戏结束状态,绘出游戏结束状态:
如图如果我翻到了炸弹,或者翻完了框框,会显示当前选择,炸弹位置,标错的标记,标对的标记。
这块其实很纠结,写一个方法有点乱,写两个方法代码又大量重复,最终我写成了一个方法
本人的注释写的很清晰 个人认为代码可读性也基本良好
再贴一遍:
public void AfterAll(bool isSuccess)
{
string timePass = (DateTime.Now - dt).Minutes + "分 " + (DateTime.Now - dt).Seconds + "秒";
for (int x = 1; x < table.GetLength(0) - 1; x++)//外围不绘制
{
for (int y = 1; y < table.GetLength(1) - 1; y++)
{
Console.SetCursorPosition(this.x + x * 2, this.y + y);
if (x == chooseX && y == chooseY)//如果正被选中
{
Console.ForegroundColor = foreColor;//不交换
Console.BackgroundColor = backColor;
}
else
{
Console.ForegroundColor = backColor;//交换前景色和背景色
Console.BackgroundColor = foreColor;
}
if (table[x, y].isBomb && table[x, y].isMarked)//如果是正确标记的地雷
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write('√');
}else if (table[x, y].isBomb == false && table[x, y].isMarked)//标错的
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Write('×');
}else if (table[x, y].isBomb)//未标记的地雷
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.Write('○');
}
else if (table[x, y].isOpen)
{
if (table[x, y].count == 0)
{
Console.Write(" ");
}
else
{
Console.Write(table[x, y].count + " ");
}
}
else
{
Console.Write("□");
}
Console.ResetColor();
}
}
if (isSuccess)
{
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 15, 3, "你成功了,用时" + timePass + " 请按任意键继续", 50);
}
else
{
BoxUtil.TypeWords(ConsoleColor.Blue, ConsoleColor.Black, 15, 3, "游戏结束,耗时" + timePass + " 请按任意键继续", 50);
}
Console.ReadKey();
}
大意是,类变量chooseX,chooseY记录了当前选择的点,如果是当前选择的点,则交换前景色和背景色,如果不是,则判断是否没开,显示方块,是否开了,显示空白或数字。如果是游戏结束状态呢,则显示开没开,标没标错,已经翻开的显示数字,没翻开的依旧显示方块。晕
好,显示问题也解决一部分了,在一个就是多线程实时显示游戏时间的问题,由于控制台的特性,我们不能单纯使用两个线程来分别绘制时间和扫雷块,必须全图一次绘制,类似于“垂直同步”,这是因为控制台似乎是单线程的,如果分别绘制,会有这样的问题
这张图是我模拟出来的,不过大概就是这个意思,画的很像了,本该在右下角显示的时间,却显示到这里来了。这是因为两个线程同步运行产生的安全问题,本质是Console只有一个类,他是相当于控制台是单线程调用的
比如:
A线程 – 逐一定位在table的某个方块上,并绘制方块
B线程 – 定位到右下角,绘制游戏时间
那么会有这种情况:
B线程先定位到右下角 – B线程刚刚输出“用时:”
A线程抢占了控制台资源 – 定位到左边扫雷框 – A线程输出了一个框框
B线程抢回了控制台资源 – B线程继续输出几分几秒
由于控制台的定位已经被A线程定位到左侧扫雷框了,所以B线程的打印接在了A线程后面。
于是就发生了线程安全问题,我们可以通过锁机制,让A线程抢占锁资源,绘制完成再释放锁资源,或者通过将绘制过程整合为一个线程,来避免线程的安全问题。我使用了后者
是时候说明游戏整体逻辑了:
进入游戏首先new出数组,布雷,并把他画出来,开启计时线程,每秒更新标志位
然后ReadKeyBoard()方法是一个死循环,只有在游戏成功或失败时才会 返回true或者false,获取他的返回值的过程就是整个游戏过程,他会在每次你敲下按键时判断你赢没赢,或者死没死。
PrintTable方法在标志位改变时绘制屏幕,也就是时间过去了一秒或者你按下了按键。死循环执行,游戏结束时停止执行。
ReadKeyBoard,IsSuccess,PutBombs等方法,不再赘述,逻辑简单,注释清晰。
最后说个TryOpen的传递。
如图所示,我只翻开了一块,却打开了一大片,通过分析得知,如果你翻开了一块计数为0的方块,他会向四周传递,一直传递到有计数的方块为止。所以我设计了TryOpen方法
/// <summary>
/// 设置开启状态,并返回isBomb属性
/// </summary>
/// <returns></returns>
public bool TryOpen(int x, int y)
{
//如果是空的 则向四周传递
if (x>0 && y>0 && x < table.GetLength(0) - 1 && y < table.GetLength(1) - 1)
{
if (table[x, y].isOpen == false && table[x, y].count == 0 && table[x, y].isBomb == false)
{
table[x, y].isOpen = true;
TryOpen(x - 1, y - 1);//↖
TryOpen(x, y - 1); //↑
TryOpen(x + 1, y - 1);//↗
TryOpen(x - 1, y); //←
TryOpen(x + 1, y); //→
TryOpen(x - 1, y + 1);//↙
TryOpen(x, y + 1); //↓
TryOpen(x + 1, y + 1);//↘
}
table[x, y].isOpen = true;
}
return table[x, y].isBomb;
}
}
不难看出这是一个递归调用方法,其实这个方法耗费了本架构师好长时间,他看起来简单,写起来还真不容易,原则上是传递打开方块这个方法,如果计数是0,要继续传递,直到把空的一片全翻开。
这里有个递归的坑,即如果A是空的,传给了他边上的B,B也是空的,又会传给A,这样死循环,来回来去的传,会栈溢出,解决很简单,在合适的时机将isOpen设置为true,并且不传递给isOpen的方块即可。
记得限制传递范围,不要传递到外圈,不然又要下标越界。
基本写到这了,为了方便拿来主义能拿来就用,我贴一下BoxUtil工具类中用到的方法
/// <summary>
/// 模拟打字机效果,一个字一个字的敲出文本
///另,只贴出了重载方法中最大的一个,最后一个delay可以设置为0则无延迟
/// </summary>
public static void TypeWords(ConsoleColor foreColor, ConsoleColor backColor, int startX, int startY, string words,int delay)
{
Console.ForegroundColor = foreColor;
Console.BackgroundColor = backColor;
int X = startX * 2;
int Y = startY;
Console.SetCursorPosition(X, Y);
char[] ch = words.ToCharArray();
foreach (char c in ch)
{
if (c=='\r')
{
Y++;
Console.SetCursorPosition(X, Y);
}
else if(c == '\n'){
continue;
}
else
{
Console.Write(c);
}
Thread.Sleep(delay);
}
Console.ResetColor();
}
如此可以简单重现出本游戏了,祝君c#学习愉快
上一篇: 原生python实现knn分类算法
下一篇: C.扫雷