I would like to know if there is anything that I missed following the GRASP patterns.
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
};
}
}
SnakeGame
createsApple
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\$