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); } }