feat(progression): Attempt ledger with terminal-outcome guard (slice 2)
Attempt carries its LevelId so settlement decides First Clear vs Replay without trusting the caller. Outcome is terminal and refuses to change: one death costs one life, one completion banks once. Coins.Zero/Add driven out by the ledger rather than added up front. 21 tests. ADR-0005: Playthrough settles Attempts, has no LoseLife.
This commit is contained in:
98
progression.tests/AttemptTests.cs
Normal file
98
progression.tests/AttemptTests.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
namespace Mr.BrickAdventures.Progression.Tests;
|
||||
|
||||
public class AttemptTests
|
||||
{
|
||||
[Fact]
|
||||
public void An_attempt_cannot_die_twice()
|
||||
{
|
||||
// Overlapping hazards in a kill zone, or a hazard re-firing during the two seconds before
|
||||
// DeathScreen reloads the scene, must not cost two lives. The Attempt refuses the second
|
||||
// death, so no caller has to remember to guard against it.
|
||||
var attempt = new Attempt(LevelId.Of("village_1"));
|
||||
|
||||
Assert.True(attempt.TryEndInDeath());
|
||||
Assert.False(attempt.TryEndInDeath());
|
||||
Assert.Equal(AttemptOutcome.Died, attempt.Outcome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_attempt_that_died_cannot_then_be_cleared()
|
||||
{
|
||||
var attempt = new Attempt(LevelId.Of("village_1"));
|
||||
attempt.TryEndInDeath();
|
||||
|
||||
Assert.False(attempt.TryEndInCompletion());
|
||||
Assert.Equal(AttemptOutcome.Died, attempt.Outcome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_attempt_that_was_cleared_cannot_then_die()
|
||||
{
|
||||
// The reverse of the above. Together these say a terminal outcome is final, which is what
|
||||
// lets both ExitDoorComponent and the console request completion without banking twice.
|
||||
var attempt = new Attempt(LevelId.Of("village_1"));
|
||||
attempt.TryEndInCompletion();
|
||||
|
||||
Assert.False(attempt.TryEndInDeath());
|
||||
Assert.Equal(AttemptOutcome.Cleared, attempt.Outcome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Coins_found_after_the_attempt_ended_do_not_count()
|
||||
{
|
||||
// The player node survives for two seconds after death while DeathScreen counts down, so a
|
||||
// coin's Area2D can still fire. That is a real event rather than an error, and it must not
|
||||
// enlarge a ledger that is already settled.
|
||||
var attempt = new Attempt(LevelId.Of("village_1"));
|
||||
attempt.RecordCoinsFound(Coins.Of(3));
|
||||
attempt.RecordCoinsFound(Coins.Of(2));
|
||||
attempt.TryEndInDeath();
|
||||
|
||||
attempt.RecordCoinsFound(Coins.Of(100));
|
||||
|
||||
Assert.Equal(Coins.Of(5), attempt.CoinsFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_purchase_after_the_attempt_ended_is_refused()
|
||||
{
|
||||
var attempt = new Attempt(LevelId.Of("village_1"));
|
||||
attempt.TryEndInDeath();
|
||||
|
||||
var recorded = attempt.TryRecordPurchase(SkillId.Of("brick_throw"), SkillLevel.Of(1), Coins.Of(10));
|
||||
|
||||
Assert.False(recorded);
|
||||
Assert.Equal(Coins.Zero, attempt.CoinsSpent);
|
||||
Assert.Empty(attempt.SkillsBought);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spending_more_than_was_found_this_attempt_is_recorded_not_rejected()
|
||||
{
|
||||
// The shortfall is drawn from the Bank when the Attempt is committed. Rejecting it here
|
||||
// would make the ledger enforce a rule it cannot see — affordability spans both scopes.
|
||||
var attempt = new Attempt(LevelId.Of("village_1"));
|
||||
attempt.RecordCoinsFound(Coins.Of(10));
|
||||
|
||||
var recorded = attempt.TryRecordPurchase(SkillId.Of("magnetic"), SkillLevel.Of(1), Coins.Of(30));
|
||||
|
||||
Assert.True(recorded);
|
||||
Assert.Equal(Coins.Of(10), attempt.CoinsFound);
|
||||
Assert.Equal(Coins.Of(30), attempt.CoinsSpent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unlocking_then_upgrading_one_skill_records_the_level_it_ended_at()
|
||||
{
|
||||
// Both happen in the marketplace mid-Attempt. Commit applies one level per skill, so the
|
||||
// ledger keeps where it ended up, while the costs accumulate.
|
||||
var brickThrow = SkillId.Of("brick_throw");
|
||||
var attempt = new Attempt(LevelId.Of("village_1"));
|
||||
|
||||
attempt.TryRecordPurchase(brickThrow, SkillLevel.Of(1), Coins.Of(10));
|
||||
attempt.TryRecordPurchase(brickThrow, SkillLevel.Of(2), Coins.Of(25));
|
||||
|
||||
Assert.Equal(SkillLevel.Of(2), attempt.SkillsBought[brickThrow]);
|
||||
Assert.Equal(Coins.Of(35), attempt.CoinsSpent);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user