自動玩貪吃蛇的小白痴機器人

AnAng發表於2021-08-04

偶然間刷到的一個非常治癒的貪吃蛇小視訊 於是萌生了製作這個小白痴機器人的念頭

使用機器人自動玩貪吃蛇

 

 

首先需要一個能正常玩貪吃蛇的遊戲

選用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);
            }
        }

 

此致 全部就完成了

 

倉庫地址:

https://github.com/2821840032/GreedySnakeIdiot

 

相關文章