using System.Collections.ObjectModel; using System.Diagnostics; namespace Snake.Model; public class SnakeLevel : IDisposable{ #region Enums /// /// States that a 'block' can be in /// public enum LevelBlock { Empty, Obstacle, Egg, Snake, SnakeHead, OutOfBounds } /// /// The directions that the snake can be headed /// public enum SnakeDirection { Up, Down, Left, Right } /// /// States that the game can be in /// public enum GameState { Running, Paused, Dead } #endregion #region Fields public static readonly TimeSpan TickTimeSpan = TimeSpan.FromMilliseconds(500); // Time between two ingame ticks private readonly List _obstacles; // List of obstacles on the map private readonly List _snake; // Coordinates of the snake's body but not the head private readonly List _eggs; // Coordinates of the current eggs private int _round; // Countdown to till the new egg private ITimer timer; // The timer object private GameState _state; // The current game state private SnakeDirection _last_direction = SnakeDirection.Down; // The direction the snake moved last round private SnakeDirection _snake_head_direction; // Current direction of the snake internal bool _disable_snake_movement = false; // Snake movement can be disabled for testing internal bool _disable_egg_generation = false; // Egg generation can be turned off for testing #endregion #region Properties public int Size { get; } // Size of the map (The map is square) public int NewEggRound { get; } // How many rounds between new egg generation public ReadOnlyCollection Obstacles => _obstacles.AsReadOnly(); public int EggLimit { get; } // Limit of how many eggs can be on the map public Point SnakeHead { get; private set; } // Coordinates of the head of the snake // Head is not in snake // First is closest to the head public ReadOnlyCollection Snake => _snake.AsReadOnly(); public int SnakeLength => Snake.Count + 1; // Current length of the snake public ReadOnlyCollection Eggs => _eggs.AsReadOnly(); public int Score { get; private set; } // Current score public GameState State { get => _state; private set { _state = value; if (_state == GameState.Dead) { timer.Change(Timeout.InfiniteTimeSpan, TickTimeSpan); } GameStateUpdate?.Invoke(this, _state); } } public SnakeDirection SnakeHeadDirection { get => _snake_head_direction; set { switch (value, _last_direction) { case { value: SnakeDirection.Up or SnakeDirection.Down, _last_direction: SnakeDirection.Left or SnakeDirection.Right }: case { value: SnakeDirection.Left or SnakeDirection.Right, _last_direction: SnakeDirection.Up or SnakeDirection.Down }: _snake_head_direction = value; break; } } } public LevelBlock this[Point p] { get { if (_obstacles.Contains(p)) { return LevelBlock.Obstacle; } else if (p == SnakeHead) { return LevelBlock.SnakeHead; } else if (_snake.Contains(p)) { return LevelBlock.Snake; } else if (_eggs.Contains(p)) { return LevelBlock.Egg; } else if (p.X < 0 || p.Y < 0 || p.X >= Size || p.Y >= Size) { return LevelBlock.OutOfBounds; } return LevelBlock.Empty; } } #endregion #region Events public event EventHandler? GameUpdate; // The map was updated public event EventHandler? GameStateUpdate; // The game state was updated #endregion #region Constructors public SnakeLevel(int size, IEnumerable obstacles, int snake_start_length, int new_egg_round, int egg_limit, TimeProvider time_provider){ this.Size = size; this.NewEggRound = new_egg_round; this._round = NewEggRound; this.EggLimit = egg_limit; _obstacles = new List(obstacles); _eggs = []; _snake = []; SnakeHead = (size / 2, snake_start_length-1); for(int i= snake_start_length - 2; i>=0; i--){ _snake.Add((size / 2, i)); } _snake_head_direction = SnakeDirection.Down; _last_direction = SnakeDirection.Down; timer = time_provider.CreateTimer(new TimerCallback(_timer_Tick), null, Timeout.InfiniteTimeSpan, TickTimeSpan); _state = GameState.Paused; } public SnakeLevel(int size, IEnumerable obstacles, int snake_start_length, int new_egg_round, int egg_limit) : this(size, obstacles, snake_start_length, new_egg_round, egg_limit, TimeProvider.System) { } #endregion #region Event handlers private void _timer_Tick(object? sender) { Tick(); GameUpdate?.Invoke(this, EventArgs.Empty); } #endregion #region Methods public void Tick(){ if (!_disable_snake_movement) { Point new_snake_head = (0, 0); switch (SnakeHeadDirection) { case SnakeDirection.Up: new_snake_head = (SnakeHead.X, SnakeHead.Y - 1); break; case SnakeDirection.Down: new_snake_head = (SnakeHead.X, SnakeHead.Y + 1); break; case SnakeDirection.Left: new_snake_head = (SnakeHead.X - 1, SnakeHead.Y); break; case SnakeDirection.Right: new_snake_head = (SnakeHead.X + 1, SnakeHead.Y); break; } if (this[new_snake_head] is LevelBlock.Obstacle or LevelBlock.Snake or LevelBlock.OutOfBounds) { State = GameState.Dead; return; } Point last = _snake.Last(); for (int i = Snake.Count - 1; i > 0; i--) { _snake[i] = _snake[i - 1]; } _snake[0] = SnakeHead; if (this[new_snake_head] is LevelBlock.Egg) { _snake.Add(last); _eggs.Remove(new_snake_head); Score++; } SnakeHead = new_snake_head; } if (!_disable_egg_generation) { if (Eggs.Count < EggLimit && --_round == 0) { _round = NewEggRound; int avail_pos = Size * Size - Eggs.Count - Obstacles.Count - SnakeLength; if (avail_pos > 0) { int pos = Random.Shared.Next(0, avail_pos); Point p = (-1, 0); for (int i = 0; i < pos + 1; i++) { do { p = (p.X + 1, p.Y); if (p.X >= Size) { p = (0, p.Y + 1); } } while (this[p] is not LevelBlock.Empty) ; } _eggs.Add(p); } } } _last_direction = SnakeHeadDirection; } public void Start() { if(State == GameState.Paused) { State = GameState.Running; timer.Change(TickTimeSpan, TickTimeSpan); } else { throw new Exception("Game is not paused"); } } public void Pause() { if(State == GameState.Running) { State = GameState.Paused; timer.Change(Timeout.InfiniteTimeSpan, TickTimeSpan); } else { throw new Exception("Game is not running"); } } #endregion #region Testing internal void AddEgg(Point p) { _eggs.Add(p); } #endregion #region IDisposable private bool disposedValue; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects) timer.Dispose(); } // TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null disposedValue = true; } } // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources // ~SnakeLevel() // { // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method // Dispose(disposing: false); // } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } #endregion }