10
\$\begingroup\$

This is my first console application in C# and I think it's unnecessarily wrong and I made some bad practices, but at least it works smoothly. How could I improve this code so that next time I write a console application it won't be as garbled as this?

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1 {
 class Program {

  static readonly int gridW = 90;
  static readonly int gridH = 25;
  static Cell[, ] grid = new Cell[gridH, gridW];
  static Cell currentCell;
  static Cell food;
  static int FoodCount;
  static int direction; //0=Up 1=Right 2=Down 3=Left
  static readonly int speed = 1;
  static bool Populated = false;
  static bool Lost = false;
  static int snakeLength;

  static void Main(string[] args) {
   if (!Populated) {
    FoodCount = 0;
    snakeLength = 5;
    populateGrid();
    currentCell = grid[(int) Math.Ceiling((double) gridH / 2), (int) Math.Ceiling((double) gridW / 2)];
    updatePos();
    addFood();
    Populated = true;
   }

   while (!Lost) {
    Restart();
   }
  }

  static void Restart() {
   Console.SetCursorPosition(0, 0);
   printGrid();
   Console.WriteLine("Length: {0}", snakeLength);
   getInput();
  }

  static void updateScreen() {
   Console.SetCursorPosition(0, 0);
   printGrid();
   Console.WriteLine("Length: {0}", snakeLength);
  }

  static void getInput() {

   //Console.Write("Where to move? [WASD] ");
   ConsoleKeyInfo input;
   while (!Console.KeyAvailable) {
    Move();
    updateScreen();
   }
   input = Console.ReadKey();
   doInput(input.KeyChar);
  }

  static void checkCell(Cell cell) {
   if (cell.val == "%") {
    eatFood();
   }
   if (cell.visited) {
    Lose();
   }
  }

  static void Lose() {
   Console.WriteLine("\n You lose!");
   Thread.Sleep(1000);
   Process.Start(System.Reflection.Assembly.GetExecutingAssembly().Location);
   Environment.Exit(-1);
  }

  static void doInput(char inp) {
   switch (inp) {
    case 'w':
     goUp();
     break;
    case 's':
     goDown();
     break;
    case 'a':
     goRight();
     break;
    case 'd':
     goLeft();
     break;
   }
  }

  static void addFood() {
   Random r = new Random();
   Cell cell;
   while (true) {
    cell = grid[r.Next(grid.GetLength(0)), r.Next(grid.GetLength(1))];
    if (cell.val == " ")
     cell.val = "%";
    break;
   }
  }

  static void eatFood() {
   snakeLength += 1;
   addFood();
  }

  static void goUp() {
   if (direction == 2)
    return;
   direction = 0;
  }

  static void goRight() {
   if (direction == 3)
    return;
   direction = 1;
  }

  static void goDown() {
   if (direction == 0)
    return;
   direction = 2;
  }

  static void goLeft() {
   if (direction == 1)
    return;
   direction = 3;
  }

  static void Move() {
   if (direction == 0) {
    //up
    if (grid[currentCell.y - 1, currentCell.x].val == "*") {
     Lose();
     return;
    }
    visitCell(grid[currentCell.y - 1, currentCell.x]);
   } else if (direction == 1) {
    //right
    if (grid[currentCell.y, currentCell.x - 1].val == "*") {
     Lose();
     return;
    }
    visitCell(grid[currentCell.y, currentCell.x - 1]);
   } else if (direction == 2) {
    //down
    if (grid[currentCell.y + 1, currentCell.x].val == "*") {
     Lose();
     return;
    }
    visitCell(grid[currentCell.y + 1, currentCell.x]);
   } else if (direction == 3) {
    //left
    if (grid[currentCell.y, currentCell.x + 1].val == "*") {
     Lose();
     return;
    }
    visitCell(grid[currentCell.y, currentCell.x + 1]);
   }
   Thread.Sleep(speed * 100);
  }

  static void visitCell(Cell cell) {
   currentCell.val = "#";
   currentCell.visited = true;
   currentCell.decay = snakeLength;
   checkCell(cell);
   currentCell = cell;
   updatePos();

   //checkCell(currentCell);
  }

  static void updatePos() {

   currentCell.Set("@");
   if (direction == 0) {
    currentCell.val = "^";
   } else if (direction == 1) {
    currentCell.val = "<";
   } else if (direction == 2) {
    currentCell.val = "v";
   } else if (direction == 3) {
    currentCell.val = ">";
   }

   currentCell.visited = false;
   return;
  }

  static void populateGrid() {
   Random random = new Random();
   for (int col = 0; col < gridH; col++) {
    for (int row = 0; row < gridW; row++) {
     Cell cell = new Cell();
     cell.x = row;
     cell.y = col;
     cell.visited = false;
     if (cell.x == 0 || cell.x > gridW - 2 || cell.y == 0 || cell.y > gridH - 2)
      cell.Set("*");
     else
      cell.Clear();
     grid[col, row] = cell;
    }
   }
  }

  static void printGrid() {
   string toPrint = "";
   for (int col = 0; col < gridH; col++) {
    for (int row = 0; row < gridW; row++) {
     grid[col, row].decaySnake();
     toPrint += grid[col, row].val;

    }
    toPrint += "\n";
   }
   Console.WriteLine(toPrint);
  }
  public class Cell {
   public string val {
    get;
    set;
   }
   public int x {
    get;
    set;
   }
   public int y {
    get;
    set;
   }
   public bool visited {
    get;
    set;
   }
   public int decay {
    get;
    set;
   }

   public void decaySnake() {
    decay -= 1;
    if (decay == 0) {
     visited = false;
     val = " ";
    }
   }

   public void Clear() {
    val = " ";
   }

   public void Set(string newVal) {
    val = newVal;
   }
  }
 }
}
\$\endgroup\$
  • \$\begingroup\$ Does that code work as intended? \$\endgroup\$ – πάντα ῥεῖ Jan 3 at 19:16
  • \$\begingroup\$ it appears to run when I try it on onlinegdb though that interactive interface isn't optimal... \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Jan 3 at 19:27
  • \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ i didnt intend it to be used in online interpreters, i wrote it in visual studio \$\endgroup\$ – Terradice Jan 3 at 19:35
  • 2
    \$\begingroup\$ I know - I just wanted to test it without VS \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Jan 3 at 19:35
11
\$\begingroup\$

Well, this is my first code review and by no means I'm an C# expert or something like that, but anyways I'd like to give a general opinion about your code.

First and foremost, you're using C# which is mostly an OO language, so I encourage you to write your code using OO constructs. In this case, it means that you should create classes to abstract the things that build your game. So for example, you would create a SnakeGame class, a Snake class, etc. This not only will help you to make your program more maintainable, but also will allow you to remove all the static properties and methods you've created.

Ideally, you Main method should be as short as possible. For example, inside your SnakeGame you could create a public Run method and in Main you would only need to instantiate that class and only invoke its Run method. In code:

static void Main(string[] args)
{
   var game = new SnakeGame();
   game.Run();
}

On the other hand, don't use int when you can use an enum. In your program you've used an int for the field direction. Instead you could create a simple enum:

enum Direction
{
   Up, Right, Down, Left
}

Then in the hypothetical SnakeGame class you could have: private Direction snakeDirection;, instead of static int direction; that you currently have in the class Program.

About the style I noticed that you have mixed camelCase with PascalCase for fields and methods: e.g., the fields Populated and Lost use PascalCase whereas the field snakeLength uses camelCase. Likewise, the method updateScreen uses camelCase, but the method Restart uses PascalCase, and this is not good. Choose a style and stick with it: be consistent!. FYI, in C# is common to use camelCase for variables, in particular for fields, and PascalCase for classes and methods. So my general advice here would be to follow the standard conventions. And since we're talking about conventions, it's also a common standard in C# to use the Allman style for indentation.

Finally, there are a couple things I want to comment. First is about the class Cell. From the way you use it in your code, there is already a built-in struct in .NET framework that would do most of the work for you: the struct Point. It's in the namespace System.Drawing. As its name says, it represent a point in Cartesian coordinates and it has already properties to get and set the X and Y coordinates among other methods.

And about namespaces, only include those that you really need. For example, you could safely remove the namespace System.Collections.Generic System.Text, etc., and instead add the namespace System.Reflection. But, in a better designed code you wouldn't need such a namespace ;)

\$\endgroup\$
1
\$\begingroup\$

I've been flirting with writing a snake game console app in C# for a while. Running across your question on a weekend when my girlfriend is working allowed me to devote some time to it.

Thank you for inspiring me, and providing working algorithms. Your program runs fine for me. It taught me some general concepts about this type of game - like Decay(), along with things like waiting for a console keypress without blocking. Thanks also to @Xam for your comments and the heads up on the Point class (which I am happy to know about even though I didn't use it in this case). Overall, I converted the existing code to my personal object-oriented programming (OOP) style.

While the code captures the bulk of my commentary, here are some additional points:

  1. Phil Karlton said one of my favorite programming quotes of all time: “There are only two hard things in Computer Science: cache invalidation and naming things.” While I have done little with cache invalidation in my coding career, naming is a constant practice for all developers. I like to invest time and energy to think of names that make sense.
  2. On that note, I typically rely on the domain for many names. When coding a snake game, it’s very likely that I’ll have Game, Board, and Snake classes. While you managed to wrangle your solution into just two classes, to me, such an approach does little to describe and document the domain. I wound up with six “domain” classes (excluding the Program and the “App”).
  3. Considering that the size of the snake will never be huge, to enable easy adds and removes, I used a LinkedList for the Snake’s body. (first time I’ve used a LinkedList in a while – perhaps since college.)
  4. I use the relatively new C# feature of Expression-Bodied members pretty much wherever possible – e.g. public int Length => BodyLength + 1;.
  5. I typically avoid “static”. I even keep Main() as short as possible, immediately passing control to a normal non-static class. The one exception I made in this case was to make the DirectionMap a “global” variable – i.e. static on the Board class. I use it in a few different places and wanted to avoid passing a board object all over.
  6. When first building the object model, I had cells being able to find their own neighbors. That relied on the board’s grid being static so that anyone could access it. Then I thought, “To find its neighbors, a cell has to know the whole board, which might be stretching their responsibilities too far.” So, I made finding a cell’s neighbors the responsibility of the board, which allowed the grid to become private again. Likewise, I had the Snake determining whether it had collided with something, but decided that also lies within the Board class’s responsibilities.
  7. In short, I consider each class’s nature and boundaries to figure out what properties and responsibilities it “owns” and what should go on another class, or a new class.
  8. Other areas where I erred on the side of abstracting / encapsulating things include the eHeading enum and the Direction class.
  9. David West says something to the effect that at its core, object-oriented programming (OOP) is an approach to message passing. In this case, I’m using crude (but effective) ways to pass messages between objects, such as the Snake’s “HasEaten” flag. It lets the board know that it needs to put out more food. Ostensibly this whole thing could be an event-driven app, with events like “OnStart”, “OnKeyPress”, “OnMove”, “OnEat”, and “OnCollide”. While I’ve done some event-driven stuff in C#, it’s far from my strong suit, so I stayed with the approach of looping through turns.
  10. Pausing between each turn to slow the game to human speed is great. In your code as the speed increases so does the pause, making 1 the fastest speed. I found it more intuitive to make the pause go down as the speed goes up. And, I set a max speed of 5 (which is now the fastest speed).
  11. I employ a concept I call “endoskeletal programming". Perhaps because I am a bit neurotic about the appearance and readability of my code, I typically conceal monstrous predicates and other calculations, even small ones, in their own property or method. This gives the benefit of naming the predicate or calculation, which in my opinion boosts readability and makes the code largely self-documenting.

For example: to determine if a cell is part of the border you had:

if (cell.x == 0 || cell.x > gridW - 2 || cell.y == 0 || cell.y > gridH - 2)
    cell.Set("*");
else
    cell.Clear();

My approach is:

private int leftEdgeX => 0;
private int rightEdgeX => Width - 1;
private int topEdgeY => 0;
private int bottomEdgeY => Height - 1;
private bool isBorder(Cell cell) => cell.X == leftEdgeX || cell.X >= rightEdgeX || cell.Y == topEdgeY || cell.Y >= bottomEdgeY;
if (isBorder(cell))
{
    cell.SetBorder();
}
else
{
    cell.SetEmpty();
}

They both get the job done. My intention is to give more insight into what the job is and how we’re doing it.

As you can probably tell, I'm a fan of OOP. Not everyone is. Even among those that do, some consider my style verbose. You proved that we can express the same logic in a far shorter code base. But, I optimize for readability, maintainability, domain descriptiveness, and my own sense of craftsmanship… rather than size.

Things we could add / modify include:

  • A separate Body class
  • Avoid putting food in a corner (though it is theortically possible to get food from a corner)
  • Cell.IsHead and Cell.IsTail properties
  • Prompt user for speed when starting and show speed
  • A game timer
  • Declare instructions on the Game class and possibly even add separate Instructions class
  • Come up with a better name than "DoTurn"
  • Avoid having DirectionMap as static

Here is the code. I am interested in your feedback on this approach.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace SnakeGame
{
    class Program
    {
        static void Main(string[] args)
        {
            var app = new SnakeGameApp();
            app.Run();
            if (System.Diagnostics.Debugger.IsAttached)
            {
                Console.WriteLine("\nPress <Enter> to continue...");
                Console.ReadLine();
            }
        }
    }

    public class SnakeGameApp
    {
        private readonly int delayBetweenGames = 1500;

        public void Run()
        {
            var finished = false;
            while (!finished)
            {
                var game = new Game();
                game.Play();
                finished = game.Quit;
                if (game.Lost)
                {
                    Console.WriteLine("\nYou lost.");
                    Thread.Sleep(delayBetweenGames);
                    Console.Clear();
                }
            }
        }
    }

    public class Game
    {
        private readonly eHeading initialHeading = eHeading.Up;
        private DirectionMap directionMap = Board.DirectionMap;

        private Board board;
        public bool Lost { get; private set; } = false;
        public bool Quit { get; private set; } = false;

        public Game(int snakeLength = 5)
        {
            board = new Board();
            var head = board.Center;
            board.Add(new Snake(head, initBody(head, snakeLength - 1), directionMap.Get(initialHeading)));
            board.AddFood(); //to avoid the snake overwriting food, add food after snake
            board.Draw();
        }

        public void Play()
        {
            while (!Lost && !Quit)
            {
                ConsoleKeyInfo input;
                if (Console.KeyAvailable)
                {
                    input = Console.ReadKey(true); //intercept = true (don't print char on console)
                    //If the directionMap doesn't find a direction for the character 
                    //the user pressed it throws an exception.
                    //Wrapping this in a try/catch allows us to ignore all non-direction keys
                    //except Escape, which quits the game.
                    try
                    {
                        var direction = directionMap.Get(input.KeyChar);
                        board.DoTurn(direction);
                        Lost = board.HasCollided;
                    }
                    catch
                    {
                        Quit = input.Key == ConsoleKey.Escape;
                    }
                }
                else
                {
                    board.DoTurn();
                    Lost = board.HasCollided;
                }
            }
        }

        private LinkedList<Cell> initBody(Cell head, int bodyLength)
        {
            var body = new LinkedList<Cell>();
            var current = board.BottomNeighbor(head);
            for (var i = 0; i < bodyLength; i++)
            {
                body.AddLast(current);
                current.SetBody(bodyLength - i);
                current = board.BottomNeighbor(current);
            }
            return body;
        }
    }

    public class Board
    {
        public static DirectionMap DirectionMap { get; } = new DirectionMap();

        private readonly string instructions = "How to Play: Avoid hitting walls or yourself. Grow by eating food (%). Highest length wins.";
        private readonly string commandBase = "Commands: {0}, Esc: Quit\n";
        private readonly string lengthBase = "Length: {0}\n";

        private Cell[,] grid;
        private Random random = new Random();
        private int leftEdgeX => 0;
        private int rightEdgeX => Width - 1;
        private int topEdgeY => 0;
        private int bottomEdgeY => Height - 1;

        public Snake Snake { get; private set; }
        public int Height { get; private set; }
        public int Width { get; private set; }
        public Cell Current => Snake.Head;
        public Cell Center => get(Width / 2, Height / 2);
        public Cell Food { get; private set; }
        public bool HasCollided { get; private set; }

        public Board(int width = 90, int height = 25)
        {
            Width = width;
            Height = height;
            grid = new Cell[Width, Height];
            initGrid();
        }

        //continue in the same direction
        public void DoTurn()
        {
            doTurn(Snake.Direction, getDestination(Snake.Direction));
        }

        public void DoTurn(Direction direction)
        {
            doTurn(direction, getDestination(direction));
        }

        public void Draw()
        {
            Console.SetCursorPosition(0, 0);
            Console.WriteLine(ToString());
        }

        public void Add(Snake snake) => Snake = snake;
        public void AddFood() => randomCell().SetFood();

        public Cell TopNeighbor(Cell cell) => grid[cell.X, cell.Y - 1];
        public Cell RightNeighbor(Cell cell) => grid[cell.X + 1, cell.Y];
        public Cell BottomNeighbor(Cell cell) => grid[cell.X, cell.Y + 1];
        public Cell LeftNeighbor(Cell cell) => grid[cell.X - 1, cell.Y];

        public override string ToString()
        {
            var sb = new StringBuilder();
            //y is in outer loop, so we draw by rows
            for (int y = 0; y < Height; y++)
            {
                for (int x = 0; x < Width; x++)
                {
                    sb.Append(grid[x, y].Value);
                }
                sb.Append("\n"); //terminate row
            }
            sb.AppendFormat(lengthBase, Snake.Length);
            sb.AppendLine(instructions);
            sb.AppendFormat(commandBase, DirectionMap.ToString());
            return sb.ToString();
        }

        private Cell get(int x, int y) => grid[x, y];

        private void add(Cell cell) => grid[cell.X, cell.Y] = cell;

        private bool isBorder(Cell cell) => cell.X == leftEdgeX || cell.X >= rightEdgeX
                                         || cell.Y == topEdgeY || cell.Y >= bottomEdgeY;

        private void doTurn(Direction direction, Cell target)
        {
            if (isLegalMove(direction, target))
            {
                Snake.Move(direction, target);

                if (Snake.HasEaten)
                {
                    Snake.Grow(getNewTail());
                    AddFood();
                }

                Draw();
            }
        }

        private bool isLegalMove(Direction direction, Cell target)
        {
            if (direction.IsOpposite(Snake.Direction))
            {
                return false;
            }

            HasCollided = target.IsForbidden;

            return !HasCollided;
        }

        private Cell getDestination(Direction direction) => getDirectionalNeighbor(Snake.Head, direction);

        private Cell getNewTail() => getDirectionalNeighbor(Snake.Tail, Snake.Direction.Opposite);

        private Cell getDirectionalNeighbor(Cell cell, Direction direction)
        {
            var neighbor = new Cell(-1, -1); //initialize to dummy cell

            if (direction.IsUp)
            {
                neighbor = TopNeighbor(cell);
            }
            else if (direction.IsRight)
            {
                neighbor = RightNeighbor(cell);
            }
            else if (direction.IsDown)
            {
                neighbor = BottomNeighbor(cell);
            }
            else if (direction.IsLeft)
            {
                neighbor = LeftNeighbor(cell);
            }

            return neighbor;
        }

        private Cell randomCell()
        {
            bool isEmpty;
            var cell = new Cell(-1, -1); //initialize to dummy cell
            do
            {
                cell = grid[random.Next(Width), random.Next(Height)];
                isEmpty = cell.IsEmpty;
            } while (!isEmpty);

            return cell;
        }

        private void initGrid()
        {
            for (int y = 0; y < Height; y++)
            {
                for (int x = 0; x < Width; x++)
                {
                    var cell = new Cell(x, y);

                    add(cell);

                    if (isBorder(cell))
                    {
                        cell.SetBorder();
                    }
                    else
                    {
                        cell.SetEmpty();
                    }
                }
            }
        }
    }

    //int values are degrees
    public enum eHeading
    {
        Up = 0,
        Right = 90,
        Down = 180,
        Left = 270
    }

    public class Direction
    {
        public eHeading Heading { get; private set; }
        public char KeyPress { get; private set; }
        public char HeadToken { get; private set; }
        public int Degrees => (int)Heading;
        public Direction Opposite => Board.DirectionMap.Get((eHeading)(Degrees >= 180 ? Degrees - 180 : Degrees + 180));

        public bool IsUp => Heading == eHeading.Up;
        public bool IsRight => Heading == eHeading.Right;
        public bool IsDown => Heading == eHeading.Down;
        public bool IsLeft => Heading == eHeading.Left;

        public Direction(eHeading vector, char keyPress, char headToken)
        {
            Heading = vector;
            KeyPress = keyPress;
            HeadToken = headToken;
        }

        public bool IsOpposite(Direction dir) => Math.Abs(Degrees - dir.Degrees) == 180;

        public bool IsSame(Direction dir) => Heading == dir.Heading;

        public string ToCommand() => $"{KeyPress}: {Heading}";
    }

    public class DirectionMap
    {
        private Dictionary<char, Direction> _directionKeys;
        private Dictionary<char, Direction> directionKeys
        {
            get
            {
                _directionKeys = _directionKeys ?? directionKeyMap();
                return _directionKeys;
            }
        }

        private Dictionary<eHeading, Direction> _directionVectors;
        private Dictionary<eHeading, Direction> directionVectors
        {
            get
            {
                _directionVectors = _directionVectors ?? directionVectorMap();
                return _directionVectors;
            }
        }

        public Direction Get(char c)
        {
            if (directionKeys.TryGetValue(c, out Direction direction))
            {
                return direction;
            }
            else
            {
                throw new Exception($"{c} not found in direction map.");
            }
        }

        public Direction Get(eHeading vector)
        {
            if (directionVectors.TryGetValue(vector, out Direction direction))
            {
                return direction;
            }
            else
            {
                throw new Exception($"Vector {vector.ToString()} not found in direction map.");
            }
        }

        public override string ToString() => string.Join(", ", directionVectors.Select(v => v.Value.ToCommand()));

        private Dictionary<eHeading, Direction> directionVectorMap()
        {
            return new Dictionary<eHeading, Direction>
            {
                {eHeading.Left, new Direction(eHeading.Left , 'a', '<')},
                {eHeading.Right, new Direction(eHeading.Right,'d', '>') },
                {eHeading.Down, new Direction(eHeading.Down, 's', 'v') },
                {eHeading.Up, new Direction(eHeading.Up, 'w', '^') }
            };
        }

        private Dictionary<char, Direction> directionKeyMap() => directionVectors.ToDictionary(d => d.Value.KeyPress, d => d.Value);
    }

    public class Cell
    {
        private readonly char unitializedToken = char.MinValue;
        private readonly char emptyToken = ' ';
        private readonly char borderToken = '*';
        private readonly char bodyToken = '#';
        private readonly char foodToken = '%';

        private int remaining;

        public int X { get; private set; }
        public int Y { get; private set; }
        public char Value { get; private set; }

        public bool IsBorder => Value == borderToken;
        public bool IsBody => Value == bodyToken;
        public bool IsFood => Value == foodToken;
        public bool IsEmpty => Value == emptyToken || Value == unitializedToken;
        public bool IsForbidden => IsBorder || IsBody;

        public Cell(int x, int y)
        {
            X = x;
            Y = y;
        }

        public void SetEmpty() => Update(emptyToken);

        public void SetHead(char headToken)
        {
            Update(headToken);
        }

        public void SetBody(int length)
        {
            Update(bodyToken);
            remaining = length;
        }

        public void SetBorder() => Update(borderToken);

        public void SetFood() => Update(foodToken);

        public void Update(char newVal) => Value = newVal;

        public void Decay()
        {
            if (--remaining == 0)
            {
                SetEmpty();
            }
        }

        public override string ToString() => $"{X}, {Y}";
    }

    public class Snake
    {
        private readonly int maxSpeed = 5;
        private readonly int delayMultiplier = 700;

        public Cell Head { get; private set; }
        public LinkedList<Cell> Body { get; private set; }
        public Cell Tail => Body.Last();
        public Direction Direction { get; private set; }
        public int Length => BodyLength + 1;
        public int BodyLength => Body.Count; // full length includes head
        public int Speed { get; private set; }
        public bool HasEaten { get; private set; }

        public Snake(Cell head, LinkedList<Cell> body, Direction initialHeading, int speed = 1)
        {
            Head = head;
            Body = body;
            Speed = Math.Min(speed, maxSpeed);
            Direction = initialHeading;
            Head.SetHead(Direction.HeadToken);
        }

        public void Move(Direction direction, Cell nextHead)
        {
            var originalHead = Head;

            Direction = direction;

            HasEaten = false; //reset to false on each turn

            //be sure to eat before resetting the head cell to an arrow
            if (nextHead.IsFood)
            {
                Eat();
            }

            Head = nextHead;

            Head.SetHead(direction.HeadToken);

            moveBody(originalHead);

            pause(); //controls speed of play
        }

        public void Eat()
        {
            HasEaten = true;
        }

        public void Grow(Cell newTail)
        {
            newTail.SetBody(1);
            Body.AddLast(newTail);
        }

        private void moveBody(Cell originalHead)
        {
            foreach (var cell in Body)
            {
                cell.Decay(); //handles clearing
            }
            Body.AddFirst(originalHead);
            originalHead.SetBody(BodyLength - 1);
            Body.RemoveLast();
        }

        private void pause() => Thread.Sleep(maxSpeed - Speed + 1 * delayMultiplier);
    }
}
\$\endgroup\$
  • \$\begingroup\$ I dont understand why 'static' and 'public' are always a big nono, whats bad with them? \$\endgroup\$ – Terradice Jan 16 at 19:11
  • \$\begingroup\$ Under the principle of encapsulation, public should be used to expose things that it makes sense for other objects to see and/or use. Everything else should be private within the class itself or protected within the class and its descendants (classes that inherit from it). Static is a different story. It exposes things globally, which is contrary to encapsulation. Furthermore, excessive use of static makes your program procedural instead of being a collection of objects working together. \$\endgroup\$ – Aron Jan 16 at 20:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.