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