feat(progression): Coins, LevelTime, SkillLevel core invariants (slice 1)
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
This commit is contained in:
37
progression/Coins.cs
Normal file
37
progression/Coins.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
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();
|
||||
}
|
||||
16
progression/LevelTime.cs
Normal file
16
progression/LevelTime.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Mr.BrickAdventures.Progression;
|
||||
|
||||
/// <summary>
|
||||
/// A completion time for one Level. Strictly positive — a zero time means the clock was never
|
||||
/// running, which is a defect rather than a fast run.
|
||||
/// </summary>
|
||||
public readonly struct LevelTime
|
||||
{
|
||||
public double Seconds { get; }
|
||||
|
||||
private LevelTime(double seconds) => Seconds = seconds;
|
||||
|
||||
public static LevelTime Of(double seconds) => seconds <= 0
|
||||
? throw new ArgumentOutOfRangeException(nameof(seconds), seconds, "A completion time must be positive.")
|
||||
: new LevelTime(seconds);
|
||||
}
|
||||
16
progression/SkillLevel.cs
Normal file
16
progression/SkillLevel.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Mr.BrickAdventures.Progression;
|
||||
|
||||
/// <summary>
|
||||
/// How far a Skill has been upgraded. Starts at one; there is no zero, because a Playthrough
|
||||
/// records only the Skills it owns and absence from that record is what "unowned" means.
|
||||
/// </summary>
|
||||
public readonly struct SkillLevel
|
||||
{
|
||||
public int Value { get; }
|
||||
|
||||
private SkillLevel(int value) => Value = value;
|
||||
|
||||
public static SkillLevel Of(int value) => value < 1
|
||||
? throw new ArgumentOutOfRangeException(nameof(value), value, "A skill level starts at 1; unowned is absence, not level 0.")
|
||||
: new SkillLevel(value);
|
||||
}
|
||||
Reference in New Issue
Block a user