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.
99 lines
3.7 KiB
C#
99 lines
3.7 KiB
C#
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);
|
|
}
|
|
}
|