namespace Snake.Core; public sealed class Snake { private readonly LinkedList _segments = new(); private readonly Queue _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 _previousSegments = new(); public Direction Direction { get; private set; } public Position Head => _segments.First!.Value; public IReadOnlyCollection Segments => _segments; /// /// Segment positions before the last Move() call, used for interpolation. /// Empty (Count == 0) before first move. /// public IReadOnlyList PreviousSegments => _previousSegments; /// /// Enqueues a direction change to be applied on a future tick. /// public void SetDirection(Direction direction) { _pendingDirections.Enqueue(direction); } /// /// Returns the head position after the next move without changing state. /// Invalid queued directions (same as current or opposite) are discarded. /// 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); } /// /// Applies at most one valid queued direction change, then moves the snake. /// 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); } /// /// Removes queued directions that would turn backwards or repeat the current direction. /// 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 }; }