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

298 lines
12 KiB
C#
Raw Normal View History

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(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<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);
}
[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;
}
}