Snake.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. using Terminal.Gui;
  7. using Terminal.Gui.Graphs;
  8. using Attribute = Terminal.Gui.Attribute;
  9. namespace UICatalog.Scenarios {
  10. [ScenarioMetadata (Name: "Snake", Description: "The game of apple eating.")]
  11. [ScenarioCategory ("Colors")]
  12. public class Snake : Scenario {
  13. private bool isDisposed;
  14. public override void Setup ()
  15. {
  16. base.Setup ();
  17. var state = new SnakeState ();
  18. state.Reset (60, 20);
  19. var snakeView = new SnakeView (state) {
  20. Width = state.Width,
  21. Height = state.Height
  22. };
  23. Win.Add (snakeView);
  24. Stopwatch sw = new Stopwatch ();
  25. Task.Run (() => {
  26. while (!isDisposed) {
  27. sw.Restart ();
  28. if (state.AdvanceState ()) {
  29. // When updating from a Thread/Task always use Invoke
  30. Application.MainLoop?.Invoke (() => {
  31. snakeView.SetNeedsDisplay ();
  32. });
  33. }
  34. var wait = state.SleepAfterAdvancingState - sw.ElapsedMilliseconds;
  35. if (wait > 0) {
  36. Task.Delay ((int)wait).Wait ();
  37. }
  38. }
  39. });
  40. }
  41. protected override void Dispose (bool disposing)
  42. {
  43. isDisposed = true;
  44. base.Dispose (disposing);
  45. }
  46. private class SnakeView : View {
  47. private Attribute red = new Terminal.Gui.Attribute (Color.Red,Color.Black);
  48. private Attribute white = new Terminal.Gui.Attribute (Color.White, Color.Black);
  49. public SnakeState State { get; }
  50. public SnakeView (SnakeState state)
  51. {
  52. State = state;
  53. CanFocus = true;
  54. ColorScheme = new ColorScheme {
  55. Normal = white,
  56. Focus = white,
  57. HotNormal = white,
  58. HotFocus = white,
  59. Disabled = white
  60. };
  61. }
  62. public override void Redraw (Rect bounds)
  63. {
  64. base.Redraw (bounds);
  65. Driver.SetAttribute (white);
  66. Clear ();
  67. var canvas = new LineCanvas ();
  68. canvas.AddLine (new Point (0, 0), State.Width - 1, Orientation.Horizontal, BorderStyle.Double);
  69. canvas.AddLine (new Point (0, 0), State.Height - 1, Orientation.Vertical, BorderStyle.Double);
  70. canvas.AddLine (new Point (0, State.Height - 1), State.Width - 1, Orientation.Horizontal, BorderStyle.Double);
  71. canvas.AddLine (new Point (State.Width - 1, 0), State.Height - 1, Orientation.Vertical, BorderStyle.Double);
  72. for (int i = 1; i < State.Snake.Count; i++) {
  73. var pt1 = State.Snake [i - 1];
  74. var pt2 = State.Snake [i];
  75. var orientation = pt1.X == pt2.X ? Orientation.Vertical : Orientation.Horizontal;
  76. var length = orientation == Orientation.Horizontal
  77. ? pt1.X > pt2.X ? 1 : -1
  78. : pt1.Y > pt2.Y ? 1 : -1;
  79. canvas.AddLine (
  80. pt2,
  81. length,
  82. orientation,
  83. BorderStyle.Single);
  84. }
  85. foreach(var p in canvas.GenerateImage (bounds)) {
  86. AddRune (p.Key.X, p.Key.Y, p.Value);
  87. }
  88. Driver.SetAttribute (red);
  89. AddRune (State.Apple.X, State.Apple.Y, 'A');
  90. Driver.SetAttribute (white);
  91. }
  92. public override bool OnKeyDown (KeyEvent keyEvent)
  93. {
  94. if (keyEvent.Key == Key.CursorUp) {
  95. State.PlannedDirection = Direction.Up;
  96. return true;
  97. }
  98. if (keyEvent.Key == Key.CursorDown) {
  99. State.PlannedDirection = Direction.Down;
  100. return true;
  101. }
  102. if (keyEvent.Key == Key.CursorLeft) {
  103. State.PlannedDirection = Direction.Left;
  104. return true;
  105. }
  106. if (keyEvent.Key == Key.CursorRight) {
  107. State.PlannedDirection = Direction.Right;
  108. return true;
  109. }
  110. return false;
  111. }
  112. }
  113. private class SnakeState {
  114. public const int StartingLength = 10;
  115. public const int AppleGrowRate = 5;
  116. public const int StartingSpeed = 50;
  117. public const int MaxSpeed = 20;
  118. public int Width { get; private set; }
  119. public int Height { get; private set; }
  120. /// <summary>
  121. /// Position of the snakes head
  122. /// </summary>
  123. public Point Head => Snake.Last ();
  124. /// <summary>
  125. /// Current position of the Apple that the snake has to eat.
  126. /// </summary>
  127. public Point Apple { get; private set; }
  128. public Direction CurrentDirection { get; private set; }
  129. public Direction PlannedDirection { get; set; }
  130. public List<Point> Snake { get; private set; }
  131. public int SleepAfterAdvancingState { get; private set; } = StartingSpeed;
  132. int step;
  133. internal bool AdvanceState ()
  134. {
  135. step++;
  136. if (step < GetStepVelocity ()) {
  137. return false;
  138. }
  139. step = 0;
  140. UpdateDirection ();
  141. var newHead = GetNewHeadPoint ();
  142. Snake.RemoveAt (0);
  143. Snake.Add (newHead);
  144. if (IsDeath (newHead)) {
  145. GameOver ();
  146. }
  147. if (newHead == Apple) {
  148. GrowSnake (AppleGrowRate);
  149. Apple = GetNewRandomApplePoint ();
  150. var delta = 5;
  151. if(SleepAfterAdvancingState < 40) {
  152. delta = 3;
  153. }
  154. if (SleepAfterAdvancingState < 30) {
  155. delta = 2;
  156. }
  157. SleepAfterAdvancingState = Math.Max (MaxSpeed, SleepAfterAdvancingState - delta);
  158. }
  159. return true;
  160. }
  161. private int GetStepVelocity ()
  162. {
  163. if (CurrentDirection == Direction.Left || CurrentDirection == Direction.Right) {
  164. return 1;
  165. }
  166. return 2;
  167. }
  168. public void GrowSnake ()
  169. {
  170. var tail = Snake.First ();
  171. Snake.Insert (0, tail);
  172. }
  173. public void GrowSnake (int amount)
  174. {
  175. for (int i = 0; i < amount; i++) {
  176. GrowSnake ();
  177. }
  178. }
  179. private void UpdateDirection ()
  180. {
  181. if (!AreOpposites (CurrentDirection, PlannedDirection)) {
  182. CurrentDirection = PlannedDirection;
  183. }
  184. }
  185. private bool AreOpposites (Direction a, Direction b)
  186. {
  187. switch (a) {
  188. case Direction.Left: return b == Direction.Right;
  189. case Direction.Right: return b == Direction.Left;
  190. case Direction.Up: return b == Direction.Down;
  191. case Direction.Down: return b == Direction.Up;
  192. }
  193. return false;
  194. }
  195. private void GameOver ()
  196. {
  197. Reset (Width, Height);
  198. }
  199. private Point GetNewHeadPoint ()
  200. {
  201. switch (CurrentDirection) {
  202. case Direction.Left:
  203. return new Point (Head.X - 1, Head.Y);
  204. case Direction.Right:
  205. return new Point (Head.X + 1, Head.Y);
  206. case Direction.Up:
  207. return new Point (Head.X, Head.Y - 1);
  208. case Direction.Down:
  209. return new Point (Head.X, Head.Y + 1);
  210. }
  211. throw new Exception ("Unknown direction");
  212. }
  213. /// <summary>
  214. /// Restarts the game with the given canvas size
  215. /// </summary>
  216. /// <param name="width"></param>
  217. /// <param name="height"></param>
  218. internal void Reset (int width, int height)
  219. {
  220. if (width < 5 || height < 5) {
  221. return;
  222. }
  223. Width = width;
  224. Height = height;
  225. var middle = new Point (width / 2, height / 2);
  226. // Start snake with a length of 2
  227. Snake = new List<Point> { middle, middle };
  228. Apple = GetNewRandomApplePoint ();
  229. SleepAfterAdvancingState = StartingSpeed;
  230. GrowSnake (StartingLength);
  231. }
  232. private Point GetNewRandomApplePoint ()
  233. {
  234. Random r = new Random ();
  235. for (int i = 0; i < 1000; i++) {
  236. var x = r.Next (0, Width);
  237. var y = r.Next (0, Height);
  238. var p = new Point (x, y);
  239. if (p == Head) {
  240. continue;
  241. }
  242. if (IsDeath (p)) {
  243. continue;
  244. }
  245. return p;
  246. }
  247. // Game is won or we are unable to generate a valid apple
  248. // point after 1000 attempts. Maybe screen size is very small
  249. // or something. Either way restart the game.
  250. Reset (Width, Height);
  251. return Apple;
  252. }
  253. private bool IsDeath (Point p)
  254. {
  255. if (p.X <= 0 || p.X >= Width - 1) {
  256. return true;
  257. }
  258. if (p.Y <= 0 || p.Y >= Height - 1) {
  259. return true;
  260. }
  261. if (Snake.Take (Snake.Count - 1).Contains (p))
  262. return true;
  263. return false;
  264. }
  265. }
  266. private enum Direction {
  267. Up,
  268. Down,
  269. Left,
  270. Right
  271. }
  272. }
  273. }