341 lines
16 KiB
C#
341 lines
16 KiB
C#
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_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 минут + конец");
|
||
}
|
||
}
|