自动玩贪吃蛇的小白痴机器人

偶然间刷到的一个非常治愈的贪吃蛇小视频 于是萌生了制作这个小白痴机器人的念头

使用机器人自动玩贪吃蛇

 

 

首先需要一个能正常玩贪吃蛇的游戏

选用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