lots of staff

This commit is contained in:
Kecskeméti László 2024-10-23 18:49:13 +02:00
parent ff23702c15
commit a98a463531
8 changed files with 216 additions and 25 deletions

View File

@ -1,10 +1,10 @@
using model;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO.Pipes; using System.Diagnostics;
using System.Numerics;
namespace Snake.Model; namespace Snake.Model;
public class SnakeLevel{ public class SnakeLevel : IDisposable{
public static readonly TimeSpan TickTimeSpan = TimeSpan.FromMilliseconds(500); public static readonly TimeSpan TickTimeSpan = TimeSpan.FromMilliseconds(500);
public int Size {get; } public int Size {get; }
@ -43,8 +43,7 @@ public class SnakeLevel{
public int NewEggRound { get; } public int NewEggRound { get; }
public int EggLimit { get; } public int EggLimit { get; }
private int _round; private int _round;
private ITimer timer;
private PeriodicTimer _timer;
public int Score { get; private set; } public int Score { get; private set; }
public event EventHandler? GameUpdate; public event EventHandler? GameUpdate;
@ -57,12 +56,18 @@ public class SnakeLevel{
{ {
_state = value; _state = value;
GameStateUpdate?.Invoke(this, _state); GameStateUpdate?.Invoke(this, _state);
if(_state == GameState.Dead)
{
timer.Change(Timeout.InfiniteTimeSpan, TickTimeSpan);
}
} }
} }
private SnakeDirection LastDirection = SnakeDirection.Down; private SnakeDirection LastDirection = SnakeDirection.Down;
private SnakeDirection _SnakeHeadDirection; private SnakeDirection _SnakeHeadDirection;
private bool disposedValue;
public SnakeDirection SnakeHeadDirection public SnakeDirection SnakeHeadDirection
{ {
get => _SnakeHeadDirection; get => _SnakeHeadDirection;
@ -96,11 +101,17 @@ public class SnakeLevel{
_SnakeHeadDirection = SnakeDirection.Down; _SnakeHeadDirection = SnakeDirection.Down;
LastDirection = 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; _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 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)
{ {
} }
@ -187,21 +198,12 @@ public class SnakeLevel{
LastDirection = SnakeHeadDirection; LastDirection = SnakeHeadDirection;
} }
private async Task TickerTask()
{
while(State == GameState.Running && await _timer.WaitForNextTickAsync())
{
Tick();
GameUpdate?.Invoke(this, EventArgs.Empty);
}
}
public void Start() public void Start()
{ {
if(State == GameState.Paused) if(State == GameState.Paused)
{ {
State = GameState.Running; State = GameState.Running;
Task.Run(TickerTask); timer.Change(TickTimeSpan, TickTimeSpan);
} }
else else
{ {
@ -214,10 +216,41 @@ public class SnakeLevel{
if(State == GameState.Running) if(State == GameState.Running)
{ {
State = GameState.Paused; State = GameState.Paused;
timer.Change(Timeout.InfiniteTimeSpan, TickTimeSpan);
} }
else else
{ {
throw new Exception("Game is not running"); 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);
}
} }

70
model/Timer.cs Normal file
View File

@ -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<EventArgs>? 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);
}
}*/
}

View File

@ -6,4 +6,11 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="ELTE.FI.SARuleSet" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project> </Project>

View File

@ -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);
}
}
}

View File

@ -9,7 +9,7 @@ public class LevelLoaderTests
public void LoadLevelTest() public void LoadLevelTest()
{ {
// Preparre // Preparre
string SaveFile = string SaveFile =
""" """
[ [
{ {
@ -20,7 +20,9 @@ public class LevelLoaderTests
[1,1], [1,1],
[4,2], [4,2],
[6,9] [6,9]
] ],
"new_egg_round": 2,
"egg_limit": 5
}, },
{ {
"level_name": "Cool snake map", "level_name": "Cool snake map",
@ -29,7 +31,9 @@ public class LevelLoaderTests
"obstacles": [ "obstacles": [
[2,3], [2,3],
[1,2] [1,2]
] ],
"new_egg_round": 3,
"egg_limit": 6
} }
] ]
"""; """;
@ -44,11 +48,15 @@ public class LevelLoaderTests
Assert.Equal(10, current_level.Size); Assert.Equal(10, current_level.Size);
Assert.Equal(5, current_level.SnakeStartLength); Assert.Equal(5, current_level.SnakeStartLength);
Assert.Equal(current_level.Obstacles, [[1,1], [4,2], [6,9]]); 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]; current_level = loader.StoredLevels[1];
Assert.Equal("Cool snake map", current_level.LevelName); Assert.Equal("Cool snake map", current_level.LevelName);
Assert.Equal(12, current_level.Size); Assert.Equal(12, current_level.Size);
Assert.Equal(3, current_level.SnakeStartLength); Assert.Equal(3, current_level.SnakeStartLength);
Assert.Equal(current_level.Obstacles, [[2,3], [1,2]]); Assert.Equal(current_level.Obstacles, [[2,3], [1,2]]);
Assert.Equal(3, current_level.NewEggRound);
Assert.Equal(6, current_level.EggLimit);
} }
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@ -10,6 +10,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

View File

@ -39,9 +39,9 @@
// //
// Canvas // Canvas
// //
Canvas.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
Canvas.BackColor = Color.White; Canvas.BackColor = Color.White;
Canvas.Controls.Add(btnStart); Canvas.Controls.Add(btnStart);
Canvas.Dock = DockStyle.Top;
Canvas.Location = new Point(0, 0); Canvas.Location = new Point(0, 0);
Canvas.Name = "Canvas"; Canvas.Name = "Canvas";
Canvas.Size = new Size(480, 480); Canvas.Size = new Size(480, 480);

View File

@ -35,7 +35,8 @@ namespace view
CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Right; CurrentLevel.SnakeHeadDirection = SnakeLevel.SnakeDirection.Right;
break; break;
case Keys.Escape: case Keys.Escape:
CurrentLevel.Pause(); if(CurrentLevel.State == SnakeLevel.GameState.Running)
CurrentLevel.Pause();
break; break;
} }
@ -44,6 +45,7 @@ namespace view
private void Draw(SnakeLevel level) private void Draw(SnakeLevel level)
{ {
var canvas_size = Math.Min(Canvas.Width, Canvas.Height); var canvas_size = Math.Min(Canvas.Width, Canvas.Height);
graphics = Canvas.CreateGraphics();
graphics.Clear(Color.White); graphics.Clear(Color.White);
@ -80,14 +82,18 @@ namespace view
break; break;
case SnakeLevel.SnakeDirection.Down: 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 + (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 + (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, 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 + (int)cell_size, snake_head_y + (int)cell_size / 2);
break; break;
case SnakeLevel.SnakeDirection.Left: 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, 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; break;
case SnakeLevel.SnakeDirection.Right: 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, 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; break;
} }
@ -111,7 +117,7 @@ namespace view
private void btnStart_Click(object sender, EventArgs e) 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); var MapChooser = new MapChooser(this.level_loader);
MapChooser.ShowDialog(); MapChooser.ShowDialog();