three bug-encoding invariants, one red/green cycle each: - Coins.TrySubtract fails instead of clamping - LevelTime rejects zero (continued-run zero best-times) - SkillLevel has no zero (unowned is absence, not Level=1) plan: drop signed NetCoins; add Attempt outcome as idempotency guard
38 lines
1.3 KiB
C#
38 lines
1.3 KiB
C#
namespace Mr.BrickAdventures.Progression;
|
|
|
|
/// <summary>
|
|
/// A non-negative quantity of coins. Amounts only — there is no signed coin value; a shortfall is
|
|
/// a failed subtraction the caller must handle, never a negative total.
|
|
/// </summary>
|
|
public readonly struct Coins : IEquatable<Coins>
|
|
{
|
|
public int Value { get; }
|
|
|
|
private Coins(int value) => Value = value;
|
|
|
|
public static Coins Of(int value) => value < 0
|
|
? throw new ArgumentOutOfRangeException(nameof(value), value, "Coins cannot be negative.")
|
|
: new Coins(value);
|
|
|
|
/// <summary>
|
|
/// Subtracts <paramref name="amount"/>. Returns false when the amount exceeds what is held,
|
|
/// leaving <paramref name="remaining"/> untouched rather than clamping to zero.
|
|
/// </summary>
|
|
public bool TrySubtract(Coins amount, out Coins remaining)
|
|
{
|
|
if (amount.Value > Value)
|
|
{
|
|
remaining = this;
|
|
return false;
|
|
}
|
|
|
|
remaining = new Coins(Value - amount.Value);
|
|
return true;
|
|
}
|
|
|
|
public bool Equals(Coins other) => Value == other.Value;
|
|
public override bool Equals(object? obj) => obj is Coins other && Equals(other);
|
|
public override int GetHashCode() => Value;
|
|
public override string ToString() => Value.ToString();
|
|
}
|