Food/AppleSpawner.cs

Selects an empty cell in an Arena for spawning an apple using a deterministic Random seeded in the constructor. It scans all cells, collects indices not occupied by the Snake into a preallocated array, then returns a uniformly random free cell or null if none available.

Native Interop
namespace Coilgarden;

/// <summary>
/// Chooses where the next apple goes. Pure rules, driven by an injected seed rather than
/// the ambient global RNG, so a test can assert what it does instead of merely watching it.
/// </summary>
public sealed class AppleSpawner
{
	private readonly Arena arena;

	/// <summary>
	/// Scratch space for the free-cell scan, allocated once. Picking uniformly from the
	/// cells that are actually free is the only approach that stays correct as the arena
	/// fills up - the obvious alternative, guessing a cell and retrying, degrades towards
	/// an unbounded number of retries exactly when the snake is longest.
	/// </summary>
	private readonly int[] freeCells;

	private readonly System.Random random;

	public AppleSpawner( Arena arena, int seed )
	{
		this.arena = arena ?? throw new System.ArgumentNullException( nameof( arena ) );

		freeCells = new int[arena.CellCount];
		random = new System.Random( seed );
	}

	/// <summary>
	/// A cell the snake is not standing on, or null when the arena is full.
	/// </summary>
	public GridPos? Pick( Snake snake )
	{
		if ( snake is null ) return null;

		var count = 0;

		for ( var index = 0; index < freeCells.Length; index++ )
		{
			if ( snake.Occupies( arena.FromIndex( index ) ) ) continue;

			freeCells[count++] = index;
		}

		if ( count == 0 ) return null;

		return arena.FromIndex( freeCells[random.Next( count )] );
	}
}