自动玩贪吃蛇的小白痴机器人
- 2021 年 8 月 5 日
- 筆記
偶然间刷到的一个非常治愈的贪吃蛇小视频 于是萌生了制作这个小白痴机器人的念头
使用机器人自动玩贪吃蛇
首先需要一个能正常玩贪吃蛇的游戏
选用winform进行开发,非常快和方便
分解需求
首先需要一块画布
在Form1中添加一个panel作为画布
然后需要根据画布大小确定游戏坐标轴
/// <summary> /// 坐标管理 /// </summary> public class LandingPointCore { /// <summary> /// 游戏落地矩阵 /// </summary> List<LandingPoints> LandingPoint { get; set; } public LandingPointCore(float DpiX, float DpiY,int SideLength) { LandingPoint = new List<LandingPoints>(); int SideLengthInterval = SideLength / 3; int LatticeDistance= SideLength + SideLengthInterval; //得到游戏面积 for (int x = 1; x < (DpiX/ LatticeDistance) -1; x++) { for (int y = 1; y < (DpiY/ LatticeDistance) -1; y++) { LandingPoint.Add(new LandingPoints() { PointX = (x * LatticeDistance)- SideLengthInterval, PointY=(y * LatticeDistance)- SideLengthInterval, X=x, Y=y }) ; } } } /// <summary> /// 获取全部坐标 /// </summary> /// <returns></returns> public List<LandingPoints> GetAllLandingPoints() { return LandingPoint; } /// <summary> /// 转换成拥有画布大小的坐标轴 /// </summary> /// <param name="bodies"></param> /// <returns></returns> public IEnumerable<LandingPoints> ExchangePoint(List<BodyPoint> bodies) { return bodies.Select(x=>(LandingPoints)x); } /// <summary> /// 游戏 X Y换算成画布对象 /// 如果为空则表示没有该位置 撞墙了 /// </summary> /// <returns></returns> public BodyPoint XYExchangePoint(int x,int y) { return LandingPoint.FirstOrDefault(c => c.X == x && c.Y == y); } } /// <summary> /// 游戏位置与画布位置 /// </summary> public class LandingPoints: BodyPoint { /// <summary> /// 对应像素点X /// </summary> public int PointX { get; set; } /// <summary> /// 对应像素点Y /// </summary> public int PointY { get; set; } } /// <summary> /// 游戏坐标 /// </summary> public class BodyPoint { /// <summary> /// 游戏坐标X /// </summary> public int X { get; set; } /// <summary> /// 游戏坐标Y /// </summary> public int Y { get; set; } }
通过这个可以根据游戏坐标换算成画布坐标
然后是
画布画正方形并填充颜色伪代码
Graphics Graphic = panel1.CreateGraphics() Rectangle r = new Rectangle(item.PointX, item.PointY, SideLength, SideLength); Graphic.DrawRectangle(Pens.White, r); Graphic.FillRectangle(Brushes.White, r);
稍加改变就能在画布中填充游戏画面
小蛇初始化生成则从游戏坐标中随机选取五位,第一位为头
Graphics Graphic = null; /// <summary> /// 边长 /// </summary> const int SideLength = 20; /// <summary> /// 落点管理 /// </summary> LandingPointCore LandingPointCores { get; set; } /// <summary> /// 蛇身 /// </summary> List<BodyPoint> SnakeBodys { get; set; } /// <summary> /// 蛇头 /// </summary> BodyPoint SnakeHead { get; set; } public GreedySnakeCore(Graphics Graphic) { this.Graphic = Graphic; LandingPointCores = new LandingPointCore(Graphic.VisibleClipBounds.Width, Graphic.VisibleClipBounds.Height, SideLength); SnakeBodys = new List<BodyPoint>(); ///初始化蛇 var snakeBodys = LandingPointCores.GetAllLandingPoints().Take(8).ToList(); SnakeBodys.AddRange(snakeBodys); SnakeHead = snakeBodys[0]; DrawSnake(); }
自此就添加一个和背景不一样的白色条条
有一个判断函数 用来检测某个位置是否可用
这个函数比较重要,很多地方都用得到
/// <summary> /// 判断是否为空地 /// </summary> /// <returns></returns> bool IsOpenSpace(int x,int y) { //空地判断 var changPoint = LandingPointCores.XYExchangePoint(x, y); if (changPoint == null) return false ; if (SnakeBodys.Contains(changPoint)) { return false; } return true; }
创建食物
/// <summary> /// 随机获取食物 /// </summary> public void ObtainFoods() { Random ra = new Random(); for (int i = 0; i < ra.Next(1,2); i++) { var food = LandingPointCores.GetAllLandingPoints().OrderBy(x => Guid.NewGuid()).FirstOrDefault(); if (IsOpenSpace(food.X,food.Y)) { Foods.Add(food); } else { i--; } } }
然后是游戏绘制代码
/// <summary> /// 游戏绘制 /// </summary> public void DrawSnake() { Graphic.Clear(Color.Black); foreach (var item in LandingPointCores.ExchangePoint(SnakeBodys)) { Rectangle r = new Rectangle(item.PointX, item.PointY, SideLength, SideLength); Graphic.DrawRectangle(Pens.White, r); Graphic.FillRectangle(Brushes.White, r); } foreach (var item in LandingPointCores.ExchangePoint(Foods)) { Rectangle r = new Rectangle(item.PointX, item.PointY, SideLength, SideLength); Graphic.DrawRectangle(Pens.Yellow, r); Graphic.FillRectangle(Brushes.Yellow, r); } }
画出纯黑色背景 和白色小蛇 外加黄色的食物
然后需要一个输入来人为控制小蛇的走动
/// <summary> /// 方向Y /// </summary> public enum DirectionY { UP=-1, Down= 1, Wait=0 } /// <summary> /// 方向X /// </summary> public enum DirectionX { Wait = 0, Right = 1, Left = -1 } /// <summary> /// 运动方向X /// </summary> DirectionX SnakeDirectionx { get; set; } /// <summary> /// 运动方向Y /// </summary> DirectionY SnakeDirectiony { get; set; } /// <summary> /// 修改方向 /// </summary> /// <param name="key"></param> public void ModifyDirection(Keys key) { //计算得出第二截相对于第一截的位置 DirectionX directionX =(DirectionX)(SnakeHead.X - SnakeBodys[1].X); DirectionY directionY = (DirectionY)(SnakeHead.Y - SnakeBodys[1].Y); if (key == Keys.Up && directionY != DirectionY.Down) { SnakeDirectionx = DirectionX.Wait; SnakeDirectiony = DirectionY.UP; } if (key == Keys.Down && directionY != DirectionY.UP) { SnakeDirectionx = DirectionX.Wait; SnakeDirectiony = DirectionY.Down; } if (key == Keys.Right && directionX!= DirectionX.Left) { SnakeDirectiony = DirectionY.Wait; SnakeDirectionx = DirectionX.Right; } if (key == Keys.Left && directionX != DirectionX.Right) { SnakeDirectiony = DirectionY.Wait; SnakeDirectionx = DirectionX.Left; } } protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { Core.ModifyDirection(keyData); return base.ProcessCmdKey(ref msg, keyData); }
通过重写Form窗体ProcessCmdKey函数可以有效避开焦点得到按键事件,然后计算出小蛇方向
Form中添加一个计时器,可以用来控制游戏速度,小蛇就可以前行了
/// <summary> /// 时钟前进 /// </summary> public void ClockForward() { var snakeHead = LandingPointCores.XYExchangePoint(SnakeHead.X + (int)SnakeDirectionx, SnakeHead.Y + (int)SnakeDirectiony); if (snakeHead == null) { //撞墙 游戏结束 return; } if (SnakeBodys.Contains(snakeHead)) { //撞身体 游戏结束 return; } //判断是否吃到食物 if (Foods.Contains(snakeHead)) { SnakeHead = snakeHead; SnakeBodys.Insert(0, SnakeHead); Foods.Remove(snakeHead); EatFoodEvent?.Invoke(); } if (Foods.Count <= 0) ObtainFoods(); else { SnakeHead = snakeHead; SnakeBodys.Insert(0, SnakeHead); SnakeBodys.RemoveAt(SnakeBodys.Count - 1); } DrawSnake(); }
然后是小白痴机器人的核心算法啦
/// <summary> /// 下一步去哪? /// </summary> /// <returns></returns> public Keys Wheregonext() { //蛇头位置 var hx = SnakeHead.X; var hy = SnakeHead.Y; //食物距离 var fx = Foods.FirstOrDefault().X; var fy = Foods.FirstOrDefault().Y; //distance结构 方向,与食物的距离,转向后可用步数 Dictionary<Keys, Tuple<int, int>> distance = new Dictionary<Keys, Tuple<int, int>>(); distance.Add(Keys.Left, new Tuple<int, int>(hx - fx, DarkEcho(SnakeHead.X - 1, SnakeHead.Y))); distance.Add(Keys.Right, new Tuple<int, int>(fx - hx, DarkEcho(SnakeHead.X + 1, SnakeHead.Y))); distance.Add(Keys.Up, new Tuple<int, int>(hy - fy, DarkEcho(SnakeHead.X, SnakeHead.Y - 1))); distance.Add(Keys.Down, new Tuple<int, int>(fy - hy, DarkEcho(SnakeHead.X, SnakeHead.Y + 1))); //预测不能走动的方向 var availabledistance = distance.Where(x => x.Value.Item2 > (SnakeBodys.Count)).ToList(); if (availabledistance.Count == 0) { //如果没有可用方向则按可用步数倒序选取第一个方向 return distance.OrderByDescending(x => x.Value.Item2).FirstOrDefault().Key; } //选择食物最小距离 return availabledistance.OrderByDescending(x => x.Value.Item1).FirstOrDefault().Key; } /// <summary> /// 回声探路 /// </summary> /// <param name="x">X坐标</param> /// <param name="y">Y坐标</param> /// <returns></returns> public int DarkEcho(int x, int y) { List<BodyPoint> points = new List<BodyPoint>(); DarkEcho(x, y, points); return points.Count; } /// <summary> /// 回声探路 递归 /// </summary> /// <param name="x">X坐标</param> /// <param name="y">Y坐标</param> /// <returns></returns> public void DarkEcho(int x,int y, List<BodyPoint> points) { if (IsOpenSpace(x, y)&& points.Where(c=>c.X==x&&c.Y==y).Count()==0) { points.Add(new BodyPoint() { X = x, Y = y }); DarkEcho(x - 1, y, points); DarkEcho(x + 1, y, points); DarkEcho(x , y - 1, points); DarkEcho(x , y + 1, points); } }
此致 全部就完成了
仓库地址:
//github.com/2821840032/GreedySnakeIdiot