256 lines
7.3 KiB
C#
256 lines
7.3 KiB
C#
using model;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
|
|
namespace Snake.Model;
|
|
|
|
public class SnakeLevel : IDisposable{
|
|
public static readonly TimeSpan TickTimeSpan = TimeSpan.FromMilliseconds(500);
|
|
public int Size {get; }
|
|
|
|
public enum LevelBlock{
|
|
Empty,
|
|
Obstacle,
|
|
Egg,
|
|
Snake,
|
|
SnakeHead,
|
|
OutOfBounds
|
|
}
|
|
|
|
public enum SnakeDirection{
|
|
Up,
|
|
Down,
|
|
Left,
|
|
Right
|
|
}
|
|
|
|
public enum GameState{
|
|
Running,
|
|
Paused,
|
|
Dead
|
|
}
|
|
|
|
private readonly List<Point> _obstacles;
|
|
public ReadOnlyCollection<Point> Obstacles => _obstacles.AsReadOnly();
|
|
public Point SnakeHead {get; private set;}
|
|
private readonly List<Point> _snake;
|
|
// Head is not in snake
|
|
// First is closest to the head
|
|
public ReadOnlyCollection<Point> Snake => _snake.AsReadOnly();
|
|
public int SnakeLength => Snake.Count + 1;
|
|
private readonly List<Point> _eggs;
|
|
public ReadOnlyCollection<Point> Eggs => _eggs.AsReadOnly();
|
|
public int NewEggRound { get; }
|
|
public int EggLimit { get; }
|
|
private int _round;
|
|
private ITimer timer;
|
|
public int Score { get; private set; }
|
|
|
|
public event EventHandler? GameUpdate;
|
|
public event EventHandler<GameState>? GameStateUpdate;
|
|
private GameState _state;
|
|
public GameState State
|
|
{
|
|
get => _state;
|
|
private set
|
|
{
|
|
_state = value;
|
|
GameStateUpdate?.Invoke(this, _state);
|
|
if(_state == GameState.Dead)
|
|
{
|
|
timer.Change(Timeout.InfiniteTimeSpan, TickTimeSpan);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private SnakeDirection LastDirection = SnakeDirection.Down;
|
|
private SnakeDirection _SnakeHeadDirection;
|
|
private bool disposedValue;
|
|
|
|
public SnakeDirection SnakeHeadDirection
|
|
{
|
|
get => _SnakeHeadDirection;
|
|
set
|
|
{
|
|
switch (value, LastDirection)
|
|
{
|
|
case { value: SnakeDirection.Up or SnakeDirection.Down, LastDirection: SnakeDirection.Left or SnakeDirection.Right}:
|
|
case { value: SnakeDirection.Left or SnakeDirection.Right, LastDirection: SnakeDirection.Up or SnakeDirection.Down }:
|
|
_SnakeHeadDirection = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public SnakeLevel(int size, IEnumerable<Point> 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<Point>(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));
|
|
}
|
|
|
|
_SnakeHeadDirection = SnakeDirection.Down;
|
|
LastDirection = SnakeDirection.Down;
|
|
|
|
timer = time_provider.CreateTimer(new TimerCallback(_timer_Tick), null, Timeout.InfiniteTimeSpan, TickTimeSpan);
|
|
|
|
_state = GameState.Paused;
|
|
}
|
|
|
|
private void _timer_Tick(object? sender)
|
|
{
|
|
Tick();
|
|
GameUpdate?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
public SnakeLevel(int size, IEnumerable<Point> 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)
|
|
{
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
public void Tick(){
|
|
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 (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 = (0, 0);
|
|
for (int i = 0; i < pos; 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);
|
|
}
|
|
}
|
|
|
|
LastDirection = 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");
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
} |