using System.Collections.Generic; using NUnit.Framework; using UnityEngine; using YachtDice.Categories; using YachtDice.Dice; using YachtDice.Economy; using YachtDice.Run; using YachtDice.Scoring; namespace YachtDice.Tests { public sealed class RunLoopServiceTests { private readonly List _createdAssets = new(); private readonly IReadOnlyList _emptyDice = new IDice[0]; private CurrencyBank _currencyBank; private ScoringSystem _scoringSystem; private StoredRollBank _storedRollBank; private RunBalanceConfigSO _config; [SetUp] public void SetUp() { var bankGo = new GameObject("CurrencyBank"); _currencyBank = bankGo.AddComponent(); _currencyBank.SetBalance(0); var scoringGo = new GameObject("ScoringSystem"); _scoringSystem = scoringGo.AddComponent(); _storedRollBank = new StoredRollBank(); _config = RunBalanceConfigSO.CreateDefault(); _createdAssets.Add(_config); } [TearDown] public void TearDown() { foreach (var scoring in Object.FindObjectsByType(FindObjectsSortMode.None)) Object.DestroyImmediate(scoring.gameObject); foreach (var bank in Object.FindObjectsByType(FindObjectsSortMode.None)) Object.DestroyImmediate(bank.gameObject); for (var i = 0; i < _createdAssets.Count; i++) { if (_createdAssets[i] != null) Object.DestroyImmediate(_createdAssets[i]); } _createdAssets.Clear(); } [Test] public void StartNewRun_StartsFirstStageWithoutShop() { var categories = CreateFixedCatalog(100, 100, 100); var service = CreateService(categories); service.StartNewRun(); Assert.AreEqual(30, service.State.BaseQuota); Assert.AreEqual(1, service.State.BetIndex); Assert.AreEqual(RunPhase.StageStart, service.State.Phase); Assert.AreEqual(0, service.State.StoredRolls); Assert.AreEqual(0, _currencyBank.Balance); Assert.IsFalse(service.State.IsShopAvailable); } [Test] public void ClearedStage_AwardsCurrencyAndBanksUnusedRolls() { var categories = CreateFixedCatalog(100, 100, 100); var service = CreateService(categories); service.StartNewRun(); service.TryBeginRoll(); service.NotifyRollResolved(_emptyDice); var cleared = service.TryScoreCategoryAsync(_emptyDice, categories.All[0]).GetAwaiter().GetResult(); Assert.IsTrue(cleared); Assert.AreEqual(25, _currencyBank.Balance); Assert.AreEqual(2, service.State.StoredRolls); Assert.AreEqual(2, service.State.StageNumber); Assert.AreEqual(RunPhase.StageStart, service.State.Phase); Assert.IsTrue(service.State.IsShopAvailable); } [Test] public void CategoryCanBeScored_WithZeroResultAfterFirstRoll() { var categories = CreateFixedCatalog(0, 100, 100); var service = CreateService(categories); service.StartNewRun(); service.TryBeginRoll(); service.NotifyRollResolved(_emptyDice); Assert.IsTrue(service.CanScoreCategory(_emptyDice, categories.All[0])); Assert.IsTrue(service.TryScoreCategoryAsync(_emptyDice, categories.All[0]).GetAwaiter().GetResult()); Assert.AreEqual(30, service.State.CurrentStageTarget); Assert.AreEqual(1, service.State.StageNumber); Assert.AreEqual(RunPhase.StageStart, service.State.Phase); } [Test] public void TargetDecreasesByRecordedScore_AndStageContinues() { var categories = CreateFixedCatalog(5, 100, 100); var service = CreateService(categories); service.StartNewRun(); service.TryBeginRoll(); service.NotifyRollResolved(_emptyDice); Assert.IsTrue(service.TryScoreCategoryAsync(_emptyDice, categories.All[0]).GetAwaiter().GetResult()); Assert.AreEqual(25, service.State.CurrentStageTarget); Assert.AreEqual(1, service.State.StageNumber); Assert.AreEqual(RunPhase.StageStart, service.State.Phase); } [Test] public void StoredRolls_CanBeConsumedOnLaterStage() { var categories = CreateFixedCatalog(100, 100, 100); var service = CreateService(categories); service.StartNewRun(); service.TryBeginRoll(); service.NotifyRollResolved(_emptyDice); service.TryScoreCategoryAsync(_emptyDice, categories.All[0]).GetAwaiter().GetResult(); for (var i = 0; i < 4; i++) { Assert.IsTrue(service.TryBeginRoll()); service.NotifyRollResolved(_emptyDice); } service.TryScoreCategoryAsync(_emptyDice, categories.All[1]).GetAwaiter().GetResult(); Assert.AreEqual(1, service.State.StoredRolls); Assert.AreEqual(50, _currencyBank.Balance); } [Test] public void CompletedCycle_GrantsBonusRaisesQuotaAndStartsNextBet() { var categories = CreateFixedCatalog(100, 100, 100); var service = CreateService(categories); service.StartNewRun(); ClearStageInOneRoll(service, categories.All[0]); ClearStageInOneRoll(service, categories.All[1]); ClearStageInOneRoll(service, categories.All[2]); Assert.AreEqual(135, _currencyBank.Balance); Assert.AreEqual(60, service.State.BaseQuota); Assert.AreEqual(2, service.State.BetIndex); Assert.AreEqual(RunPhase.StageStart, service.State.Phase); Assert.AreEqual(6, service.State.StoredRolls); Assert.AreEqual(0, _scoringSystem.CategoriesFilledCount); Assert.IsTrue(service.State.IsShopAvailable); } [Test] public void FailedStage_EndsRunWhenNoTargetCanBeMet() { var categories = CreateFixedCatalog(10); var service = CreateService(categories); service.StartNewRun(); Assert.IsTrue(service.TryBeginRoll()); service.NotifyRollResolved(_emptyDice); Assert.IsFalse(service.State.IsFailed); Assert.AreEqual(RunPhase.CategorySelection, service.State.Phase); } [Test] public void ShopUnlocksAfterFirstClear_AndClosesAfterFirstRoll() { var categories = CreateFixedCatalog(100, 100, 100, 100); var service = CreateService(categories); service.StartNewRun(); Assert.IsFalse(service.State.IsShopAvailable); ClearStageInOneRoll(service, categories.All[0]); Assert.IsTrue(service.State.IsShopAvailable); Assert.AreEqual(RunPhase.StageStart, service.State.Phase); Assert.IsTrue(service.TryBeginRoll()); Assert.IsFalse(service.State.IsShopAvailable); service.NotifyRollResolved(_emptyDice); Assert.IsTrue(service.TryScoreCategoryAsync(_emptyDice, categories.All[1]).GetAwaiter().GetResult()); Assert.IsTrue(service.State.IsShopAvailable); ClearStageInOneRoll(service, categories.All[2]); Assert.AreEqual(RunPhase.StageStart, service.State.Phase); Assert.AreEqual(2, service.State.BetIndex); Assert.IsTrue(service.State.IsShopAvailable); } [Test] public void CanBeginRoll_BecomesFalse_WhenNoRollsOrStoredRollsRemain() { var categories = CreateFixedCatalog(100, 100, 100, 100); var service = CreateService(categories); service.StartNewRun(); Assert.IsTrue(service.TryBeginRoll()); service.NotifyRollResolved(_emptyDice); Assert.IsTrue(service.TryBeginRoll()); service.NotifyRollResolved(_emptyDice); Assert.IsTrue(service.TryBeginRoll()); service.NotifyRollResolved(_emptyDice); Assert.IsFalse(service.CanBeginRoll()); Assert.AreEqual(RunPhase.CategorySelection, service.State.Phase); } private RunLoopService CreateService(CategoryCatalog catalog) { _scoringSystem.Construct(null, null, catalog, _currencyBank); return new RunLoopService(_config, _scoringSystem, _currencyBank, _storedRollBank); } private CategoryCatalog CreateFixedCatalog(params int[] scores) { var categories = new List(); for (var i = 0; i < scores.Length; i++) { var category = FixedScoreCategory.Create($"cat_{i}", scores[i]); _createdAssets.Add(category); categories.Add(category); } var catalog = CategoryCatalog.CreateForTest(categories); _createdAssets.Add(catalog); return catalog; } private void ClearStageInOneRoll(RunLoopService service, CategoryDefinition category) { Assert.IsTrue(service.TryBeginRoll()); service.NotifyRollResolved(_emptyDice); Assert.IsTrue(service.TryScoreCategoryAsync(_emptyDice, category).GetAwaiter().GetResult()); } private sealed class FixedScoreCategory : CategoryDefinition { [SerializeField] private int fixedScore; public override int Calculate(IReadOnlyList dice) { return fixedScore; } public static FixedScoreCategory Create(string id, int score) { var category = CreateInstance(); category.SetTestData(id, id); category.fixedScore = score; return category; } } } }