diff --git a/model/Point.cs b/model/Point.cs
index e19705e..8b25d09 100644
--- a/model/Point.cs
+++ b/model/Point.cs
@@ -2,7 +2,7 @@ using System.Numerics;
namespace Snake.Model;
-// 0,0 is top left
+// Struct to store a point. A bit functional looking
public readonly record struct Point{
public readonly int X;
public readonly int Y;
@@ -12,6 +12,7 @@ public readonly record struct Point{
Y = y;
}
+ // This is where the magic happens
public static implicit operator Point((int x, int y) tuple){
return new Point(tuple.x, tuple.y);
}
diff --git a/model/SnakeLevel.cs b/model/SnakeLevel.cs
index d74a8a9..9a91f2b 100644
--- a/model/SnakeLevel.cs
+++ b/model/SnakeLevel.cs
@@ -1,14 +1,15 @@
-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{
+ #region Enums
+ ///
+ /// States that a 'block' can be in
+ ///
+ public enum LevelBlock
+ {
Empty,
Obstacle,
Egg,
@@ -17,72 +18,121 @@ public class SnakeLevel : IDisposable{
OutOfBounds
}
- public enum SnakeDirection{
+ ///
+ /// The directions that the snake can be headed
+ ///
+ public enum SnakeDirection
+ {
Up,
Down,
Left,
Right
}
- public enum GameState{
+ ///
+ /// States that the game can be in
+ ///
+ public enum GameState
+ {
Running,
Paused,
Dead
}
+ #endregion
- private readonly List _obstacles;
+ #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 Point SnakeHead {get; private set;}
- private readonly List _snake;
+ 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;
- private readonly List _eggs;
+ public int SnakeLength => Snake.Count + 1; // Current length of the snake
public ReadOnlyCollection 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? GameStateUpdate;
- private GameState _state;
+ public int Score { get; private set; } // Current score
+
public GameState State
{
get => _state;
private set
{
_state = value;
- GameStateUpdate?.Invoke(this, _state);
- if(_state == GameState.Dead)
+ if (_state == GameState.Dead)
{
timer.Change(Timeout.InfiniteTimeSpan, TickTimeSpan);
}
+ GameStateUpdate?.Invoke(this, _state);
}
}
-
- private SnakeDirection LastDirection = SnakeDirection.Down;
- private SnakeDirection _SnakeHeadDirection;
- private bool disposedValue;
-
public SnakeDirection SnakeHeadDirection
{
- get => _SnakeHeadDirection;
+ get => _snake_head_direction;
set
{
- switch (value, LastDirection)
+ switch (value, _last_direction)
{
- 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;
+ 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;
@@ -98,104 +148,100 @@ public class SnakeLevel : IDisposable{
_snake.Add((size / 2, i));
}
- _SnakeHeadDirection = SnakeDirection.Down;
- LastDirection = SnakeDirection.Down;
+ _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
- 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)
- {
- }
-
- 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;
- }
- }
-
+ #region Methods
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--)
+ if (!_disable_snake_movement)
{
- _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)
+ Point new_snake_head = (0, 0);
+ switch (SnakeHeadDirection)
{
- int pos = Random.Shared.Next(0, avail_pos);
- Point p = (0, 0);
- for (int i = 0; i < pos; i++)
+ 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)
{
- do
+ int pos = Random.Shared.Next(0, avail_pos);
+ Point p = (-1, 0);
+ for (int i = 0; i < pos + 1; i++)
{
- p = (p.X + 1, p.Y);
- if (p.X >= Size)
+ do
{
- p = (0, p.Y + 1);
- }
- } while (this[p] is not LevelBlock.Empty);
+ 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);
}
- _eggs.Add(p);
}
}
- LastDirection = SnakeHeadDirection;
+ _last_direction = SnakeHeadDirection;
}
public void Start()
@@ -223,7 +269,17 @@ public class SnakeLevel : IDisposable{
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)
@@ -253,4 +309,5 @@ public class SnakeLevel : IDisposable{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
+ #endregion
}
\ No newline at end of file
diff --git a/model/Timer.cs b/model/Timer.cs
deleted file mode 100644
index 662cedf..0000000
--- a/model/Timer.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace model
-{
- /*internal class Timer : IDisposable
- {
- private PeriodicTimer _timer;
- public event EventHandler? Tick;
- public Timer(TimeSpan tick, TimeProvider _time_provider)
- {
- _timer = new PeriodicTimer(tick, _time_provider);
- }
-
- private CancellationTokenSource? _cts;
- private bool disposedValue;
-
- private async Task TickerTask()
- {
- _cts = new CancellationTokenSource();
- while (await _timer.WaitForNextTickAsync(_cts.Token))
- {
- Tick?.Invoke(this, EventArgs.Empty);
- }
- }
-
- public void Start()
- {
- Task.Run(TickerTask);
- }
-
- public void Stop()
- {
- _cts?.Cancel();
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (!disposedValue)
- {
- if (disposing)
- {
- // TODO: dispose managed state (managed objects)
- _cts?.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
- // ~Timer()
- // {
- // // 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);
- }
- }*/
-}
diff --git a/model/model.csproj b/model/model.csproj
index dcaa502..08a8fc9 100644
--- a/model/model.csproj
+++ b/model/model.csproj
@@ -7,6 +7,9 @@
+
+ <_Parameter1>tests
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/persistance/SnakeLevelLoader.cs b/persistance/SnakeLevelLoader.cs
index d00b9b0..b90bf9d 100644
--- a/persistance/SnakeLevelLoader.cs
+++ b/persistance/SnakeLevelLoader.cs
@@ -12,13 +12,20 @@ public class SnakeLevelLoader{
if(!File.Exists(SaveFileName)){
throw new FileNotFoundException("Save file not found", SaveFileName);
}
- string file_content = File.ReadAllText(SaveFileName);
- _stored_levels = JsonSerializer.Deserialize>(file_content, new JsonSerializerOptions(){PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower})
- ?? throw new JsonException("Failed to deserialize stored levels");
}
- private List _stored_levels;
- public ReadOnlyCollection StoredLevels => _stored_levels.AsReadOnly();
+ public async Task ReadAllDataAsync()
+ {
+ string file_content = await File.ReadAllTextAsync(SaveFileName);
+ _stored_levels = JsonSerializer.Deserialize>(file_content, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower })
+ ?? throw new JsonException("Failed to deserialize stored levels");
+
+ await Task.Delay(2000);
+ }
+
+ private List? _stored_levels;
+ public ReadOnlyCollection StoredLevels => _stored_levels?.AsReadOnly()
+ ?? throw new InvalidOperationException("Can not query stored levels before loading the data");
public SnakeLevel LoadLevel(StoredSnakeLevel level){
return new SnakeLevel(level.Size,
diff --git a/tests/Model/SnakeLevelTest.cs b/tests/Model/SnakeLevelTest.cs
index 07823b5..d143eb4 100644
--- a/tests/Model/SnakeLevelTest.cs
+++ b/tests/Model/SnakeLevelTest.cs
@@ -1,9 +1,4 @@
using Snake.Model;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
@@ -11,25 +6,39 @@ namespace tests.Model
{
public class SnakeLevelTest
{
+ #region Level constants
+ private const int Size = 10;
+ private const int SnakeStartLength = 5;
+ private const int NewEggRound = 3;
+ private const int EggLimit = 2;
+ Point[] obstacles = [(0, 4), (3, 3)];
+ #endregion
+
+ #region Test objects
FakeTimeProvider timeProvider = new FakeTimeProvider();
SnakeLevel level;
- Point[] obstacles = [(3, 3), (4, 4)];
+ #endregion
+
+ #region Constructor and initializazion
public SnakeLevelTest()
{
level = new SnakeLevel(
- 10, obstacles,
- 5, 3, 2, timeProvider
+ size: Size, obstacles: obstacles,
+ snake_start_length: SnakeStartLength, new_egg_round: NewEggRound,
+ egg_limit: EggLimit, time_provider: timeProvider
);
}
+ #endregion
+ #region Tests
[Fact]
public void SnakeInitialValuesTests()
{
level.SnakeHeadDirection.Should().Be(SnakeLevel.SnakeDirection.Down);
- level.SnakeLength.Should().Be(5);
- level.EggLimit.Should().Be(2);
- level.NewEggRound.Should().Be(3);
- level.Size.Should().Be(10);
+ level.SnakeLength.Should().Be(SnakeStartLength);
+ level.EggLimit.Should().Be(EggLimit);
+ level.NewEggRound.Should().Be(NewEggRound);
+ level.Size.Should().Be(Size);
level.State.Should().Be(SnakeLevel.GameState.Paused);
foreach(Point p in obstacles)
{
@@ -40,6 +49,7 @@ namespace tests.Model
[Fact]
public void SnakeOutOfBoundsTest()
{
+ level._disable_egg_generation = true;
bool game_state_changed_raised = false;
level.GameStateUpdate += (s, e) =>
{
@@ -50,7 +60,7 @@ namespace tests.Model
game_state_changed_raised.Should().Be(true);
game_state_changed_raised = false;
- for(int i=0; i<5; i++)
+ for(int i=0; i
+ {
+ game_state_changed_raised = true;
+ };
+
+ level.Start();
+ game_state_changed_raised.Should().Be(true);
+ game_state_changed_raised = false;
+ level.SnakeHeadDirection = SnakeLevel.SnakeDirection.Left;
+ for(int i=0; i < Size/2 - 1; i++)
+ {
+ timeProvider.Advance(SnakeLevel.TickTimeSpan);
+ }
+
+ game_state_changed_raised.Should().Be(false);
+ timeProvider.Advance(SnakeLevel.TickTimeSpan);
+ game_state_changed_raised.Should().Be(true);
+ level.State.Should().Be(SnakeLevel.GameState.Dead);
+ }
+
+ [Fact]
+ public void EggPickupTest()
+ {
+ // check test constants
+ obstacles.Should().NotContain((Size / 2, SnakeStartLength), "There must be no eggs upto two blocks below the snake for egg pickup test");
+ obstacles.Should().NotContain((Size / 2, SnakeStartLength + 1), "There must be no eggs upto two blocks below the snake for egg pickup test");
+
+ level._disable_egg_generation = true;
+ Point test_egg = (Size / 2, SnakeStartLength + 1);
+ level.AddEgg(test_egg);
+ level.Start();
+ timeProvider.Advance(SnakeLevel.TickTimeSpan);
+ level.Eggs.Should().Contain(test_egg);
+ level.SnakeLength.Should().Be(SnakeStartLength);
+ timeProvider.Advance(SnakeLevel.TickTimeSpan);
+ level.Eggs.Should().NotContain(test_egg);
+ level.Score.Should().Be(1);
+ level.SnakeLength.Should().Be(SnakeStartLength + 1);
+ }
+
+ [Fact]
+ public void EggGenerationTest()
+ {
+ level._disable_snake_movement = true;
+ level.Start();
+ for(int i=0; i
+ /// Draws the current map to the canvas
+ ///
+ private void Draw()
{
if (CurrentLevel == null) return;
- switch (e.KeyCode)
- {
- case Keys.Up:
- CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Up;
- break;
- case Keys.Down:
- CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Down;
- break;
- case Keys.Left:
- CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Left;
- break;
- case Keys.Right:
- CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Right;
- break;
- case Keys.Escape:
- if(CurrentLevel.State == SnakeLevel.GameState.Running)
- CurrentLevel.Pause();
- break;
- }
- }
-
- private void Draw(SnakeLevel level)
- {
- var canvas_size = Math.Min(Canvas.Width, Canvas.Height);
- graphics = Canvas.CreateGraphics();
+ using Graphics canvas_graphics = Canvas.CreateGraphics();
+ using Bitmap bmp = new Bitmap(Canvas.Width, Canvas.Height, canvas_graphics);
+ using Graphics graphics = Graphics.FromImage(bmp);
graphics.Clear(Color.White);
- double cell_size = (double)(canvas_size - (level.Size + 1) * line_width) / level.Size;
+ var canvas_size = Math.Min(Canvas.Width, Canvas.Height);
+
+ double cell_size = (double)(canvas_size - (CurrentLevel.Size + 1) * line_width) / CurrentLevel.Size;
// vertical lines
- for (int i = 0; i <= level.Size; i++)
+ for (int i = 0; i <= CurrentLevel.Size; i++)
{
int x = (int)(i * (cell_size + line_width));
graphics.FillRectangle(Brushes.Black, x, 0, line_width, canvas_size);
}
// horizontal lines
- for (int i = 0; i <= level.Size; i++)
+ for (int i = 0; i <= CurrentLevel.Size; i++)
{
int y = (int)(i * (cell_size + line_width));
graphics.FillRectangle(Brushes.Black, 0, y, canvas_size, line_width);
}
// Ssnake
- foreach (var pos in level.Snake)
+ foreach (var pos in CurrentLevel.Snake)
{
int x = (int)(cell_size * pos.X + line_width * (pos.X + 1));
int y = (int)(cell_size * pos.Y + line_width * (pos.Y + 1));
graphics.FillRectangle(Brushes.Green, x, y, (int)cell_size, (int)cell_size);
}
// snake head
- int snake_head_x = (int)(cell_size * level.SnakeHead.X + line_width * (level.SnakeHead.X + 1));
- int snake_head_y = (int)(cell_size * level.SnakeHead.Y + line_width * (level.SnakeHead.Y + 1));
+ int snake_head_x = (int)(cell_size * CurrentLevel.SnakeHead.X + line_width * (CurrentLevel.SnakeHead.X + 1));
+ int snake_head_y = (int)(cell_size * CurrentLevel.SnakeHead.Y + line_width * (CurrentLevel.SnakeHead.Y + 1));
graphics.FillRectangle(Brushes.Red, snake_head_x, snake_head_y, (int)cell_size, (int)cell_size);
- switch (level.SnakeHeadDirection)
+ switch (CurrentLevel.SnakeHeadDirection)
{
case SnakeLevel.SnakeDirection.Up:
graphics.DrawLine(Pens.Black, snake_head_x + (int)cell_size / 2, snake_head_y, snake_head_x + (int)cell_size / 2, snake_head_y + (int)cell_size);
@@ -99,7 +85,7 @@ namespace view
}
// obstacles
- foreach (var pos in level.Obstacles)
+ foreach (var pos in CurrentLevel.Obstacles)
{
int x = (int)(cell_size * pos.X + line_width * (pos.X + 1));
int y = (int)(cell_size * pos.Y + line_width * (pos.Y + 1));
@@ -107,12 +93,41 @@ namespace view
}
//eggs
- foreach (var pos in level.Eggs)
+ foreach (var pos in CurrentLevel.Eggs)
{
int x = (int)(cell_size * pos.X + line_width * (pos.X + 1));
int y = (int)(cell_size * pos.Y + line_width * (pos.Y + 1));
graphics.FillRectangle(Brushes.Pink, x, y, (int)cell_size, (int)cell_size);
}
+
+ canvas_graphics.DrawImage(bmp, 0, 0);
+ }
+ #endregion
+
+ #region Form event handlers
+ private void Form1_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (CurrentLevel == null) return;
+ switch (e.KeyCode)
+ {
+ case Keys.Up:
+ CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Up;
+ break;
+ case Keys.Down:
+ CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Down;
+ break;
+ case Keys.Left:
+ CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Left;
+ break;
+ case Keys.Right:
+ CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Right;
+ break;
+ case Keys.Escape:
+ if (CurrentLevel.State == SnakeLevel.GameState.Running)
+ CurrentLevel.Pause();
+ break;
+
+ }
}
private void btnStart_Click(object sender, EventArgs e)
@@ -139,31 +154,86 @@ namespace view
}
}
+ private void Form1_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
+ {
+ if (CurrentLevel?.State is SnakeLevel.GameState.Running)
+ {
+ e.IsInputKey = true;
+ }
+ }
+
+ private void Form1_Shown(object sender, EventArgs e)
+ {
+ btnEndGame.Visible = false; // It will trigger a whole repaint the first time it is shown unless this is done
+ level_loader.ReadAllDataAsync().ContinueWith((task) =>
+ {
+ if (!this.IsDisposed) // The form can be closed before the file is loaded
+ {
+ this.Invoke(() =>
+ {
+ if (task.IsCompletedSuccessfully)
+ {
+ lblLoading.Visible = false;
+ btnStart.Enabled = true;
+ btnStart.Focus();
+ }
+ else
+ {
+ MessageBox.Show("Failed to read data");
+ }
+ });
+ }
+ });
+ }
+
+ private void button1_Click(object sender, EventArgs e)
+ {
+ CurrentLevel = null;
+ btnEndGame.Visible = false;
+ Canvas.Refresh();
+ txtScore.Text = "";
+ }
+
+ private void Form1_FormClosing(object sender, FormClosingEventArgs e)
+ {
+ if (CurrentLevel != null)
+ {
+ if (CurrentLevel.State == SnakeLevel.GameState.Running)
+ {
+ CurrentLevel.Pause();
+ }
+ CurrentLevel = null;
+ }
+ }
+
+ #endregion
+
+ #region Game event handlers
private void CurrentLevel_GameUpdate(object? sender, EventArgs e)
{
+ if (CurrentLevel == null) return;
this.Invoke(() =>
{
- if (CurrentLevel != null)
- {
- Draw(CurrentLevel);
- txtScore.Text = CurrentLevel.Score.ToString();
- }
+ Draw();
+ txtScore.Text = CurrentLevel.Score.ToString();
});
}
private void CurrentLevel_GameStateUpdate(object? sender, SnakeLevel.GameState e)
{
+ if (CurrentLevel == null) return;
this.Invoke(() =>
{
- if (CurrentLevel == null) return;
switch (e)
{
case SnakeLevel.GameState.Running:
btnStart.Visible = false;
- Draw(CurrentLevel);
+ btnEndGame.Visible = false;
+ Draw();
break;
case SnakeLevel.GameState.Paused:
btnStart.Visible = true;
+ btnEndGame.Visible = true;
break;
case SnakeLevel.GameState.Dead:
btnStart.Visible = true;
@@ -172,13 +242,6 @@ namespace view
}
});
}
-
- private void Form1_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
- {
- if(CurrentLevel?.State is SnakeLevel.GameState.Running)
- {
- e.IsInputKey = true;
- }
- }
+ #endregion
}
}
diff --git a/view/MapChooser.cs b/view/MapChooser.cs
index 93393e9..8203b33 100644
--- a/view/MapChooser.cs
+++ b/view/MapChooser.cs
@@ -4,8 +4,15 @@ namespace view
{
public partial class MapChooser : Form
{
+ #region Fields
private SnakeLevelLoader _level_loader;
+ #endregion
+
+ #region Properties
public StoredSnakeLevel? SelectedLevel { get; private set; }
+ #endregion
+
+ #region Constructor
public MapChooser(SnakeLevelLoader level_loader)
{
this._level_loader = level_loader;
@@ -16,7 +23,24 @@ namespace view
lstLevels.Items.Add(level.LevelName);
}
}
+ #endregion
+ #region Methods
+ void ChooseLevel()
+ {
+ if (lstLevels.SelectedIndex == -1)
+ {
+ SelectedLevel = null;
+ }
+ else
+ {
+ SelectedLevel = _level_loader.StoredLevels[lstLevels.SelectedIndex];
+ Close();
+ }
+ }
+ #endregion
+
+ #region Event handlers
private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
if (lstLevels.SelectedIndex == -1)
@@ -48,18 +72,6 @@ namespace view
ChooseLevel();
}
}
-
- void ChooseLevel()
- {
- if (lstLevels.SelectedIndex == -1)
- {
- SelectedLevel = null;
- }
- else
- {
- SelectedLevel = _level_loader.StoredLevels[lstLevels.SelectedIndex];
- Close();
- }
- }
+ #endregion
}
}
diff --git a/view/levels.json b/view/levels.json
index 605eb67..25ea8fa 100644
--- a/view/levels.json
+++ b/view/levels.json
@@ -21,5 +21,50 @@
[ 2, 3 ],
[ 1, 2 ]
]
+ },
+ {
+ "level_name": "Easy",
+ "size": 10,
+ "snake_start_length": 5,
+ "new_egg_round": 2,
+ "egg_limit": 4,
+ "obstacles": [
+ [ 1, 1 ],
+ [ 1, 8 ],
+ [ 8, 1 ],
+ [ 8, 8 ]
+ ]
+ },
+ {
+ "level_name": "Medium",
+ "size": 9,
+ "snake_start_length": 5,
+ "new_egg_round": 4,
+ "egg_limit": 2,
+ "obstacles": [
+ [ 1, 1 ],
+ [ 1, 7 ],
+ [ 7, 1 ],
+ [ 7, 7 ]
+ ]
+ },
+ {
+ "level_name": "Hard",
+ "size": 7,
+ "snake_start_length": 5,
+ "new_egg_round": 8,
+ "egg_limit": 1,
+ "obstacles": [
+ [ 1, 1 ],
+ [ 1, 2 ],
+ [ 1, 3 ],
+ [ 1, 4 ],
+ [ 1, 5 ],
+ [ 5, 1 ],
+ [ 5, 2 ],
+ [ 5, 3 ],
+ [ 5, 4 ],
+ [ 5, 5 ]
+ ]
}
]
\ No newline at end of file