2026-10-27 18:04:27 +03:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
|
// Предел жилья с запасом -- для тестов прироста (по умолчанию 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, "построен Дом -- прирост идёт");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 18:04:27 +03:00
|
|
|
|
// Стартовый город: 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);
|
2026-10-31 19:34:33 +03:00
|
|
|
|
Assert.AreEqual(270, p.Workforce, "работать может любой житель -- общего ограничения нет (регламент 31.10)");
|
2026-10-27 18:51:35 +03:00
|
|
|
|
Assert.AreEqual(0, p.Workers, "рабочих расставляет игрок (или CitySync по зданиям)");
|
2026-10-31 19:34:33 +03:00
|
|
|
|
Assert.AreEqual(270, p.FreeWorkers);
|
2026-10-27 18:04:27 +03:00
|
|
|
|
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);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
var p = StartCity(Roomy());
|
2026-10-27 18:04:27 +03:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 22:11:36 +03:00
|
|
|
|
// ---------------- Найм воинов из жителей (решение команды от 19.10.2026) ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Recruit_150Free_Reserve30_Max120()
|
|
|
|
|
|
{
|
2026-10-31 19:34:33 +03:00
|
|
|
|
settlement.TownHallLevel = 6; // резерв 30 -- с 6 уровня Ратуши
|
2026-10-28 22:11:36 +03:00
|
|
|
|
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, "новобранцы не посчитаны дважды");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-31 20:52:28 +03:00
|
|
|
|
// ---------------- Группы людей не пересекаются (регламент 31.10) ----------------
|
|
|
|
|
|
|
|
|
|
|
|
static void AssertGroupsAddUp(PopulationService p, string when)
|
|
|
|
|
|
{
|
|
|
|
|
|
int free = p.Idle, workers = p.Workers, recruits = p.InTraining, soldiers = p.Soldiers;
|
|
|
|
|
|
Assert.AreEqual(p.Total, free + workers + recruits + soldiers, "свободные + рабочие + новобранцы + воины = все люди: " + when);
|
|
|
|
|
|
Assert.AreEqual(p.Civilians, free + workers, "жители = свободные + рабочие: " + when);
|
|
|
|
|
|
Assert.GreaterOrEqual(free, 0, when);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Groups_NeverOverlap_SumEqualsTotal()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
AssertGroupsAddUp(p, "старт");
|
|
|
|
|
|
|
|
|
|
|
|
p.SetWorkers(WorkJob.Farmer, 100);
|
|
|
|
|
|
AssertGroupsAddUp(p, "поставили 100 рабочих");
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(p.TryRecruit(150)); // берутся только свободные: 170 - резерв 10 = 160 доступно
|
|
|
|
|
|
Assert.AreEqual(100, p.Workers, "рабочих в новобранцы не взяли");
|
|
|
|
|
|
AssertGroupsAddUp(p, "наняли 150");
|
|
|
|
|
|
|
|
|
|
|
|
p.RecruitsTrained(150);
|
|
|
|
|
|
army.AddTroops(0, 150);
|
|
|
|
|
|
AssertGroupsAddUp(p, "новобранцы стали воинами");
|
|
|
|
|
|
Assert.AreEqual(300, p.Total, "людей не прибавилось и не убавилось");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Workers_CannotExceedFreeCivilians()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
p.SetWorkers(WorkJob.Farmer, 100);
|
|
|
|
|
|
p.TryRecruit(150); // жителей осталось 120: 100 рабочих + 20 свободных
|
|
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(20, p.SetWorkers(WorkJob.Woodcutter, 50), "поставить можно только 20 свободных -- не воинов и не новобранцев");
|
|
|
|
|
|
Assert.AreEqual(0, p.FreeWorkers);
|
|
|
|
|
|
Assert.AreEqual(0, p.SetWorkers(WorkJob.Stonecutter, 10), "свободных нет -- никого");
|
|
|
|
|
|
AssertGroupsAddUp(p, "все жители на работе");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Restore_WorkersNeverMoreThanCivilians()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
p.SetWorkers(WorkJob.Farmer, 120);
|
|
|
|
|
|
string save = p.Capture().Replace("civilians=270", "civilians=50"); // испорченное сохранение
|
|
|
|
|
|
var copy = new PopulationService(new PopulationRules());
|
|
|
|
|
|
copy.Restore(save);
|
|
|
|
|
|
Assert.AreEqual(50, copy.Workers, "рабочих не больше, чем жителей");
|
|
|
|
|
|
Assert.AreEqual(0, copy.Idle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Интеграция: новобранцы переходят из Населения в Армию (настоящий ArmyService) ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Training_RecruitsMoveToArmy_NoOneLostOrDuplicated()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
int infantryBefore = army.GetTroops(0);
|
|
|
|
|
|
Assert.IsTrue(p.TryRecruit(20));
|
|
|
|
|
|
AssertGroupsAddUp(p, "новобранцы в Казармах");
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(p.CompleteTraining(0, 20));
|
|
|
|
|
|
Assert.AreEqual(0, p.InTraining);
|
|
|
|
|
|
Assert.AreEqual(infantryBefore + 20, army.GetTroops(0), "в Армии +20 пехоты");
|
|
|
|
|
|
Assert.AreEqual(300, p.Total, "людей столько же");
|
|
|
|
|
|
AssertGroupsAddUp(p, "обучение закончено");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Training_ArmyRejects_RecruitsStayInBarracks()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
p.TryRecruit(20);
|
|
|
|
|
|
int soldiersBefore = p.Soldiers;
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsFalse(p.CompleteTraining(99, 20), "такого типа войск нет -- Армия не приняла");
|
|
|
|
|
|
Assert.AreEqual(20, p.InTraining, "новобранцы не пропали");
|
|
|
|
|
|
Assert.AreEqual(soldiersBefore, p.Soldiers, "и воинов из ничего не появилось");
|
|
|
|
|
|
Assert.AreEqual(300, p.Total);
|
|
|
|
|
|
AssertGroupsAddUp(p, "Армия не приняла");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Training_RepeatedCompletion_DoesNotDuplicate()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
p.TryRecruit(20);
|
|
|
|
|
|
Assert.IsTrue(p.CompleteTraining(0, 20));
|
|
|
|
|
|
Assert.IsFalse(p.CompleteTraining(0, 20), "второй раз новобранцев уже нет");
|
|
|
|
|
|
Assert.AreEqual(50, p.Soldiers, "30 стартовых + 20, а не 70");
|
|
|
|
|
|
Assert.AreEqual(300, p.Total);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Training_NoArmyModule_NothingChanges()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
p.TryRecruit(20);
|
|
|
|
|
|
GameServices.Unregister<IArmy>(army);
|
|
|
|
|
|
Assert.IsFalse(p.CompleteTraining(0, 20));
|
|
|
|
|
|
Assert.AreEqual(20, p.InTraining, "без Армии передавать некуда -- новобранцы ждут");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Training_PartOfRecruits_OthersKeepTraining()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
p.TryRecruit(20);
|
|
|
|
|
|
Assert.IsTrue(p.CompleteTraining(1, 8)); // 8 лучников готовы
|
|
|
|
|
|
Assert.AreEqual(12, p.InTraining);
|
|
|
|
|
|
Assert.AreEqual(8, army.GetTroops(1));
|
|
|
|
|
|
Assert.AreEqual(300, p.Total);
|
|
|
|
|
|
AssertGroupsAddUp(p, "часть обучена");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-31 19:34:33 +03:00
|
|
|
|
[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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 22:11:36 +03:00
|
|
|
|
[Test]
|
|
|
|
|
|
public void Recruit_TakesOnlyFree_WorkersStayAtWork()
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = StartCity();
|
|
|
|
|
|
p.SetWorkers(WorkJob.Farmer, 120);
|
2026-10-31 19:34:33 +03:00
|
|
|
|
p.TryRecruit(140);
|
2026-10-28 22:11:36 +03:00
|
|
|
|
Assert.AreEqual(120, p.Workers, "рабочие остались на работе");
|
2026-10-31 19:34:33 +03:00
|
|
|
|
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);
|
2026-10-28 22:11:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 18:04:27 +03:00
|
|
|
|
[Test]
|
|
|
|
|
|
public void FoodReturns_StarvationEnds_GrowthResumes()
|
|
|
|
|
|
{
|
|
|
|
|
|
economy.Set(ResourceType.Food, 0);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
var p = StartCity(Roomy());
|
2026-10-27 18:04:27 +03:00
|
|
|
|
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);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
var a = StartCity(Roomy());
|
2026-10-27 18:04:27 +03:00
|
|
|
|
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);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
var b = StartCity(Roomy());
|
2026-10-27 18:04:27 +03:00
|
|
|
|
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);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
var p = StartCity(Roomy());
|
|
|
|
|
|
p.SetWorkers(WorkJob.Farmer, 40);
|
|
|
|
|
|
p.SetHouseLevels(2);
|
2026-10-27 18:04:27 +03:00
|
|
|
|
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);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
Assert.AreEqual(40, q.GetWorkers(WorkJob.Farmer));
|
|
|
|
|
|
Assert.AreEqual(2, q.HouseLevels);
|
2026-10-27 18:04:27 +03:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Заглушки модулей ----------------
|
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
|
public class FakeEconomy : IEconomy
|
2026-10-27 18:04:27 +03:00
|
|
|
|
{
|
|
|
|
|
|
readonly Dictionary<ResourceType, float> res = new Dictionary<ResourceType, float>();
|
2026-10-27 18:51:35 +03:00
|
|
|
|
readonly Dictionary<ResourceType, float> caps = new Dictionary<ResourceType, float>();
|
2026-10-27 18:04:27 +03:00
|
|
|
|
public void Set(ResourceType t, float v) { res[t] = v; }
|
2026-10-27 18:51:35 +03:00
|
|
|
|
public void SetCapacity(ResourceType t, float v) { caps[t] = v; }
|
|
|
|
|
|
public float GetCapacity(ResourceType type) => caps.TryGetValue(type, out float c) ? c : float.PositiveInfinity;
|
2026-10-27 18:04:27 +03:00
|
|
|
|
public float Get(ResourceType type) => res.TryGetValue(type, out float v) ? v : 0f;
|
|
|
|
|
|
public bool Has(ResourceType type, float amount) => Get(type) >= amount;
|
2026-10-27 18:51:35 +03:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-27 18:04:27 +03:00
|
|
|
|
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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
|
public class FakeSettlement : ISettlement
|
2026-10-27 18:04:27 +03:00
|
|
|
|
{
|
|
|
|
|
|
public int TownHallLevel { get; set; }
|
|
|
|
|
|
public int BuildingCount => 0;
|
|
|
|
|
|
public bool IsCityLoaded => true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|