diff --git a/Snake.Avalonia/MainWindow.axaml b/Snake.Avalonia/MainWindow.axaml
index 55047b6..7571fae 100644
--- a/Snake.Avalonia/MainWindow.axaml
+++ b/Snake.Avalonia/MainWindow.axaml
@@ -3,8 +3,11 @@
xmlns:views="using:Snake.Avalonia.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="420"
x:Class="Snake.Avalonia.MainWindow"
- Title="Snake">
+ Title="Snake"
+ Width="480"
+ Height="420"
+ CanResize="False">
diff --git a/Snake.Avalonia/Views/GameView.axaml b/Snake.Avalonia/Views/GameView.axaml
index e4ac10a..bc68e28 100644
--- a/Snake.Avalonia/Views/GameView.axaml
+++ b/Snake.Avalonia/Views/GameView.axaml
@@ -1,7 +1,30 @@
-
+ x:Class="Snake.Avalonia.Views.GameView"
+ Focusable="True">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Snake.Avalonia/Views/GameView.axaml.cs b/Snake.Avalonia/Views/GameView.axaml.cs
index df8fddc..06732ad 100644
--- a/Snake.Avalonia/Views/GameView.axaml.cs
+++ b/Snake.Avalonia/Views/GameView.axaml.cs
@@ -1,15 +1,176 @@
+using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Threading;
+using Snake.Core;
namespace Snake.Avalonia.Views;
-///
-/// Placeholder for the Avalonia game view.
-/// Wire up here for rendering and input.
-///
public partial class GameView : UserControl
{
+ private const int BoardWidth = 20;
+ private const int BoardHeight = 15;
+ private const double CellSize = 20;
+ private const int BaseTickMs = 120;
+
+ private SnakeGame _game = new(BoardWidth, BoardHeight);
+ private readonly DispatcherTimer _timer;
+ private bool _isRunning;
+
public GameView()
{
InitializeComponent();
+
+ GameCanvas.Width = BoardWidth * CellSize;
+ GameCanvas.Height = BoardHeight * CellSize;
+
+ _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(BaseTickMs) };
+ _timer.Tick += OnTimerTick;
+
+ StartButton.Click += OnStartClick;
+ RestartButton.Click += OnRestartClick;
+ KeyDown += OnKeyDown;
+ GameCanvas.PointerPressed += (_, _) => Focus();
+
+ UpdateHud();
+ RenderBoard();
+ }
+
+ private void OnStartClick(object? sender, RoutedEventArgs e)
+ {
+ if (_isRunning && !_game.IsGameOver)
+ return;
+
+ if (_game.IsGameOver)
+ ResetGame();
+
+ StartGame();
+ }
+
+ private void OnRestartClick(object? sender, RoutedEventArgs e)
+ {
+ ResetGame();
+ StartGame();
+ }
+
+ private void ResetGame()
+ {
+ _game = new SnakeGame(BoardWidth, BoardHeight);
+ _isRunning = false;
+ _timer.Stop();
+ UpdateHud();
+ RenderBoard();
+ }
+
+ private void StartGame()
+ {
+ _isRunning = true;
+ _timer.Interval = TimeSpan.FromMilliseconds(SnakeGame.GetTickIntervalMs(_game.Score, BaseTickMs));
+ _timer.Start();
+ UpdateHud();
+ Focus();
+ }
+
+ private void OnTimerTick(object? sender, EventArgs e)
+ {
+ if (!_isRunning || _game.IsGameOver)
+ return;
+
+ _game.Tick();
+
+ if (_game.IsGameOver)
+ {
+ _isRunning = false;
+ _timer.Stop();
+ }
+ else
+ {
+ var interval = SnakeGame.GetTickIntervalMs(_game.Score, BaseTickMs);
+ if (_timer.Interval != TimeSpan.FromMilliseconds(interval))
+ _timer.Interval = TimeSpan.FromMilliseconds(interval);
+ }
+
+ UpdateHud();
+ RenderBoard();
+ }
+
+ private void OnKeyDown(object? sender, KeyEventArgs e)
+ {
+ if (!_isRunning && !_game.IsGameOver && e.Key is Key.Up or Key.Down or Key.Left or Key.Right)
+ {
+ StartGame();
+ }
+
+ switch (e.Key)
+ {
+ case Key.Up:
+ _game.SetDirection(Direction.Up);
+ break;
+ case Key.Down:
+ _game.SetDirection(Direction.Down);
+ break;
+ case Key.Left:
+ _game.SetDirection(Direction.Left);
+ break;
+ case Key.Right:
+ _game.SetDirection(Direction.Right);
+ break;
+ }
+ }
+
+ private void UpdateHud()
+ {
+ ScoreText.Text = $"Score: {_game.Score}";
+ LevelText.Text = $"Level: {_game.Level}";
+
+ StatusText.Text = _game.IsGameOver
+ ? "Game Over!"
+ : _isRunning
+ ? "Playing"
+ : "Press Start or an arrow key";
+ }
+
+ private void RenderBoard()
+ {
+ GameCanvas.Children.Clear();
+
+ var snakeCells = _game.Snake.Segments.ToHashSet();
+ var head = _game.Snake.Head;
+
+ for (var y = 0; y < BoardHeight; y++)
+ {
+ for (var x = 0; x < BoardWidth; x++)
+ {
+ var position = new Position(x, y);
+ if (!snakeCells.Contains(position))
+ continue;
+
+ var rect = new Rectangle
+ {
+ Width = CellSize - 1,
+ Height = CellSize - 1,
+ Fill = position == head ? Brushes.LimeGreen : Brushes.Green
+ };
+
+ Canvas.SetLeft(rect, x * CellSize);
+ Canvas.SetTop(rect, y * CellSize);
+ GameCanvas.Children.Add(rect);
+ }
+ }
+
+ var food = _game.Food.Position;
+ var foodRect = new Rectangle
+ {
+ Width = CellSize - 1,
+ Height = CellSize - 1,
+ Fill = Brushes.Red
+ };
+
+ Canvas.SetLeft(foodRect, food.X * CellSize);
+ Canvas.SetTop(foodRect, food.Y * CellSize);
+ GameCanvas.Children.Add(foodRect);
}
}
diff --git a/Snake.CLI/ConsoleRenderer.cs b/Snake.CLI/ConsoleRenderer.cs
index 41386d7..5f8bf5c 100644
--- a/Snake.CLI/ConsoleRenderer.cs
+++ b/Snake.CLI/ConsoleRenderer.cs
@@ -7,7 +7,7 @@ internal static class ConsoleRenderer
public static void Render(SnakeGame game)
{
Console.Clear();
- Console.WriteLine($"Score: {game.Score}");
+ Console.WriteLine($"Score: {game.Score} Level: {game.Level}");
if (game.IsGameOver)
Console.WriteLine("Game Over! Press R to restart or Q to quit.");
diff --git a/Snake.CLI/Program.cs b/Snake.CLI/Program.cs
index a5bb0fb..0452e03 100644
--- a/Snake.CLI/Program.cs
+++ b/Snake.CLI/Program.cs
@@ -3,7 +3,7 @@ using Snake.Core;
const int boardWidth = 20;
const int boardHeight = 15;
-const int tickMs = 120;
+const int baseTickMs = 120;
SnakeGame game = new(boardWidth, boardHeight);
ConsoleRenderer.Render(game);
@@ -40,5 +40,5 @@ while (true)
game.Tick();
ConsoleRenderer.Render(game);
- Thread.Sleep(tickMs);
+ Thread.Sleep(SnakeGame.GetTickIntervalMs(game.Score, baseTickMs));
}
diff --git a/Snake.Core/SnakeGame.cs b/Snake.Core/SnakeGame.cs
index fe01bfe..cc44090 100644
--- a/Snake.Core/SnakeGame.cs
+++ b/Snake.Core/SnakeGame.cs
@@ -24,6 +24,13 @@ public sealed class SnakeGame
public bool IsGameOver { get; private set; }
+ public const int PointsPerLevel = 5;
+
+ public int Level => 1 + Score / PointsPerLevel;
+
+ public static int GetTickIntervalMs(int score, int baseIntervalMs = 120, int minIntervalMs = 50) =>
+ Math.Max(minIntervalMs, baseIntervalMs - (1 + score / PointsPerLevel - 1) * 10);
+
public void SetDirection(Direction direction) => Snake.SetDirection(direction);
public GameTickResult Tick()