Immutable record struct that holds all per-run game configuration for the snake game, including grid size, start length/direction, buffered turns and apple points. It provides a Default from GameConfig, a Clamped() method that forces values into safe ranges, and a computed CellCount.
namespace Coilgarden;
/// <summary>
/// Every value that changes how a run behaves, as one immutable bundle fixed for the
/// lifetime of that run.
/// <para>
/// This exists so the rules have no hidden dependency on static configuration. Before it,
/// <see cref="SnakeGame"/> took its arena size as arguments but read start length, buffer
/// size and apple value straight out of <see cref="GameConfig"/> - which meant a test could
/// not vary them, and half the game's configurability was compile-time only.
/// </para>
/// <para>
/// Immutable per run on purpose: a rule that can change mid-run is a rule a restart has to
/// remember to put back. Changing the rules means starting a run with different ones.
/// </para>
/// </summary>
public readonly record struct GameRules(
int Width,
int Height,
int StartLength,
Direction StartDirection,
int MaxBufferedTurns,
int ApplePoints )
{
/// <summary>The designed values, from <see cref="GameConfig"/>.</summary>
public static GameRules Default => new(
GameConfig.GridWidth,
GameConfig.GridHeight,
GameConfig.StartLength,
GameConfig.StartDirection,
GameConfig.MaxBufferedTurns,
GameConfig.ApplePoints );
/// <summary>
/// The same rules with every value forced into a range a run can actually be played
/// with.
/// <para>
/// The point is that nothing downstream has to defend itself. These values arrive from
/// editor sliders and one day from a settings file, so a zero or a negative is a matter
/// of time - and an arena of width 0 would otherwise reach <see cref="Arena"/>, which
/// throws, and take the whole scene down rather than starting a slightly odd game.
/// </para>
/// </summary>
public GameRules Clamped()
{
var width = Math.Clamp( Width, GameConfig.MinGridSize, GameConfig.MaxGridSize );
var height = Math.Clamp( Height, GameConfig.MinGridSize, GameConfig.MaxGridSize );
// The body is laid out behind a head sitting at the centre, so the limit is the
// distance from the centre to the wall it runs towards. Asking the arena keeps this
// the same arithmetic the snake is actually built with.
var longest = Math.Max( 1, Arena.MaxStartLength( width, height, StartDirection ) );
return this with
{
Width = width,
Height = height,
StartLength = Math.Clamp( StartLength, 1, longest ),
MaxBufferedTurns = Math.Clamp( MaxBufferedTurns, 1, GameConfig.MaxBufferedTurnsLimit ),
ApplePoints = Math.Max( 0, ApplePoints )
};
}
public int CellCount => Width * Height;
}