Files
MVP1/Assets/Scripts/Modules/Tests/PopulationTests.cs
T

448 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<IEconomy>(economy);
GameServices.Register<ISettlement>(settlement);
GameServices.Register<IArmy>(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(270, p.Workforce, "работать может любой житель -- общего ограничения нет (регламент 31.10)");
Assert.AreEqual(0, p.Workers, "рабочих расставляет игрок (или CitySync по зданиям)");
Assert.AreEqual(270, 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<StarvationChangedEvent>(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);
}
// ---------------- Найм воинов из жителей (решение команды от 19.10.2026) ----------------
[Test]
public void Recruit_150Free_Reserve30_Max120()
{
settlement.TownHallLevel = 6; // резерв 30 -- с 6 уровня Ратуши
var p = StartCity();
p.SetWorkers(WorkJob.Farmer, 120); // 120 рабочих -- свободных 150
Assert.AreEqual(150, p.Idle);
Assert.AreEqual(30, p.CivilianReserve);
Assert.AreEqual(120, p.Recruitable, "пример команды: 150 свободных, резерв 30 -> 120");
Assert.IsFalse(p.TryRecruit(121));
Assert.IsTrue(p.TryRecruit(120));
Assert.AreEqual(0, p.Recruitable);
}
[Test]
public void Recruit_TakesCivilians_TotalStaysTheSame()
{
var p = StartCity();
Assert.IsTrue(p.TryRecruit(20));
Assert.AreEqual(250, p.Civilians);
Assert.AreEqual(20, p.InTraining);
Assert.AreEqual(300, p.Total, "житель стал новобранцем -- людей столько же");
p.RecruitsTrained(20);
army.AddTroops(0, 20); // Казармы выпускают воинов в Армию
Assert.AreEqual(0, p.InTraining);
Assert.AreEqual(50, p.Soldiers);
Assert.AreEqual(300, p.Total, "новобранцы не посчитаны дважды");
}
[Test]
public void Reserve_DependsOnTownHallLevel_10_20_30()
{
var p = StartCity();
int[] expected = { 10, 10, 20, 20, 20, 30, 30, 30, 30, 30, 30 }; // 11 уровень -- как последний
for (int level = 1; level <= expected.Length; level++)
{
settlement.TownHallLevel = level;
Assert.AreEqual(expected[level - 1], p.CivilianReserve, "Ратуша " + level);
}
}
[Test]
public void Recruit_StartCity_Reserve10()
{
var p = StartCity();
p.SetWorkers(WorkJob.Farmer, 120);
Assert.AreEqual(10, p.CivilianReserve, "Ратуша 1 -- резерв 10");
Assert.AreEqual(140, p.Recruitable, "150 свободных - 10");
}
[Test]
public void Recruit_TakesOnlyFree_WorkersStayAtWork()
{
var p = StartCity();
p.SetWorkers(WorkJob.Farmer, 120);
p.TryRecruit(140);
Assert.AreEqual(120, p.Workers, "рабочие остались на работе");
Assert.AreEqual(10, p.Idle, "остался резерв");
Assert.AreEqual(0, p.Recruitable);
}
[Test]
public void AutoFillAll_NewGame_Places120Workers_RestStayFree()
{
var p = StartCity();
p.AutoFillAll(); // без модуля добычи мест не меньше -- ограничивает только старт: 120 рабочих
Assert.AreEqual(120, p.Workers);
Assert.AreEqual(150, p.Idle);
}
[Test]
public void Recruits_EatLikeSoldiers_AndAreSaved()
{
var p = StartCity();
p.TryRecruit(10);
Assert.AreEqual(260 * 0.1 + (30 + 10) * 0.2, p.FoodPerHour, 0.001);
var copy = new PopulationService(new PopulationRules());
copy.Restore(p.Capture());
Assert.AreEqual(10, copy.InTraining);
Assert.AreEqual(260, copy.Civilians);
copy.ResetState();
Assert.AreEqual(0, copy.InTraining);
}
[Test]
public void OldTrainingOrder_WithoutRecruits_DoesNotGoNegative()
{
var p = StartCity();
p.RecruitsTrained(5); // заказ из старого сохранения -- людей тогда не брали
Assert.AreEqual(0, p.InTraining);
Assert.AreEqual(270, p.Civilians);
}
// ---------------- История голода (вопрос Саши 7): бой, прошедший офлайн, знает голод на свой момент ----------------
[Test]
public void FamineHistory_OfflineBattleSeesHungerAtItsMoment()
{
economy.Set(ResourceType.Food, 50); // 33 еды в час -- хватит примерно на полтора часа
var p = StartCity();
p.Tick(T0 + 5 * Hour); // игра была закрыта 5 часов
Assert.IsFalse(p.WasStarvingAt(T0 + Hour), "через час еда ещё была");
Assert.IsTrue(p.WasStarvingAt(T0 + 2 * Hour), "через два часа -- голод");
Assert.IsTrue(p.WasStarvingAt(T0 + 5 * Hour), "и сейчас голод");
economy.Set(ResourceType.Food, 1000); // привезли еду
p.Tick(T0 + 8 * Hour);
Assert.IsFalse(p.IsStarving);
Assert.IsTrue(p.WasStarvingAt(T0 + 3 * Hour), "прошлый голод помнится");
Assert.IsFalse(p.WasStarvingAt(T0 + 7 * Hour), "после еды голода нет");
}
[Test]
public void FamineHistory_SurvivesSave_AndIsClearedByReset()
{
economy.Set(ResourceType.Food, 50);
var p = StartCity();
p.Tick(T0 + 5 * Hour);
economy.Set(ResourceType.Food, 1000);
p.Tick(T0 + 8 * Hour);
var copy = new PopulationService(new PopulationRules());
copy.Restore(p.Capture());
Assert.IsTrue(copy.WasStarvingAt(T0 + 3 * Hour));
Assert.IsFalse(copy.WasStarvingAt(T0 + Hour));
copy.ResetState();
Assert.IsFalse(copy.WasStarvingAt(T0 + 3 * Hour), "новая игра -- истории нет");
}
[Test]
public void FamineHistory_OlderThan48Hours_IsForgotten()
{
economy.Set(ResourceType.Food, 50);
var p = StartCity();
p.Tick(T0 + 5 * Hour);
economy.Set(ResourceType.Food, 10000);
p.Tick(T0 + 8 * Hour);
p.Tick(T0 + 60 * Hour);
Assert.IsFalse(p.WasStarvingAt(T0 + 3 * Hour), "голод 57 часов назад бою уже не нужен");
Assert.IsFalse(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<GameTimeAdvancedEvent>(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<ResourceType, float> res = new Dictionary<ResourceType, float>();
readonly Dictionary<ResourceType, float> caps = new Dictionary<ResourceType, float>();
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<ResourceAmount> cost) { foreach (var c in cost) if (!Has(c.Type, c.Amount)) return false; return true; }
public bool TrySpend(IReadOnlyList<ResourceAmount> 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;
}
}