feat: fully functional CLI and Avalonia UI for Snake game
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for the Avalonia game view.
|
||||
/// Wire up <see cref="Snake.Core.SnakeGame"/> here for rendering and input.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user