using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using Terminal.Gui; namespace UICatalog.Scenarios; [ScenarioMetadata ("Snake", "The game of apple eating.")] [ScenarioCategory ("Colors")] [ScenarioCategory ("Drawing")] public class Snake : Scenario { private bool isDisposed; public override void Main () { Application.Init (); var win = new Window { Title = GetQuitKeyAndName () }; var state = new SnakeState (); state.Reset (60, 20); var snakeView = new SnakeView (state) { Width = state.Width, Height = state.Height }; win.Add (snakeView); var sw = new Stopwatch (); Task.Run ( () => { while (!isDisposed) { sw.Restart (); if (state.AdvanceState ()) { // When updating from a Thread/Task always use Invoke Application.Invoke (() => { snakeView.SetNeedsDisplay (); }); } long wait = state.SleepAfterAdvancingState - sw.ElapsedMilliseconds; if (wait > 0) { Task.Delay ((int)wait).Wait (); } } } ); Application.Run (win); win.Dispose (); Application.Shutdown (); } protected override void Dispose (bool disposing) { isDisposed = true; base.Dispose (disposing); } private enum Direction { Up, Down, Left, Right } private class SnakeState { public const int AppleGrowRate = 5; public const int MaxSpeed = 20; public const int StartingLength = 10; public const int StartingSpeed = 50; private int step; /// Current position of the Apple that the snake has to eat. public Point Apple { get; private set; } public Direction CurrentDirection { get; private set; } /// Position of the snakes head public Point Head => Snake.Last (); public int Height { get; private set; } public Direction PlannedDirection { get; set; } public int SleepAfterAdvancingState { get; private set; } = StartingSpeed; public List Snake { get; private set; } public int Width { get; private set; } public void GrowSnake () { Point tail = Snake.First (); Snake.Insert (0, tail); } public void GrowSnake (int amount) { for (var i = 0; i < amount; i++) { GrowSnake (); } } internal bool AdvanceState () { step++; if (step < GetStepVelocity ()) { return false; } step = 0; UpdateDirection (); Point newHead = GetNewHeadPoint (); Snake.RemoveAt (0); Snake.Add (newHead); if (IsDeath (newHead)) { GameOver (); } if (newHead == Apple) { GrowSnake (AppleGrowRate); Apple = GetNewRandomApplePoint (); var delta = 5; if (SleepAfterAdvancingState < 40) { delta = 3; } if (SleepAfterAdvancingState < 30) { delta = 2; } SleepAfterAdvancingState = Math.Max (MaxSpeed, SleepAfterAdvancingState - delta); } return true; } /// Restarts the game with the given canvas size /// /// internal void Reset (int width, int height) { if (width < 5 || height < 5) { return; } Width = width; Height = height; var middle = new Point (width / 2, height / 2); // Start snake with a length of 2 Snake = new List { middle, middle }; Apple = GetNewRandomApplePoint (); SleepAfterAdvancingState = StartingSpeed; GrowSnake (StartingLength); } private bool AreOpposites (Direction a, Direction b) { switch (a) { case Direction.Left: return b == Direction.Right; case Direction.Right: return b == Direction.Left; case Direction.Up: return b == Direction.Down; case Direction.Down: return b == Direction.Up; } return false; } private void GameOver () { Reset (Width, Height); } private Point GetNewHeadPoint () { switch (CurrentDirection) { case Direction.Left: return new Point (Head.X - 1, Head.Y); case Direction.Right: return new Point (Head.X + 1, Head.Y); case Direction.Up: return new Point (Head.X, Head.Y - 1); case Direction.Down: return new Point (Head.X, Head.Y + 1); } throw new Exception ("Unknown direction"); } private Point GetNewRandomApplePoint () { var r = new Random (); for (var i = 0; i < 1000; i++) { int x = r.Next (0, Width); int y = r.Next (0, Height); var p = new Point (x, y); if (p == Head) { continue; } if (IsDeath (p)) { continue; } return p; } // Game is won or we are unable to generate a valid apple // point after 1000 attempts. Maybe screen size is very small // or something. Either way restart the game. Reset (Width, Height); return Apple; } private int GetStepVelocity () { if (CurrentDirection == Direction.Left || CurrentDirection == Direction.Right) { return 1; } return 2; } private bool IsDeath (Point p) { if (p.X <= 0 || p.X >= Width - 1) { return true; } if (p.Y <= 0 || p.Y >= Height - 1) { return true; } if (Snake.Take (Snake.Count - 1).Contains (p)) { return true; } return false; } private void UpdateDirection () { if (!AreOpposites (CurrentDirection, PlannedDirection)) { CurrentDirection = PlannedDirection; } } } private class SnakeView : View { private readonly Rune _appleRune; private readonly Attribute red = new (Color.Red, Color.Black); private readonly Attribute white = new (Color.White, Color.Black); public SnakeView (SnakeState state) { _appleRune = CM.Glyphs.Apple; if (!Driver.IsRuneSupported (_appleRune)) { _appleRune = CM.Glyphs.AppleBMP; } State = state; CanFocus = true; ColorScheme = new ColorScheme { Normal = white, Focus = white, HotNormal = white, HotFocus = white, Disabled = white }; } public SnakeState State { get; } public override void OnDrawContent (Rectangle viewport) { base.OnDrawContent (viewport); Driver.SetAttribute (white); Clear (); var canvas = new LineCanvas (); canvas.AddLine (Point.Empty, State.Width, Orientation.Horizontal, LineStyle.Double); canvas.AddLine (Point.Empty, State.Height, Orientation.Vertical, LineStyle.Double); canvas.AddLine (new Point (0, State.Height - 1), State.Width, Orientation.Horizontal, LineStyle.Double); canvas.AddLine (new Point (State.Width - 1, 0), State.Height, Orientation.Vertical, LineStyle.Double); for (var i = 1; i < State.Snake.Count; i++) { Point pt1 = State.Snake [i - 1]; Point pt2 = State.Snake [i]; Orientation orientation = pt1.X == pt2.X ? Orientation.Vertical : Orientation.Horizontal; int length = orientation == Orientation.Horizontal ? pt1.X > pt2.X ? 2 : -2 : pt1.Y > pt2.Y ? 2 : -2; canvas.AddLine ( pt2, length, orientation, LineStyle.Single ); } foreach (KeyValuePair p in canvas.GetMap (Viewport)) { AddRune (p.Key.X, p.Key.Y, p.Value); } Driver.SetAttribute (red); AddRune (State.Apple.X, State.Apple.Y, _appleRune); Driver.SetAttribute (white); } // BUGBUG: Should (can) this use key bindings instead. public override bool OnKeyDown (Key keyEvent) { if (keyEvent.KeyCode == KeyCode.CursorUp) { State.PlannedDirection = Direction.Up; return true; } if (keyEvent.KeyCode == KeyCode.CursorDown) { State.PlannedDirection = Direction.Down; return true; } if (keyEvent.KeyCode == KeyCode.CursorLeft) { State.PlannedDirection = Direction.Left; return true; } if (keyEvent.KeyCode == KeyCode.CursorRight) { State.PlannedDirection = Direction.Right; return true; } return false; } } }