using System.Collections.Generic; using NUnit.Framework; // Тесты добычи через рабочих (этап 3) и лимитов склада (этап 1). // Запуск: Window -> General -> Test Runner -> EditMode -> Run All. // Проверка из дизайн-документа: сбалансированный город даёт 90 еды, 50 дерева, 22.5 камня и 15 металла в час. public class ProductionTests { const long T0 = 1_800_000_000; const long Hour = 3600; PopulationTests.FakeEconomy economy; ArmyService army; PopulationService population; ProductionService production; [SetUp] public void SetUp() { GameServices.Clear(); EventBus.Clear(); SaveRegistry.Clear(); GameClock.ResetOffset(); economy = new PopulationTests.FakeEconomy(); army = new ArmyService(); population = new PopulationService(new PopulationRules()); production = new ProductionService(new ProductionRules()); GameServices.Register(economy); GameServices.Register(new PopulationTests.FakeSettlement { TownHallLevel = 1 }); GameServices.Register(army); GameServices.Register(population); GameServices.Register(production); army.AddTroops(0, 30); population.Tick(T0); } // По одному зданию 1 уровня на каждую профессию, кроме золота (нужно 2 фермы для 45 фермеров) static List> Buildings(params (WorkJob job, int level)[] list) { var result = new List>(); foreach (var b in list) result.Add(new KeyValuePair(b.job, b.level)); return result; } [Test] public void BalancedCity_Gives90Food_50Wood_22_5Stone_15Iron() { production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Farmer, 1), (WorkJob.Woodcutter, 1), (WorkJob.Stonecutter, 1), (WorkJob.MinerIron, 1))); population.SetWorkers(WorkJob.Farmer, 45); population.SetWorkers(WorkJob.Woodcutter, 25); population.SetWorkers(WorkJob.Stonecutter, 15); population.SetWorkers(WorkJob.MinerIron, 15); Assert.AreEqual(90f, production.RatePerHour(ResourceType.Food), 0.001f); Assert.AreEqual(50f, production.RatePerHour(ResourceType.Wood), 0.001f); Assert.AreEqual(22.5f, production.RatePerHour(ResourceType.Stone), 0.001f); Assert.AreEqual(15f, production.RatePerHour(ResourceType.Iron), 0.001f); Assert.AreEqual(100, population.Workers); Assert.AreEqual(170, population.FreeWorkers, "работать может любой из 270 жителей"); } [Test] public void OneHour_AddsProduction_AndFoodIsNetOfEating() { economy.Set(ResourceType.Food, 100); production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Farmer, 1), (WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Farmer, 45); population.SetWorkers(WorkJob.Woodcutter, 25); population.Tick(T0 + Hour); Assert.AreEqual(100 + 90 - 33, economy.Get(ResourceType.Food), 0.01, "+90 добыли, -33 съели"); Assert.AreEqual(50, economy.Get(ResourceType.Wood), 0.01); } [Test] public void ProductionComesBeforeEating_NoFalseStarvationOffline() { economy.Set(ResourceType.Food, 0); // склад пуст, но фермы кормят город production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Farmer, 1))); population.SetWorkers(WorkJob.Farmer, 45); population.Tick(T0 + 10 * Hour); Assert.IsFalse(population.IsStarving, "сначала добыли, потом съели"); Assert.AreEqual((90 - 33) * 10, economy.Get(ResourceType.Food), 1.0); } [Test] public void Workers_LimitedBySlots_AndByFreeWorkers() { production.SetBuildings(Buildings((WorkJob.Woodcutter, 1))); // 30 мест Assert.AreEqual(30, population.SetWorkers(WorkJob.Woodcutter, 50), "больше мест не поставить"); var farms = new List> { new KeyValuePair(WorkJob.Woodcutter, 1) }; for (int i = 0; i < 9; i++) farms.Add(new KeyValuePair(WorkJob.Farmer, 1)); // 270 мест production.SetBuildings(farms); Assert.AreEqual(240, population.SetWorkers(WorkJob.Farmer, 270), "свободных жителей осталось 240 из 270"); Assert.AreEqual(0, population.FreeWorkers); } [Test] public void NoBuilding_NoWorkers() { Assert.AreEqual(0, population.SetWorkers(WorkJob.MinerGold, 10), "нет Золотого рудника -- нет мест"); Assert.AreEqual(0f, production.RatePerHour(ResourceType.Gold)); } [Test] public void HigherLevel_MoreSlots_AndEfficiency() { production.SetBuildings(Buildings((WorkJob.Woodcutter, 3))); // 30 + 2×10 мест, норма ×1.2 Assert.AreEqual(50, production.Slots(WorkJob.Woodcutter)); Assert.AreEqual(1.2f, production.Efficiency(WorkJob.Woodcutter), 0.0001f); population.SetWorkers(WorkJob.Woodcutter, 50); Assert.AreEqual(50 * 2 * 1.2f, production.RatePerHour(ResourceType.Wood), 0.001f); } [Test] public void StorageCap_OverflowIsLost() { economy.SetCapacity(ResourceType.Wood, 1000); economy.Set(ResourceType.Wood, 990); production.SetBuildings(Buildings((WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Woodcutter, 30); // 60 в час population.Tick(T0 + Hour); Assert.AreEqual(1000, economy.Get(ResourceType.Wood), 0.0001, "склад полон -- лишнее пропало"); } [Test] public void AutoFillAll_SpreadsWorkersEvenly() { production.SetBuildings(Buildings((WorkJob.Woodcutter, 1), (WorkJob.Farmer, 1), (WorkJob.Stonecutter, 1))); population.AutoFillAll(); Assert.AreEqual(30, population.GetWorkers(WorkJob.Woodcutter)); Assert.AreEqual(30, population.GetWorkers(WorkJob.Farmer)); Assert.AreEqual(30, population.GetWorkers(WorkJob.Stonecutter)); Assert.AreEqual(180, population.FreeWorkers); Assert.IsTrue(population.JobsInitialized); } [Test] public void AutoFillAll_StartCity_NoMoreThan120Workers() { production.SetBuildings(Buildings((WorkJob.Woodcutter, 1), (WorkJob.Farmer, 1), (WorkJob.Stonecutter, 1), (WorkJob.MinerIron, 1), (WorkJob.MinerGold, 1))); // 150 мест population.AutoFillAll(); Assert.AreEqual(120, population.Workers, "стартовый город: 120 рабочих"); Assert.AreEqual(24, population.GetWorkers(WorkJob.Woodcutter), "поровну по профессиям"); } // ---------------- Голод (регламент 31.10, ТЕСТ): добыча кроме еды ×0.5 ---------------- [Test] public void Famine_HalvesNonFoodProduction_FoodUnaffected_PenaltyEndsWhenFed() { economy.Set(ResourceType.Food, 0); production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Farmer, 5); // 10 еды в час -- город ест 33, голод population.SetWorkers(WorkJob.Woodcutter, 30); // 60 дерева в час population.Tick(T0 + Hour); Assert.IsTrue(population.IsStarving); Assert.AreEqual(30f, production.RatePerHour(ResourceType.Wood), 0.001f, "дерево вдвое меньше"); Assert.AreEqual(10f, production.RatePerHour(ResourceType.Food), 0.001f, "еда без штрафа"); economy.Set(ResourceType.Food, 1000); // привезли еду population.Tick(T0 + 2 * Hour); Assert.IsFalse(population.IsStarving); Assert.AreEqual(60f, production.RatePerHour(ResourceType.Wood), 0.001f, "сыты -- штраф снят"); } [Test] public void NewBuilding_SlotsStayEmpty_UntilPlayerAssigns() { production.SetBuildings(Buildings((WorkJob.Farmer, 1))); population.AutoFillAll(); // стартовая расстановка -- один раз Assert.AreEqual(30, population.GetWorkers(WorkJob.Farmer)); production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Farmer, 1))); // построили вторую ферму population.ClampJobs(); Assert.AreEqual(60, production.Slots(WorkJob.Farmer)); Assert.AreEqual(30, population.GetWorkers(WorkJob.Farmer), "новые места пустые -- сами не заполняются"); Assert.AreEqual(60, population.SetWorkers(WorkJob.Farmer, 60), "игрок ставит сам"); } [Test] public void StartPlacement_HappensOnlyOnce() { production.SetBuildings(Buildings((WorkJob.Farmer, 1))); population.AutoFillAll(); Assert.IsTrue(population.JobsInitialized); population.SetWorkers(WorkJob.Farmer, 0); // игрок снял всех // Сохранение и загрузка: расстановка уже была, повторно не нужна var copy = new PopulationService(new PopulationRules()); copy.Restore(population.Capture()); Assert.IsTrue(copy.JobsInitialized, "второй раз CitySync расстановку не вызовет"); Assert.AreEqual(0, copy.GetWorkers(WorkJob.Farmer)); } [Test] public void FewerSlots_ClampJobs_RemovesExtraWorkers() { production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Farmer, 1))); population.SetWorkers(WorkJob.Farmer, 60); production.SetBuildings(Buildings((WorkJob.Farmer, 1))); population.ClampJobs(); Assert.AreEqual(30, population.GetWorkers(WorkJob.Farmer)); Assert.AreEqual(240, population.FreeWorkers); } [Test] public void ManySmallTicks_EqualOneBigTick_WithProduction() { economy.Set(ResourceType.Food, 500); production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Farmer, 30); population.SetWorkers(WorkJob.Woodcutter, 30); for (long t = T0 + 2; t <= T0 + 5 * Hour; t += 2) population.Tick(t); float foodA = economy.Get(ResourceType.Food), woodA = economy.Get(ResourceType.Wood); SetUp(); economy.Set(ResourceType.Food, 500); production.SetBuildings(Buildings((WorkJob.Farmer, 1), (WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Farmer, 30); population.SetWorkers(WorkJob.Woodcutter, 30); population.Tick(T0 + 5 * Hour); Assert.AreEqual(foodA, economy.Get(ResourceType.Food), 1.0); Assert.AreEqual(woodA, economy.Get(ResourceType.Wood), 1.0); Assert.AreEqual(300, woodA, 1.0, "30 лесорубов × 2 × 5 часов"); } [Test] public void SaveAndRestore_KeepsSlots() { production.SetBuildings(Buildings((WorkJob.Stonecutter, 2))); var copy = new ProductionService(new ProductionRules()); copy.Restore(production.Capture()); Assert.AreEqual(40, copy.Slots(WorkJob.Stonecutter)); Assert.AreEqual(1.1f, copy.Efficiency(WorkJob.Stonecutter), 0.0001f); } // ---------------- Офлайн по временным меткам (решения 04.11) ---------------- // Стройка, закончившаяся в офлайне, вступает в силу в момент расчёта, когда время дошло до её конца // (GameTimeSimulatedEvent). В игре это делает App/ConstructionClock; здесь -- подписка в тесте. void CompleteUpgradeAt(long atUnix, List> after) { bool done = false; EventBus.Subscribe(e => { if (done || e.UnixNow < atUnix) return; done = true; production.SetBuildings(after); }); } [Test] public void Offline_UpgradeFinishedMidway_ProducesNewRateOnlyFromCompletion() { economy.Set(ResourceType.Food, 100000); economy.Set(ResourceType.Wood, 0); production.SetBuildings(Buildings((WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Woodcutter, 30); // 60 дерева в час, на 2 уровне -- 66 CompleteUpgradeAt(T0 + 2 * Hour, Buildings((WorkJob.Woodcutter, 2))); population.Tick(T0 + 4 * Hour); // 4 часа офлайна, улучшение закончилось через 2 часа Assert.AreEqual(2 * 60 + 2 * 66, economy.Get(ResourceType.Wood), 0.01f, "2 часа по 60 + 2 часа по 66, а не все 4 часа по новой норме"); } [Test] public void Offline_FinishedInsideStep_CountsFromNextStep_NeverEarlier() { economy.Set(ResourceType.Food, 100000); economy.Set(ResourceType.Wood, 0); production.SetBuildings(Buildings((WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Woodcutter, 30); CompleteUpgradeAt(T0 + 2 * Hour + 5 * 60, Buildings((WorkJob.Woodcutter, 2))); // посреди 10-минутного шага population.Tick(T0 + 4 * Hour); float wood = economy.Get(ResourceType.Wood); Assert.LessOrEqual(wood, 125 / 60f * 60 + 115 / 60f * 66 + 0.01f, "не больше, чем если бы новая норма шла ровно с конца стройки"); Assert.AreEqual(130 / 60f * 60 + 110 / 60f * 66, wood, 0.01f, "с ближайшего шага расчёта после конца стройки"); } [Test] public void Tick_SameMomentTwice_NoExtraResources() { economy.Set(ResourceType.Food, 100000); production.SetBuildings(Buildings((WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Woodcutter, 30); population.Tick(T0 + Hour); float wood = economy.Get(ResourceType.Wood); population.Tick(T0 + Hour); population.Tick(T0 + Hour - 60); // часы ушли назад -- ничего не начисляется population.Tick(T0 + Hour); Assert.AreEqual(wood, economy.Get(ResourceType.Wood), 0.001f); } [Test] public void Restore_SameMoment_NoSecondCredit() { economy.Set(ResourceType.Food, 100000); production.SetBuildings(Buildings((WorkJob.Woodcutter, 1))); population.SetWorkers(WorkJob.Woodcutter, 30); population.Tick(T0 + Hour); float wood = economy.Get(ResourceType.Wood); var copy = new PopulationService(new PopulationRules()); copy.Restore(population.Capture()); // как загрузка сохранения: метка последнего расчёта та же GameServices.Register(copy); copy.Tick(T0 + Hour); Assert.AreEqual(wood, economy.Get(ResourceType.Wood), 0.001f, "загрузка не начисляет то же время второй раз"); copy.Tick(T0 + 2 * Hour); Assert.AreEqual(wood + 60, economy.Get(ResourceType.Wood), 0.01f, "а следующий час -- ровно один раз"); } [Test] public void SimulatedEvents_GoForwardStepByStep_EndAtNow() { var times = new List(); EventBus.Subscribe(e => times.Add(e.UnixNow)); population.Tick(T0 + Hour); Assert.AreEqual(T0 + Hour, times[times.Count - 1], "последнее событие -- момент Tick"); for (int i = 1; i < times.Count; i++) Assert.GreaterOrEqual(times[i], times[i - 1], "время расчёта не идёт назад"); Assert.AreEqual(7, times.Count, "6 шагов по 10 минут + конец"); } }