Data class that stores persistent high score info for the game: version, best score, best length, runs played and total apples eaten. It validates and updates totals via Submit and normalises values with Clamp.
namespace Coilgarden;
/// <summary>
/// What survives between runs. Deliberately separate from <see cref="SnakeGame"/>: that
/// owns one run's score, this owns the record across all of them, and conflating the two is
/// how a restart ends up clearing a personal best.
/// <para>
/// A plain serialisable class with no engine dependency, so the rules about what counts as
/// a new best are testable headlessly. <see cref="HighScoreStore"/> is the only part that
/// touches the filesystem.
/// </para>
/// </summary>
public sealed class HighScoreData
{
/// <summary>
/// Bumped when the shape of this file changes in a way older builds cannot read. Kept
/// so a future change has somewhere to hang a migration rather than having to refuse
/// the file.
/// </summary>
public int Version { get; set; } = CurrentVersion;
public const int CurrentVersion = 1;
public int BestScore { get; set; }
public int BestLength { get; set; }
public int RunsPlayed { get; set; }
public int ApplesEatenTotal { get; set; }
/// <summary>
/// Files a finished run and reports whether it beat the record.
/// <para>
/// Score and length are tracked independently because they can genuinely come apart
/// once scoring stops being flat - the longest snake need not be the highest score, and
/// a player who cares about one should not have the other quietly overwrite it.
/// </para>
/// <para>
/// Strictly greater than, not greater-or-equal: matching your best is not beating it,
/// and reporting it as a new record every time would make the celebration meaningless.
/// </para>
/// </summary>
public bool Submit( int score, int length, int applesEaten )
{
RunsPlayed++;
ApplesEatenTotal += Math.Max( 0, applesEaten );
if ( length > BestLength ) BestLength = length;
if ( score <= BestScore ) return false;
BestScore = score;
return true;
}
/// <summary>
/// Forces every value into a range this build considers possible.
/// <para>
/// The file is on disk, so it can be hand-edited, truncated, or written by a different
/// build. A negative best would display as a negative record; nothing else in the game
/// should have to defend against that.
/// </para>
/// </summary>
public void Clamp()
{
Version = CurrentVersion;
BestScore = Math.Max( 0, BestScore );
BestLength = Math.Max( 0, BestLength );
RunsPlayed = Math.Max( 0, RunsPlayed );
ApplesEatenTotal = Math.Max( 0, ApplesEatenTotal );
}
}