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

362 lines
17 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;
// Тесты добычи через рабочих (этап 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<IEconomy>(economy);
GameServices.Register<ISettlement>(new PopulationTests.FakeSettlement { TownHallLevel = 1 });
GameServices.Register<IArmy>(army);
GameServices.Register<IPopulation>(population);
GameServices.Register<IProduction>(production);
army.AddTroops(0, 30);
population.Tick(T0);
}
// По одному зданию 1 уровня на каждую профессию, кроме золота (нужно 2 фермы для 45 фермеров)
static List<KeyValuePair<WorkJob, int>> Buildings(params (WorkJob job, int level)[] list)
{
var result = new List<KeyValuePair<WorkJob, int>>();
foreach (var b in list) result.Add(new KeyValuePair<WorkJob, int>(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<KeyValuePair<WorkJob, int>> { new KeyValuePair<WorkJob, int>(WorkJob.Woodcutter, 1) };
for (int i = 0; i < 9; i++) farms.Add(new KeyValuePair<WorkJob, int>(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<KeyValuePair<WorkJob, int>> after)
{
bool done = false;
EventBus.Subscribe<GameTimeSimulatedEvent>(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_ClockWentBack_StillReportsSimulatedTime_NoResources()
{
// «+1 час», потом «Сбросить перемотку»: часы ушли назад. Стройка, у которой время вышло, должна завершиться
// (событие о расчёте приходит), а ресурсы за уже посчитанный час второй раз не начисляются (05.11)
economy.Set(ResourceType.Food, 100000);
economy.Set(ResourceType.Wood, 0);
production.SetBuildings(Buildings((WorkJob.Woodcutter, 1)));
population.SetWorkers(WorkJob.Woodcutter, 30);
population.Tick(T0);
population.Tick(T0 + Hour);
float wood = economy.Get(ResourceType.Wood);
long reported = 0;
EventBus.Subscribe<GameTimeSimulatedEvent>(e => reported = e.UnixNow);
population.Tick(T0 + 60);
Assert.AreEqual(T0 + 60, reported, "расчёт сообщает, до какого момента дошло время -- стройки завершаются");
Assert.AreEqual(wood, economy.Get(ResourceType.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<IPopulation>(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<long>();
EventBus.Subscribe<GameTimeSimulatedEvent>(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 минут + конец");
}
}