From a98a4635311ae3bcad136f7fc9941ec28b02fc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kecskem=C3=A9ti=20L=C3=A1szl=C3=B3?= Date: Wed, 23 Oct 2024 18:49:13 +0200 Subject: [PATCH] lots of staff --- model/SnakeLevel.cs | 65 +++++++++++++++++++------ model/Timer.cs | 70 +++++++++++++++++++++++++++ model/model.csproj | 7 +++ tests/Model/SnakeLevelTest.cs | 65 +++++++++++++++++++++++++ tests/Persistance/LevelLoaderTests.cs | 14 ++++-- tests/tests.csproj | 4 +- view/Form1.Designer.cs | 2 +- view/Form1.cs | 14 ++++-- 8 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 model/Timer.cs create mode 100644 tests/Model/SnakeLevelTest.cs diff --git a/model/SnakeLevel.cs b/model/SnakeLevel.cs index c025526..d74a8a9 100644 --- a/model/SnakeLevel.cs +++ b/model/SnakeLevel.cs @@ -1,10 +1,10 @@ +using model; using System.Collections.ObjectModel; -using System.IO.Pipes; -using System.Numerics; +using System.Diagnostics; namespace Snake.Model; -public class SnakeLevel{ +public class SnakeLevel : IDisposable{ public static readonly TimeSpan TickTimeSpan = TimeSpan.FromMilliseconds(500); public int Size {get; } @@ -43,8 +43,7 @@ public class SnakeLevel{ public int NewEggRound { get; } public int EggLimit { get; } private int _round; - - private PeriodicTimer _timer; + private ITimer timer; public int Score { get; private set; } public event EventHandler? GameUpdate; @@ -57,12 +56,18 @@ public class SnakeLevel{ { _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; @@ -96,11 +101,17 @@ public class SnakeLevel{ _SnakeHeadDirection = SnakeDirection.Down; LastDirection = SnakeDirection.Down; - _timer = new PeriodicTimer(TickTimeSpan, time_provider); + 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 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) { } @@ -187,21 +198,12 @@ public class SnakeLevel{ LastDirection = SnakeHeadDirection; } - private async Task TickerTask() - { - while(State == GameState.Running && await _timer.WaitForNextTickAsync()) - { - Tick(); - GameUpdate?.Invoke(this, EventArgs.Empty); - } - } - public void Start() { if(State == GameState.Paused) { State = GameState.Running; - Task.Run(TickerTask); + timer.Change(TickTimeSpan, TickTimeSpan); } else { @@ -214,10 +216,41 @@ public class SnakeLevel{ 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); + } } \ No newline at end of file diff --git a/model/Timer.cs b/model/Timer.cs new file mode 100644 index 0000000..662cedf --- /dev/null +++ b/model/Timer.cs @@ -0,0 +1,70 @@ +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 fa71b7a..dcaa502 100644 --- a/model/model.csproj +++ b/model/model.csproj @@ -6,4 +6,11 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/Model/SnakeLevelTest.cs b/tests/Model/SnakeLevelTest.cs new file mode 100644 index 0000000..07823b5 --- /dev/null +++ b/tests/Model/SnakeLevelTest.cs @@ -0,0 +1,65 @@ +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; + +namespace tests.Model +{ + public class SnakeLevelTest + { + FakeTimeProvider timeProvider = new FakeTimeProvider(); + SnakeLevel level; + Point[] obstacles = [(3, 3), (4, 4)]; + public SnakeLevelTest() + { + level = new SnakeLevel( + 10, obstacles, + 5, 3, 2, timeProvider + ); + } + + [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.State.Should().Be(SnakeLevel.GameState.Paused); + foreach(Point p in obstacles) + { + level[p].Should().Be(SnakeLevel.LevelBlock.Obstacle); + } + } + + [Fact] + public void SnakeOutOfBoundsTest() + { + bool game_state_changed_raised = false; + level.GameStateUpdate += (s, e) => + { + game_state_changed_raised = true; + }; + + level.Start(); + + game_state_changed_raised.Should().Be(true); + game_state_changed_raised = false; + for(int i=0; i<5; 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); + } + } +} diff --git a/tests/Persistance/LevelLoaderTests.cs b/tests/Persistance/LevelLoaderTests.cs index 43e41a4..e469aaa 100644 --- a/tests/Persistance/LevelLoaderTests.cs +++ b/tests/Persistance/LevelLoaderTests.cs @@ -9,7 +9,7 @@ public class LevelLoaderTests public void LoadLevelTest() { // Preparre - string SaveFile = + string SaveFile = """ [ { @@ -20,7 +20,9 @@ public class LevelLoaderTests [1,1], [4,2], [6,9] - ] + ], + "new_egg_round": 2, + "egg_limit": 5 }, { "level_name": "Cool snake map", @@ -29,7 +31,9 @@ public class LevelLoaderTests "obstacles": [ [2,3], [1,2] - ] + ], + "new_egg_round": 3, + "egg_limit": 6 } ] """; @@ -44,11 +48,15 @@ public class LevelLoaderTests Assert.Equal(10, current_level.Size); Assert.Equal(5, current_level.SnakeStartLength); Assert.Equal(current_level.Obstacles, [[1,1], [4,2], [6,9]]); + Assert.Equal(2, current_level.NewEggRound); + Assert.Equal(5, current_level.EggLimit); current_level = loader.StoredLevels[1]; Assert.Equal("Cool snake map", current_level.LevelName); Assert.Equal(12, current_level.Size); Assert.Equal(3, current_level.SnakeStartLength); Assert.Equal(current_level.Obstacles, [[2,3], [1,2]]); + Assert.Equal(3, current_level.NewEggRound); + Assert.Equal(6, current_level.EggLimit); } } \ No newline at end of file diff --git a/tests/tests.csproj b/tests/tests.csproj index c786adc..3226aa4 100644 --- a/tests/tests.csproj +++ b/tests/tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -10,6 +10,8 @@ + + diff --git a/view/Form1.Designer.cs b/view/Form1.Designer.cs index 67b5098..3c06eaa 100644 --- a/view/Form1.Designer.cs +++ b/view/Form1.Designer.cs @@ -39,9 +39,9 @@ // // Canvas // + Canvas.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; Canvas.BackColor = Color.White; Canvas.Controls.Add(btnStart); - Canvas.Dock = DockStyle.Top; Canvas.Location = new Point(0, 0); Canvas.Name = "Canvas"; Canvas.Size = new Size(480, 480); diff --git a/view/Form1.cs b/view/Form1.cs index 11690b2..b53ac4b 100644 --- a/view/Form1.cs +++ b/view/Form1.cs @@ -35,7 +35,8 @@ namespace view CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Right; break; case Keys.Escape: - CurrentLevel.Pause(); + if(CurrentLevel.State == SnakeLevel.GameState.Running) + CurrentLevel.Pause(); break; } @@ -44,6 +45,7 @@ namespace view private void Draw(SnakeLevel level) { var canvas_size = Math.Min(Canvas.Width, Canvas.Height); + graphics = Canvas.CreateGraphics(); graphics.Clear(Color.White); @@ -80,14 +82,18 @@ namespace view break; case SnakeLevel.SnakeDirection.Down: 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); - graphics.DrawLine(Pens.Black, snake_head_x + (int)cell_size / 2, snake_head_y, snake_head_x, snake_head_y + (int)cell_size / 2); - graphics.DrawLine(Pens.Black, snake_head_x + (int)cell_size / 2, snake_head_y, snake_head_x + (int)cell_size, snake_head_y + (int)cell_size / 2); + graphics.DrawLine(Pens.Black, snake_head_x + (int)cell_size / 2, snake_head_y + (int)cell_size, snake_head_x, snake_head_y + (int)cell_size / 2); + graphics.DrawLine(Pens.Black, snake_head_x + (int)cell_size / 2, snake_head_y + (int)cell_size, snake_head_x + (int)cell_size, snake_head_y + (int)cell_size / 2); break; case SnakeLevel.SnakeDirection.Left: graphics.DrawLine(Pens.Black, snake_head_x, snake_head_y + (int)cell_size / 2, snake_head_x + (int)cell_size, snake_head_y + (int)cell_size / 2); + graphics.DrawLine(Pens.Black, snake_head_x, snake_head_y + (int)(cell_size / 2), snake_head_x + (int)(cell_size / 2), snake_head_y); + graphics.DrawLine(Pens.Black, snake_head_x, snake_head_y + (int)(cell_size / 2), snake_head_x + (int)(cell_size / 2), snake_head_y + (int)cell_size); break; case SnakeLevel.SnakeDirection.Right: graphics.DrawLine(Pens.Black, snake_head_x, snake_head_y + (int)cell_size / 2, snake_head_x + (int)cell_size, snake_head_y + (int)cell_size / 2); + graphics.DrawLine(Pens.Black, snake_head_x + (int)cell_size, snake_head_y + (int)(cell_size / 2), snake_head_x + (int)(cell_size / 2), snake_head_y); + graphics.DrawLine(Pens.Black, snake_head_x + (int)cell_size, snake_head_y + (int)(cell_size / 2), snake_head_x + (int)(cell_size / 2), snake_head_y + (int)cell_size); break; } @@ -111,7 +117,7 @@ namespace view private void btnStart_Click(object sender, EventArgs e) { - if (CurrentLevel is null || CurrentLevel.State != SnakeLevel.GameState.Running) + if (CurrentLevel is null || CurrentLevel.State == SnakeLevel.GameState.Dead) { var MapChooser = new MapChooser(this.level_loader); MapChooser.ShowDialog();