Files
snake-csharp/Snake.Core/Snake.cs
Heller 7d55a08380 feat: smooth snake animation + beautiful Avalonia UI overhaul
- Two-timer architecture: game timer + 60fps render timer for smooth interpolation
- Snake body: StreamGeometry path with teal gradient, rounded joins
- Directional head with white eyes and dark pupils
- Food: pulsating glow, highlight, green leaf animation
- Modern dark theme (#0D1117), glassmorphism HUD
- Speed indicator bar, score +N popup
- High score persistence to JSON
- All keyboard shortcuts: Arrows, WASD, Space/P pause, Enter start, R restart, Esc quit
- Window resizable, 640x540 default

New files: AnimationHelper.cs, HighScoreManager.cs, SnakeRenderer.cs
2026-06-19 10:02:07 +00:00

128 lines
4.1 KiB
C#

namespace Snake.Core;
public sealed class Snake
{
private readonly LinkedList<Position> _segments = new();
private readonly Queue<Direction> _pendingDirections = 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++)
{
var pos = new Position(
start.X - offset.X * i,
start.Y - offset.Y * i);
_segments.AddLast(pos);
_previousSegments.Add(pos);
}
}
private readonly List<Position> _previousSegments = new();
public Direction Direction { get; private set; }
public Position Head => _segments.First!.Value;
public IReadOnlyCollection<Position> Segments => _segments;
/// <summary>
/// Segment positions before the last Move() call, used for interpolation.
/// Empty (Count == 0) before first move.
/// </summary>
public IReadOnlyList<Position> PreviousSegments => _previousSegments;
/// <summary>
/// Enqueues a direction change to be applied on a future tick.
/// </summary>
public void SetDirection(Direction direction)
{
_pendingDirections.Enqueue(direction);
}
/// <summary>
/// Returns the head position after the next move without changing state.
/// Invalid queued directions (same as current or opposite) are discarded.
/// </summary>
public Position PeekNextHead()
{
DiscardInvalidPendingDirections();
var direction = _pendingDirections.Count > 0 ? _pendingDirections.Peek() : Direction;
var offset = DirectionToOffset(direction);
return new Position(Head.X + offset.X, Head.Y + offset.Y);
}
/// <summary>
/// Applies at most one valid queued direction change, then moves the snake.
/// </summary>
public Position Move(bool grow = false)
{
DiscardInvalidPendingDirections();
if (_pendingDirections.Count > 0)
Direction = _pendingDirections.Dequeue();
// Capture current segments as "previous" for interpolation
_previousSegments.Clear();
foreach (var seg in _segments)
_previousSegments.Add(seg);
var offset = DirectionToOffset(Direction);
var newHead = new Position(Head.X + offset.X, Head.Y + offset.Y);
_segments.AddFirst(newHead);
if (!grow)
_segments.RemoveLast();
return newHead;
}
public bool Occupies(Position position, bool excludeTail = false)
{
if (excludeTail && _segments.Last is { } last && last.Value == position)
return false;
return _segments.Contains(position);
}
/// <summary>
/// Removes queued directions that would turn backwards or repeat the current direction.
/// </summary>
private void DiscardInvalidPendingDirections()
{
while (_pendingDirections.Count > 0)
{
var next = _pendingDirections.Peek();
if (next != Direction && !AreOpposite(Direction, next))
break;
_pendingDirections.Dequeue();
}
}
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
};
}