feat: initial project structure with Snake.Core, CLI and Avalonia skeleton
This commit is contained in:
24
Snake.Core/Board.cs
Normal file
24
Snake.Core/Board.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Snake.Core;
|
||||
|
||||
public sealed class Board
|
||||
{
|
||||
public Board(int width, int height)
|
||||
{
|
||||
if (width < 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(width), "Width must be at least 2.");
|
||||
|
||||
if (height < 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(height), "Height must be at least 2.");
|
||||
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
public int Width { get; }
|
||||
|
||||
public int Height { get; }
|
||||
|
||||
public bool IsWithinBounds(Position position) =>
|
||||
position.X >= 0 && position.X < Width &&
|
||||
position.Y >= 0 && position.Y < Height;
|
||||
}
|
||||
9
Snake.Core/Direction.cs
Normal file
9
Snake.Core/Direction.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Snake.Core;
|
||||
|
||||
public enum Direction
|
||||
{
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
38
Snake.Core/Food.cs
Normal file
38
Snake.Core/Food.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace Snake.Core;
|
||||
|
||||
public sealed class Food
|
||||
{
|
||||
private readonly Random _random;
|
||||
|
||||
public Food(Board board, IEnumerable<Position> occupiedPositions, Random? random = null)
|
||||
{
|
||||
_random = random ?? Random.Shared;
|
||||
Board = board;
|
||||
Respawn(occupiedPositions);
|
||||
}
|
||||
|
||||
public Board Board { get; }
|
||||
|
||||
public Position Position { get; private set; }
|
||||
|
||||
public void Respawn(IEnumerable<Position> occupiedPositions)
|
||||
{
|
||||
var occupied = occupiedPositions.ToHashSet();
|
||||
var freeCells = new List<Position>();
|
||||
|
||||
for (var y = 0; y < Board.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < Board.Width; x++)
|
||||
{
|
||||
var position = new Position(x, y);
|
||||
if (!occupied.Contains(position))
|
||||
freeCells.Add(position);
|
||||
}
|
||||
}
|
||||
|
||||
if (freeCells.Count == 0)
|
||||
throw new InvalidOperationException("No free cells available for food.");
|
||||
|
||||
Position = freeCells[_random.Next(freeCells.Count)];
|
||||
}
|
||||
}
|
||||
8
Snake.Core/GameTickResult.cs
Normal file
8
Snake.Core/GameTickResult.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Snake.Core;
|
||||
|
||||
public enum GameTickResult
|
||||
{
|
||||
Moved,
|
||||
AteFood,
|
||||
GameOver
|
||||
}
|
||||
3
Snake.Core/Position.cs
Normal file
3
Snake.Core/Position.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Snake.Core;
|
||||
|
||||
public readonly record struct Position(int X, int Y);
|
||||
9
Snake.Core/Snake.Core.csproj
Normal file
9
Snake.Core/Snake.Core.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
80
Snake.Core/Snake.cs
Normal file
80
Snake.Core/Snake.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
namespace Snake.Core;
|
||||
|
||||
public sealed class Snake
|
||||
{
|
||||
private readonly LinkedList<Position> _segments = new();
|
||||
|
||||
public Snake(Position start, int length, Direction direction)
|
||||
{
|
||||
if (length < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Length must be at least 1.");
|
||||
|
||||
Direction = direction;
|
||||
|
||||
var offset = DirectionToOffset(direction);
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
_segments.AddLast(new Position(
|
||||
start.X - offset.X * i,
|
||||
start.Y - offset.Y * i));
|
||||
}
|
||||
}
|
||||
|
||||
public Direction Direction { get; private set; }
|
||||
|
||||
public Position Head => _segments.First!.Value;
|
||||
|
||||
public IReadOnlyCollection<Position> Segments => _segments;
|
||||
|
||||
public void SetDirection(Direction direction)
|
||||
{
|
||||
if (direction == Direction)
|
||||
return;
|
||||
|
||||
if (AreOpposite(Direction, direction))
|
||||
return;
|
||||
|
||||
Direction = direction;
|
||||
}
|
||||
|
||||
public Position PeekNextHead()
|
||||
{
|
||||
var offset = DirectionToOffset(Direction);
|
||||
return new Position(Head.X + offset.X, Head.Y + offset.Y);
|
||||
}
|
||||
|
||||
public Position Move(bool grow = false)
|
||||
{
|
||||
var newHead = PeekNextHead();
|
||||
|
||||
_segments.AddFirst(newHead);
|
||||
|
||||
if (!grow)
|
||||
_segments.RemoveLast();
|
||||
|
||||
return newHead;
|
||||
}
|
||||
|
||||
public bool Occupies(Position position) =>
|
||||
_segments.Contains(position);
|
||||
|
||||
private static Position DirectionToOffset(Direction direction) =>
|
||||
direction switch
|
||||
{
|
||||
Direction.Up => new Position(0, -1),
|
||||
Direction.Down => new Position(0, 1),
|
||||
Direction.Left => new Position(-1, 0),
|
||||
Direction.Right => new Position(1, 0),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(direction))
|
||||
};
|
||||
|
||||
private static bool AreOpposite(Direction current, Direction next) =>
|
||||
(current, next) switch
|
||||
{
|
||||
(Direction.Up, Direction.Down) => true,
|
||||
(Direction.Down, Direction.Up) => true,
|
||||
(Direction.Left, Direction.Right) => true,
|
||||
(Direction.Right, Direction.Left) => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
60
Snake.Core/SnakeGame.cs
Normal file
60
Snake.Core/SnakeGame.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace Snake.Core;
|
||||
|
||||
public sealed class SnakeGame
|
||||
{
|
||||
public SnakeGame(int width, int height, Random? random = null)
|
||||
{
|
||||
_random = random ?? Random.Shared;
|
||||
Board = new Board(width, height);
|
||||
|
||||
var start = new Position(width / 2, height / 2);
|
||||
Snake = new Snake(start, length: 3, Direction.Right);
|
||||
Food = new Food(Board, Snake.Segments, _random);
|
||||
}
|
||||
|
||||
private readonly Random _random;
|
||||
|
||||
public Board Board { get; }
|
||||
|
||||
public Snake Snake { get; }
|
||||
|
||||
public Food Food { get; }
|
||||
|
||||
public int Score { get; private set; }
|
||||
|
||||
public bool IsGameOver { get; private set; }
|
||||
|
||||
public void SetDirection(Direction direction) => Snake.SetDirection(direction);
|
||||
|
||||
public GameTickResult Tick()
|
||||
{
|
||||
if (IsGameOver)
|
||||
return GameTickResult.GameOver;
|
||||
|
||||
var newHead = Snake.PeekNextHead();
|
||||
|
||||
if (!Board.IsWithinBounds(newHead) || Snake.Occupies(newHead))
|
||||
{
|
||||
IsGameOver = true;
|
||||
return GameTickResult.GameOver;
|
||||
}
|
||||
|
||||
var ateFood = newHead == Food.Position;
|
||||
Snake.Move(grow: ateFood);
|
||||
|
||||
if (ateFood)
|
||||
{
|
||||
Score++;
|
||||
if (Snake.Segments.Count >= Board.Width * Board.Height)
|
||||
{
|
||||
IsGameOver = true;
|
||||
return GameTickResult.AteFood;
|
||||
}
|
||||
|
||||
Food.Respawn(Snake.Segments);
|
||||
return GameTickResult.AteFood;
|
||||
}
|
||||
|
||||
return GameTickResult.Moved;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user