8
\$\begingroup\$

I would like to know if there is anything that I missed following the GRASP patterns.

UML Class Diagram

The Main Controller class:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;

    namespace ConsoleApplication1
    {
        public class SnakeGame
        {
            private bool GameIsPlaying { get; set; }
            private bool AppleEaten { get; set; }
            private bool Paused { get; set; }

            public List<Coord> TailCoordsList;
            private PlayField PlayField { get; set; }
            private Snake Player { get; set; }
            private Stopwatch Tick { get; set; }
            private GameRenderer Renderer { get; set; }
            private Direction Direction { get; set; }
            private Apple Apple { get; set; }
            private Input Input { get; set; }


            private void Initiliaze()
            {
                GameIsPlaying = true;
                Paused = false;
                AppleEaten = false;

                TailCoordsList = new List<Coord>();

                Apple = new Apple();
                PlayField = new PlayField(TailCoordsList);
                Apple.SetAppleOnField(TailCoordsList, PlayField.Width, PlayField.Height);

                Tick  = new Stopwatch();
                Renderer = new GameRenderer();

                Player = new Snake(TailCoordsList);
                Direction = new Direction();

                Tick.Start();
                UpdatePlayField();
            }

            private void UpdatePlayField()
            {
                while (GameIsPlaying)
                {
                    OnKeyDown(); // Input logic

                    if (!Paused) // Game logic
                    {
                        if (Tick.ElapsedMilliseconds < 100) continue; // Tick ensures that each update will happen in constant time.
                        Tick.Restart();

                        Player.GetNextPosition(TailCoordsList);
                        Player.SwitchDirection(Direction.NewDirection);

                        GameIsPlaying = Player.BorderCollisionCheck(PlayField.Width, PlayField.Height);
                        if (!GameIsPlaying) continue;

                        AppleEaten = Player.AppleCollisionCheck(PlayField.Width, PlayField.Height, Apple.AppleCoord, Apple, TailCoordsList, PlayField);

                        GameIsPlaying = Player.SelfCollisionTest(TailCoordsList, AppleEaten);
                        if (!GameIsPlaying) continue;

                        Renderer.DrawCharAtLoc(Player.HeadCoord, '0');

                        if (!AppleEaten)
                            Renderer.DrawCharAtLoc(Player.TailEndCoord, ' ');
                        else
                            AppleEaten = true;

                        Player.ExtendSnakeTail(TailCoordsList);
                        Renderer.DrawCharAtLoc(Player.NewHeadCoord, '@');
                        Direction.LastDirection = Direction.NewDirection;

                    }
                }
            }

            private void OnKeyDown()
            {
                Input = new Input();
                if (!Input.hasInput) return;
                if(!Input.Quit())
                    GameIsPlaying = Input.Quit();
                else if (Input.Pause())
                    Paused = !Paused;
                else if (Input.Up(Direction) == Direction.Dir.Up)
                    Direction.NewDirection = Input.Up(Direction);
                else if (Input.Down(Direction) == Direction.Dir.Down)
                    Direction.NewDirection = Input.Down(Direction);
                else if (Input.Left(Direction) == Direction.Dir.Left)
                    Direction.NewDirection = Input.Left(Direction);
                else if (Input.Right(Direction) == Direction.Dir.Right)
                    Direction.NewDirection = Input.Right(Direction);    
            }

            public static void Main()
            {
                var game = new SnakeGame();
                game.Initiliaze();
            }
        }
    }

The player class, containing player logic:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1
{
    public class Snake : SnakeGame
    {       
        public Coord TailEndCoord { get; set; }
        public Coord HeadCoord { get; set; }
        public Coord NewHeadCoord { get; set; }

        public Snake(List<Coord> tailCoords)
        {
            FillTail(tailCoords);   
        }

        protected Snake()
        {
        }

        public void FillTail(List<Coord> tailCoords)
        {
            tailCoords.Add(new Coord(10, 10));
            tailCoords.Add(new Coord(10, 10));
            tailCoords.Add(new Coord(10, 10));
            tailCoords.Add(new Coord(10, 10));
        }

        public void GetNextPosition(List<Coord> tailCoords)
        {
            TailEndCoord = new Coord(tailCoords.First());
            HeadCoord = new Coord(tailCoords.Last());
            NewHeadCoord = new Coord(HeadCoord);

        }

        public void SwitchDirection(Direction.Dir newDir)
        {
            switch (newDir)
            {
                case Direction.Dir.Up:
                    NewHeadCoord.Y--;
                    break;
                case Direction.Dir.Right:
                    NewHeadCoord.X++;
                    break;
                case Direction.Dir.Down:
                    NewHeadCoord.Y++;
                    break;
                default:
                    NewHeadCoord.X--;
                    break;
            }
        }

        public bool BorderCollisionCheck(int width, int height)
        {
            if (NewHeadCoord.X < 0 || NewHeadCoord.X >= width)
                return false;
            if (NewHeadCoord.Y < 0 || NewHeadCoord.Y >= height)
                return false;
            return true;
        }

        public bool AppleCollisionCheck(int width, int height, Coord appleCoord, Apple apple, List<Coord> tailCoords, PlayField playField)
        {
            if (NewHeadCoord.X == appleCoord.X && NewHeadCoord.Y == appleCoord.Y)
            {
                if (tailCoords.Count + 1 >= width*height)
                    // No more room to place apples -- game over.
                    Environment.Exit(0);
                else
                {
                    return apple.SetAppleOnField(tailCoords, playField.Width, playField.Height);
                }
            }
            return false;
        }

        // Death by accidental self-cannibalism.
        public bool SelfCollisionTest(List<Coord> tailCoords, bool appleEaten)
        {
            if (!appleEaten)
            {
                tailCoords.RemoveAt(0);
                foreach (Coord x in tailCoords)
                    if (x.X == NewHeadCoord.X && x.Y == NewHeadCoord.Y)
                    {
                        // Death by accidental self-cannibalism.
                        return false;
                    }
            }
            return true;
        }


        public void ExtendSnakeTail(List<Coord> tailCoords)
        {
            tailCoords.Add(new Coord(NewHeadCoord));
        }
    }
}

The GameBoard / PlayField class containing information about the board:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1
{
    public class PlayField : SnakeGame
    {
        public int Width { get; set; }
        public int Height { get; set; }

        private readonly FieldRenderer _renderer = new FieldRenderer();
        private readonly Random _random = new Random();

        public PlayField(List<Coord> tailCoords)
        {
            SetSnakeField(tailCoords);
        }

        protected PlayField(){}

        public void SetSnakeField(List<Coord> tailCoords)
        {
            Width = Console.WindowWidth;
            Height = Console.WindowHeight;

            _renderer.InitConsole();
        }
    }
}

The apple class containing the logic behind spawing apples:

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

namespace ConsoleApplication1
{
    public class Apple : PlayField
    {
        public Coord AppleCoord { get; set; }
        private readonly Random _random = new Random();
        private readonly FieldRenderer _renderer = new FieldRenderer();

        public Apple()
        {
            AppleCoord = new Coord();
        }

        public bool SetAppleOnField(List<Coord> tailCoords, int width, int height)
        {
            while (true)
            {
                AppleCoord.X = _random.Next(0, width);
                AppleCoord.Y = _random.Next(0, height);

                var freeSpot = tailCoords.All(i => i.X != AppleCoord.X || i.Y != AppleCoord.Y);
                if (!freeSpot) continue;

                _renderer.DrawCharAtLoc(AppleCoord, '$');

                return true;
            }
        }
    }
}

Input Class containing input logic:

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

namespace ConsoleApplication1
{
    class Input : SnakeGame
    {
        public ConsoleKeyInfo KeyInfo;
        public bool hasInput { get; set; }
        public Input()
        {
            if (KeyAvalible())
            {
                hasInput = false;
                return;
            }
            hasInput = true;
            KeyInfo = Console.ReadKey(true);                
        }


        public bool KeyAvalible()
        {
            return !Console.KeyAvailable;

        }

        public bool Pause()
        {
            return KeyInfo.Key == ConsoleKey.Spacebar;
        }

        public bool Quit()
        {
            return KeyInfo.Key != ConsoleKey.Escape;
        }

        public Direction.Dir Up(Direction direction)
        {
            if (KeyInfo.Key == ConsoleKey.UpArrow && direction.LastDirection != Direction.Dir.Down)
                return Direction.Dir.Up;
            return Direction.Dir.Down;
        }

        public Direction.Dir Down(Direction direction)
        {
            if (KeyInfo.Key == ConsoleKey.DownArrow && direction.LastDirection != Direction.Dir.Up)
                return Direction.Dir.Down;
            return Direction.Dir.Up;
        }

        public Direction.Dir Right(Direction direction)
        {
            if (KeyInfo.Key == ConsoleKey.RightArrow && direction.LastDirection != Direction.Dir.Left)
                return Direction.Dir.Right;
            return Direction.Dir.Left;
        }

        public Direction.Dir Left(Direction direction)
        {
            if (KeyInfo.Key == ConsoleKey.LeftArrow && direction.LastDirection != Direction.Dir.Right)
                return Direction.Dir.Left;
            return Direction.Dir.Right;
        }
    }
}

The Coord class holding information about the location of objects on the field:

namespace ConsoleApplication1
{
    public class Coord
    {
        public int X { get; set; }
        public int Y { get; set; }

        public Coord()
        {
            X = 0;
            Y = 0;
        }

        public Coord(Coord input)
       {
            X = input.X;
            Y = input.Y;
        }

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

The rendering classes made like this because I wanted to use abstract classes, but found no other place to use it:

using System;

namespace ConsoleApplication1
{
    public abstract class Renderer : SnakeGame
    { 
        public abstract void DrawCharAtLoc(Coord coord, char c);
    }
}

namespace ConsoleApplication1
{
    public class GameRenderer : Renderer
    {
        public override void DrawCharAtLoc(Coord coord, char c)
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.SetCursorPosition(coord.X, coord.Y);
            Console.Write(c);
        }
    }
}



namespace ConsoleApplication1
{
    public class FieldRenderer : Renderer
    {
        public override void DrawCharAtLoc(Coord coord, char c)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.SetCursorPosition(coord.X, coord.Y);
            Console.Write(c);
        }

        public void InitConsole()
        {
            Console.CursorVisible = false;
            Console.Title = "Westerdals Oslo ACT - SNAKE";
            Console.ForegroundColor = ConsoleColor.Green;
            Console.SetCursorPosition(10, 10);
            Console.Write("@");
        }
    }
}

Direction class containing enum for which direction the snake is going:

using System;

namespace ConsoleApplication1
{
    public class Direction : Snake
    {
        public Dir NewDirection { get; set; }
        public Dir LastDirection { get; set; }

        public Direction()
        {
            NewDirection = Dir.Down;
            LastDirection = NewDirection;
        }

        public enum Dir
        {
            Up,
            Right,
            Down,
            Left
        };

    }
}
\$\endgroup\$
  • \$\begingroup\$ You mentioned GRASP but I don't see a domain model (only a design model). For questions like "who creates?" GRASP Creator tries to leverage coupling found in the domain model. In your code, SnakeGame creates Apple instances, so according to Creator, the first should aggregate the second (or some other variant). A domain model helps for determining responsibilities in GRASP. \$\endgroup\$ – Fuhrmanator Oct 28 '15 at 16:44
4
\$\begingroup\$

Sort/remove usings

it's not a huge deal, just adds to noise to the source, but make it a habbit to remove unused usings in your source code. I prefer to use Producitiviy tools (a free extension for VS) you can right click on the solution and remove and sort all unused usings.

Apple.cs

I feel like your code is doing some strange things. Apple extends PlayField which extends SnakeGame. Why? I commented out the extends PlayField part and the game worked just fine. You create a new instance of Random and FieldRenderer in the file, but initialize AppleCoord in the constructor? It would be better to stay consistant and initialize all 3 in the constructor, however Random is typically best if you make it a static field. FieldRenderer is a concrete implementation of Renderer, but you only use the abstract method, instead pass in your Renderer in the constructor instead of initializing it. AppleCoord is a bad name, and should be spelled out. Also you allow it to be set! It would be better to make it readonly and make a public getter for collision detection later. Lastly you pass in a List<Coord> it would be better if you just passed in IEnumerable. Remember to favor depending on the abstract (or even bettter the interface). Why does it return bool? You don't use the value. have it return void instead.

public class Apple
{
    private static readonly Random _random = new Random();
    public Coord AppleCoordinate { get { return _appleCoordinate; } }

    private readonly Renderer _renderer;
    private readonly Coord _appleCoordinate;
    private readonly int _playFieldWidth;
    private readonly int _playFieldHeight;

    public Apple(Renderer renderer, PlayField playField)
    {
        _renderer = renderer;
        _appleCoordinate = new Coord();
        _playFieldWidth = playField.Width;
        _playFieldHeight = playField.Height;
    }

    public void SetAppleOnField(IEnumerable<Coord> tailCoords)
    {
        var isValidAppleCoordinate = false;
        while (!isValidAppleCoordinate)
        {
            _appleCoordinate.X = _random.Next(0, _playFieldWidth);
            _appleCoordinate.Y = _random.Next(0, _playFieldHeight);

            isValidAppleCoordinate = tailCoords.All(i => i.X != _appleCoordinate.X || i.Y != _appleCoordinate.Y);
        }
        _renderer.DrawCharAtLoc(_appleCoordinate, '$');
    }
}

Renderers

Renderer does not need to extend SnakeGame (same reason as Apple). There is some duplicate code between you different renderers so make a protected method that you renderes call. One thing that baffles me is the FieldRenderer::InitConsole. Why is it in FieldRenderer? That violates SRP. Move it somewhere else.. like SnakeGame. That would make more sense. DrawCharAtLoc is a bad name for a method. Since your Renderer "Renders" maybe it would be better called Render.

public abstract class Renderer
{
    public abstract void Render(Coord coord, char c);

    protected void Draw(Coord coord, char c, ConsoleColor color)
    {
        Console.ForegroundColor = color;
        Console.SetCursorPosition(coord.X, coord.Y);
        Console.Write(c);
    }
}

public class GameRenderer : Renderer
{
    public override void Render(Coord coord, char c)
    {
        Draw(coord, c, ConsoleColor.Yellow);
    }
}

public class FieldRenderer : Renderer
{
    public override void Render(Coord coord, char c)
    {
        Draw(coord, c, ConsoleColor.Green);
    }
}

Direction.cs & Input.cs

Why does Direction extend Snake? Again I commented it out and the game works just fine. These two classes are tightly coupled. That might not be a bad thing, but I would rethink this. Take a step back and think: "What goes in a direction? A snake. Who should tell the snake what direction to go? The input. What if the snake can't do what was input? I would say Ignore the input" with that in mind. Lets just check if there is input, and if there is lets suggest to the snake what direction to go. Then we can remove the dependency on Direction in the Input class and it can stand on its own.

internal class Input
{
    private readonly ConsoleKeyInfo _keyInfo;
    public bool hasInput { get; set; }

    public Input()
    {
        _keyInfo = Console.ReadKey(true);
    }

    public bool KeyAvalible { get { return !Console.KeyAvailable; } }

    public bool IsPause { get { return _keyInfo.Key == ConsoleKey.Spacebar; } }

    public bool IsQuit { get { return _keyInfo.Key == ConsoleKey.Escape; } }

    public bool IsUp { get { return _keyInfo.Key == ConsoleKey.UpArrow; } }

    public bool IsDown { get { return _keyInfo.Key == ConsoleKey.DownArrow; } }

    public bool IsRight { get { return _keyInfo.Key == ConsoleKey.RightArrow; } }

    public bool IsLeft { get { return _keyInfo.Key == ConsoleKey.LeftArrow; } }
}

//SnakeGame.cs
private void OnKeyDown()
{
    input = new Input();
    if (!input.HasInput) return;
    if (input.IsQuit)
        GameIsPlaying = false;
    else if (input.IsPause)
        Paused = !Paused;
    else if (input.IsUp)
        _snake.SuggestGoingUp();
    else if (input.IsDown)
        _snake.SuggestGoingDown();
    else if (input.IsLeft)
        _snake.SuggestGoingLeft();
    else if (input.IsRight)
        _snake.SuggestGoingRight();
}

Well this is getting lengthy so I'm going to wrap up with that you are on the rigth course. I believe if you start also writing some unit tests to create a safety net for you to fall back on, then start relentlessly refactoring and removing code that isn't needed your code will start to take better shape.

| improve this answer | |
\$\endgroup\$

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.