using System.Collections.Generic; using NUnit.Framework; // Тесты модуля Населения (этап 2). Запуск: Window -> General -> Test Runner -> EditMode -> Run All. // Цифры -- из дизайн-документа: стартовый город 300 человек (из них 30 воинов) ест 33 еды в час. public class PopulationTests { const long T0 = 1_800_000_000; // произвольный момент времени, секунды Unix const long Hour = 3600; FakeEconomy economy; FakeSettlement settlement; ArmyService army; [SetUp] public void SetUp() { GameServices.Clear(); EventBus.Clear(); SaveRegistry.Clear(); GameClock.ResetOffset(); GameClock.RealUnixSource = null; economy = new FakeEconomy(); settlement = new FakeSettlement { TownHallLevel = 1 }; army = new ArmyService(); GameServices.Register(economy); GameServices.Register(settlement); GameServices.Register(army); } [TearDown] public void TearDown() { GameClock.RealUnixSource = null; GameClock.ResetOffset(); } // Предел жилья с запасом -- для тестов прироста (по умолчанию 300: стартовый город упирается в предел) static PopulationRules Roomy() => new PopulationRules { HousingBase = 400 }; [Test] public void StartCity_AtDefaultHousingCap_DoesNotGrow_HouseLetsItGrow() { economy.Set(ResourceType.Food, 10000); var p = StartCity(); // решение команды от 19.10: 300 жителей при пределе 300 -- прирост стоит p.Tick(T0 + 10 * Hour); Assert.AreEqual(300, p.Total); p.SetHouseLevels(1); // построен Дом -- +50 мест p.Tick(T0 + 20 * Hour); Assert.AreEqual(325, p.Total, "построен Дом -- прирост идёт"); } // Стартовый город: 270 жителей + 30 воинов в Армии PopulationService StartCity(PopulationRules rules = null) { var p = new PopulationService(rules ?? new PopulationRules()); army.AddTroops(0, p.Rules.StartSoldiers); p.Tick(T0); // первый расчёт только запоминает время return p; } [Test] public void StartCity_Has300People_AndEats33FoodPerHour() { economy.Set(ResourceType.Food, 1000); var p = StartCity(); Assert.AreEqual(300, p.Total); Assert.AreEqual(270, p.Civilians); Assert.AreEqual(120, p.Workforce, "могут работать 120"); Assert.AreEqual(0, p.Workers, "рабочих расставляет игрок (или CitySync по зданиям)"); Assert.AreEqual(120, p.FreeWorkers); Assert.AreEqual(30, p.Soldiers); Assert.AreEqual(33f, p.FoodPerHour, 0.001f); p.Tick(T0 + Hour); Assert.AreEqual(1000 - 33, economy.Get(ResourceType.Food), 0.01, "за час город съел 33 еды"); } [Test] public void WithFood_GrowsTwoAndHalfPerHour() { economy.Set(ResourceType.Food, 10000); var p = StartCity(Roomy()); p.Tick(T0 + 10 * Hour); Assert.AreEqual(300 + 25, p.Total, "за 10 часов +25 человек"); Assert.IsFalse(p.IsStarving); } [Test] public void NoFood_GrowthStops_AndStarvationStarts() { economy.Set(ResourceType.Food, 0); var p = StartCity(); bool? starvingEvent = null; EventBus.Subscribe(e => starvingEvent = e.IsStarving); p.Tick(T0 + 10 * Hour); Assert.AreEqual(300, p.Total, "без еды прироста нет"); Assert.IsTrue(p.IsStarving); Assert.AreEqual(true, starvingEvent); Assert.AreEqual(0f, p.GrowthPerHour); Assert.AreEqual(0, economy.Get(ResourceType.Food), 0.0001); } [Test] public void FoodRunsOutMidway_EatsTheRest_ThenStarves() { economy.Set(ResourceType.Food, 50); // хватит на полтора часа var p = StartCity(); p.Tick(T0 + 3 * Hour); Assert.AreEqual(0, economy.Get(ResourceType.Food), 0.0001, "остатки доедены"); Assert.IsTrue(p.IsStarving); } [Test] public void FoodReturns_StarvationEnds_GrowthResumes() { economy.Set(ResourceType.Food, 0); var p = StartCity(Roomy()); p.Tick(T0 + Hour); Assert.IsTrue(p.IsStarving); economy.Set(ResourceType.Food, 10000); p.Tick(T0 + 5 * Hour); Assert.IsFalse(p.IsStarving); Assert.Greater(p.Total, 300); } [Test] public void HousingCap_StopsGrowth_HigherTownHall_ResumesIt() { economy.Set(ResourceType.Food, 100000); var p = StartCity(new PopulationRules { HousingBase = 300, HousingPerTownHallLevel = 100 }); p.Tick(T0 + 10 * Hour); Assert.AreEqual(300, p.Total, "300 жителей при пределе 300 -- прирост стоит"); Assert.AreEqual(0f, p.GrowthPerHour); settlement.TownHallLevel = 2; // предел 400 p.Tick(T0 + 20 * Hour); Assert.AreEqual(325, p.Total, "предел вырос -- прирост пошёл"); } [Test] public void Growth_NeverExceedsHousingCap() { economy.Set(ResourceType.Food, 100000); var p = StartCity(new PopulationRules { HousingBase = 305 }); p.Tick(T0 + 40 * Hour); Assert.AreEqual(305, p.Total); } [Test] public void Offline_IsCappedAtMaxHours() { economy.Set(ResourceType.Food, 100000); var p = StartCity(new PopulationRules { MaxOfflineHours = 48 }); p.Tick(T0 + 100 * Hour); double eaten = 100000 - economy.Get(ResourceType.Food); Assert.Greater(eaten, 33 * 48 - 1, "посчитано 48 часов"); Assert.Less(eaten, 33 * 60, "а не 100 (за 48 часов прибавилось 120 человек -- ели чуть больше 33 в час)"); Assert.AreEqual(T0 + 100 * Hour, p.LastTickUnix, "дальше считаем от 'сейчас'"); } [Test] public void ManySmallTicks_EqualOneBigTick() { economy.Set(ResourceType.Food, 10000); var a = StartCity(Roomy()); for (long t = T0 + 2; t <= T0 + 10 * Hour; t += 2) a.Tick(t); // как в игре: раз в 2 секунды float foodA = economy.Get(ResourceType.Food); int totalA = a.Total; SetUp(); economy.Set(ResourceType.Food, 10000); var b = StartCity(Roomy()); b.Tick(T0 + 10 * Hour); // как офлайн-прогресс Assert.AreEqual(totalA, b.Total); Assert.AreEqual(foodA, economy.Get(ResourceType.Food), 3.0, "игра и офлайн считают одинаково (разница -- доли часа, когда родился человек)"); } [Test] public void ClockGoesBack_NothingHappens_ThenContinues() { economy.Set(ResourceType.Food, 1000); var p = StartCity(); p.Tick(T0 - 5000); Assert.AreEqual(1000, economy.Get(ResourceType.Food), 0.0001, "время назад -- ничего не съедено"); Assert.AreEqual(300, p.Total); p.Tick(T0 - 5000 + Hour); Assert.AreEqual(1000 - 33, economy.Get(ResourceType.Food), 0.01); } [Test] public void DebugAdvance_GoesThroughTheSameRules() { long real = T0; GameClock.RealUnixSource = () => real; economy.Set(ResourceType.Food, 1000); var p = StartCity(); EventBus.Subscribe(e => p.Tick(e.UnixNow)); // как в App/GameModules GameClock.Advance(Hour); Assert.AreEqual(1000 - 33, economy.Get(ResourceType.Food), 0.01, "+1 час = час обычных правил"); Assert.AreEqual(Hour, GameClock.OffsetSeconds); GameClock.ResetOffset(); // сдвиг сброшен -- время "назад", ничего не ломается p.Tick(GameClock.UnixNow); Assert.AreEqual(1000 - 33, economy.Get(ResourceType.Food), 0.01); } [Test] public void SaveAndRestore_KeepsEverything() { economy.Set(ResourceType.Food, 10000); var p = StartCity(Roomy()); p.SetWorkers(WorkJob.Farmer, 40); p.SetHouseLevels(2); p.Tick(T0 + 3 * Hour); // +7 человек и доля восьмого string saved = p.Capture(); var q = new PopulationService(new PopulationRules()); q.Restore(saved); Assert.IsTrue(q.RestoredFromSave); Assert.AreEqual(p.Civilians, q.Civilians); Assert.AreEqual(p.Workers, q.Workers); Assert.AreEqual(40, q.GetWorkers(WorkJob.Farmer)); Assert.AreEqual(2, q.HouseLevels); Assert.AreEqual(p.LastTickUnix, q.LastTickUnix); } [Test] public void StartingKit_IsGivenOnce_AndNotToOldSaves() { var fresh = new PopulationService(new PopulationRules()); Assert.IsTrue(fresh.TryTakeStartingKit(), "новая игра -- выдать"); Assert.IsFalse(fresh.TryTakeStartingKit(), "второй раз -- нет"); var old = new PopulationService(new PopulationRules()); old.OnGameLoaded(T0); // старое сохранение без секции населения Assert.IsFalse(old.TryTakeStartingKit(), "старому игроку стартовый набор не выдаётся"); old.ResetState(); // сброс прогресса -- новая игра Assert.IsTrue(old.TryTakeStartingKit()); } [Test] public void Reset_ReturnsToStartCity() { economy.Set(ResourceType.Food, 10000); var p = StartCity(); p.Tick(T0 + 10 * Hour); p.ResetState(); Assert.AreEqual(270, p.Civilians); Assert.AreEqual(0, p.LastTickUnix); Assert.IsFalse(p.IsStarving); } // ---------------- Заглушки модулей ---------------- public class FakeEconomy : IEconomy { readonly Dictionary res = new Dictionary(); readonly Dictionary caps = new Dictionary(); public void Set(ResourceType t, float v) { res[t] = v; } public void SetCapacity(ResourceType t, float v) { caps[t] = v; } public float GetCapacity(ResourceType type) => caps.TryGetValue(type, out float c) ? c : float.PositiveInfinity; public float Get(ResourceType type) => res.TryGetValue(type, out float v) ? v : 0f; public bool Has(ResourceType type, float amount) => Get(type) >= amount; public void Add(ResourceType type, float amount) { float cur = Get(type), v = cur + amount; if (amount > 0) v = System.Math.Min(v, System.Math.Max(cur, GetCapacity(type))); // как ResourceManager res[type] = v < 0 ? 0 : v; } public bool TrySpend(ResourceType type, float amount) { if (!Has(type, amount)) return false; Add(type, -amount); return true; } public bool CanAfford(IReadOnlyList cost) { foreach (var c in cost) if (!Has(c.Type, c.Amount)) return false; return true; } public bool TrySpend(IReadOnlyList cost) { if (!CanAfford(cost)) return false; foreach (var c in cost) Add(c.Type, -c.Amount); return true; } } public class FakeSettlement : ISettlement { public int TownHallLevel { get; set; } public int BuildingCount => 0; public bool IsCityLoaded => true; } }