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? state)
{
Tick();
GameUpdate?.Invoke(this, EventArgs.Empty);
}
#endregion
#region Methods
private 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 _disposed_value;
protected virtual void Dispose(bool disposing)
{
if (!_disposed_value)
{
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
_disposed_value = 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
}