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.
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 )] );
}
}