偶然間刷到的一個非常治癒的貪吃蛇小視訊 於是萌生了製作這個小白痴機器人的念頭
使用機器人自動玩貪吃蛇
首先需要一個能正常玩貪吃蛇的遊戲
選用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