feat: initial project structure with Snake.Core, CLI and Avalonia skeleton
This commit is contained in:
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
## Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
## Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
## Visual Studio cache/options directory
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
## MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
## NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
## .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
## NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
|
||||||
|
## Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
## VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
## OS generated files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
## Avalonia
|
||||||
|
*.DotSettings.user
|
||||||
8
Directory.Build.props
Normal file
8
Directory.Build.props
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
66
README.md
Normal file
66
README.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Snake (C#)
|
||||||
|
|
||||||
|
Классическая игра «Змейка» на .NET 8 с разделением логики и интерфейсов.
|
||||||
|
|
||||||
|
## Структура решения
|
||||||
|
|
||||||
|
| Проект | Назначение |
|
||||||
|
|--------|------------|
|
||||||
|
| `Snake.Core` | Игровая логика: поле, змейка, еда, движение, коллизии |
|
||||||
|
| `Snake.CLI` | Консольный интерфейс |
|
||||||
|
| `Snake.Avalonia` | Графический интерфейс (Avalonia UI) |
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Консольная версия
|
||||||
|
dotnet run --project Snake.CLI
|
||||||
|
|
||||||
|
# Avalonia UI (требуется графическая среда)
|
||||||
|
dotnet run --project Snake.Avalonia
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Flow
|
||||||
|
|
||||||
|
Проект использует [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/):
|
||||||
|
|
||||||
|
| Ветка | Назначение |
|
||||||
|
|-------|------------|
|
||||||
|
| `main` | Стабильные релизы |
|
||||||
|
| `develop` | Интеграционная ветка для текущей разработки |
|
||||||
|
| `feature/*` | Новые функции (ветвление от `develop`) |
|
||||||
|
| `release/*` | Подготовка релиза (ветвление от `develop`) |
|
||||||
|
| `hotfix/*` | Срочные исправления в production (ветвление от `main`) |
|
||||||
|
|
||||||
|
### Типичный workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Начать новую функцию
|
||||||
|
git checkout develop
|
||||||
|
git checkout -b feature/my-feature
|
||||||
|
|
||||||
|
# Завершить функцию
|
||||||
|
git checkout develop
|
||||||
|
git merge --no-ff feature/my-feature
|
||||||
|
git branch -d feature/my-feature
|
||||||
|
|
||||||
|
# Релиз
|
||||||
|
git checkout -b release/1.0.0 develop
|
||||||
|
# ... финальные правки, тег версии ...
|
||||||
|
git checkout main
|
||||||
|
git merge --no-ff release/1.0.0
|
||||||
|
git tag -a v1.0.0
|
||||||
|
git checkout develop
|
||||||
|
git merge --no-ff release/1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Игровая логика (Snake.Core)
|
||||||
|
|
||||||
|
- `Board` — размеры поля и проверка границ
|
||||||
|
- `Snake` — сегменты тела, направление, движение
|
||||||
|
- `Food` — случайное размещение еды на свободных клетках
|
||||||
|
- `SnakeGame` — игровой цикл, счёт, коллизии со стенами и собой
|
||||||
10
Snake.Avalonia/App.axaml
Normal file
10
Snake.Avalonia/App.axaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="Snake.Avalonia.App"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
23
Snake.Avalonia/App.axaml.cs
Normal file
23
Snake.Avalonia/App.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace Snake.Avalonia;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Snake.Avalonia/MainWindow.axaml
Normal file
10
Snake.Avalonia/MainWindow.axaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
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"
|
||||||
|
x:Class="Snake.Avalonia.MainWindow"
|
||||||
|
Title="Snake">
|
||||||
|
<views:GameView />
|
||||||
|
</Window>
|
||||||
11
Snake.Avalonia/MainWindow.axaml.cs
Normal file
11
Snake.Avalonia/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Snake.Avalonia;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Snake.Avalonia/Program.cs
Normal file
24
Snake.Avalonia/Program.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Snake.Avalonia;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
|
// yet and stuff might break.
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||||
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
#if DEBUG
|
||||||
|
.WithDeveloperTools()
|
||||||
|
#endif
|
||||||
|
.WithInterFont()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
24
Snake.Avalonia/Snake.Avalonia.csproj
Normal file
24
Snake.Avalonia/Snake.Avalonia.csproj
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="12.0.4" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="12.0.4" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.4" />
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.4" />
|
||||||
|
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1">
|
||||||
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Snake.Core\Snake.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
7
Snake.Avalonia/Views/GameView.axaml
Normal file
7
Snake.Avalonia/Views/GameView.axaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="Snake.Avalonia.Views.GameView">
|
||||||
|
<TextBlock Text="Snake game UI — connect Snake.Core here"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</UserControl>
|
||||||
15
Snake.Avalonia/Views/GameView.axaml.cs
Normal file
15
Snake.Avalonia/Views/GameView.axaml.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public GameView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Snake.Avalonia/app.manifest
Normal file
18
Snake.Avalonia/app.manifest
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<!-- This manifest is used on Windows only.
|
||||||
|
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||||
|
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="Snake.Avalonia.Desktop"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
50
Snake.CLI/ConsoleRenderer.cs
Normal file
50
Snake.CLI/ConsoleRenderer.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Snake.Core;
|
||||||
|
|
||||||
|
namespace Snake.CLI;
|
||||||
|
|
||||||
|
internal static class ConsoleRenderer
|
||||||
|
{
|
||||||
|
public static void Render(SnakeGame game)
|
||||||
|
{
|
||||||
|
Console.Clear();
|
||||||
|
Console.WriteLine($"Score: {game.Score}");
|
||||||
|
|
||||||
|
if (game.IsGameOver)
|
||||||
|
Console.WriteLine("Game Over! Press R to restart or Q to quit.");
|
||||||
|
|
||||||
|
var board = game.Board;
|
||||||
|
var snakeCells = game.Snake.Segments.ToHashSet();
|
||||||
|
var food = game.Food.Position;
|
||||||
|
|
||||||
|
Console.Write('+');
|
||||||
|
Console.Write(new string('-', board.Width * 2));
|
||||||
|
Console.WriteLine('+');
|
||||||
|
|
||||||
|
for (var y = 0; y < board.Height; y++)
|
||||||
|
{
|
||||||
|
Console.Write('|');
|
||||||
|
|
||||||
|
for (var x = 0; x < board.Width; x++)
|
||||||
|
{
|
||||||
|
var position = new Position(x, y);
|
||||||
|
var symbol = position == food
|
||||||
|
? '@'
|
||||||
|
: position == game.Snake.Head
|
||||||
|
? 'O'
|
||||||
|
: snakeCells.Contains(position)
|
||||||
|
? 'o'
|
||||||
|
: ' ';
|
||||||
|
|
||||||
|
Console.Write(symbol);
|
||||||
|
Console.Write(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write('+');
|
||||||
|
Console.Write(new string('-', board.Width * 2));
|
||||||
|
Console.WriteLine('+');
|
||||||
|
Console.WriteLine("Controls: Arrow keys to move, R to restart, Q to quit.");
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Snake.CLI/Program.cs
Normal file
44
Snake.CLI/Program.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using Snake.CLI;
|
||||||
|
using Snake.Core;
|
||||||
|
|
||||||
|
const int boardWidth = 20;
|
||||||
|
const int boardHeight = 15;
|
||||||
|
const int tickMs = 120;
|
||||||
|
|
||||||
|
SnakeGame game = new(boardWidth, boardHeight);
|
||||||
|
ConsoleRenderer.Render(game);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (Console.KeyAvailable)
|
||||||
|
{
|
||||||
|
var key = Console.ReadKey(intercept: true).Key;
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case ConsoleKey.UpArrow:
|
||||||
|
game.SetDirection(Direction.Up);
|
||||||
|
break;
|
||||||
|
case ConsoleKey.DownArrow:
|
||||||
|
game.SetDirection(Direction.Down);
|
||||||
|
break;
|
||||||
|
case ConsoleKey.LeftArrow:
|
||||||
|
game.SetDirection(Direction.Left);
|
||||||
|
break;
|
||||||
|
case ConsoleKey.RightArrow:
|
||||||
|
game.SetDirection(Direction.Right);
|
||||||
|
break;
|
||||||
|
case ConsoleKey.Q:
|
||||||
|
return;
|
||||||
|
case ConsoleKey.R when game.IsGameOver:
|
||||||
|
game = new SnakeGame(boardWidth, boardHeight);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!game.IsGameOver)
|
||||||
|
game.Tick();
|
||||||
|
|
||||||
|
ConsoleRenderer.Render(game);
|
||||||
|
Thread.Sleep(tickMs);
|
||||||
|
}
|
||||||
14
Snake.CLI/Snake.CLI.csproj
Normal file
14
Snake.CLI/Snake.CLI.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Snake.Core\Snake.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Snake.sln
Normal file
34
Snake.sln
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snake.Core", "Snake.Core\Snake.Core.csproj", "{C74F838E-FFB4-499F-99DF-57142C110454}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snake.CLI", "Snake.CLI\Snake.CLI.csproj", "{8DB20DDA-CE97-41B6-B51E-C195DD387C70}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snake.Avalonia", "Snake.Avalonia\Snake.Avalonia.csproj", "{4C16155D-6998-4635-8478-5F27FDC4189D}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{C74F838E-FFB4-499F-99DF-57142C110454}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C74F838E-FFB4-499F-99DF-57142C110454}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C74F838E-FFB4-499F-99DF-57142C110454}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C74F838E-FFB4-499F-99DF-57142C110454}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8DB20DDA-CE97-41B6-B51E-C195DD387C70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8DB20DDA-CE97-41B6-B51E-C195DD387C70}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8DB20DDA-CE97-41B6-B51E-C195DD387C70}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8DB20DDA-CE97-41B6-B51E-C195DD387C70}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4C16155D-6998-4635-8478-5F27FDC4189D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4C16155D-6998-4635-8478-5F27FDC4189D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4C16155D-6998-4635-8478-5F27FDC4189D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4C16155D-6998-4635-8478-5F27FDC4189D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
6
global.json
Normal file
6
global.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"rollForward": "latestMinor"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user