2026-10-27 18:51:35 +03:00
|
|
|
|
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);
|
2026-10-31 19:34:33 +03:00
|
|
|
|
Assert.AreEqual(170, population.FreeWorkers, "работать может любой из 270 жителей");
|
2026-10-27 18:51:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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), "больше мест не поставить");
|
|
|
|
|
|
|
2026-10-31 19:34:33 +03:00
|
|
|
|
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");
|
2026-10-27 18:51:35 +03:00
|
|
|
|
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));
|
2026-10-31 19:34:33 +03:00
|
|
|
|
Assert.AreEqual(180, population.FreeWorkers);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
Assert.IsTrue(population.JobsInitialized);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-31 19:34:33 +03:00
|
|
|
|
[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, "сыты -- штраф снят");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
|
[Test]
|
2026-10-31 20:52:28 +03:00
|
|
|
|
public void NewBuilding_SlotsStayEmpty_UntilPlayerAssigns()
|
2026-10-27 18:51:35 +03:00
|
|
|
|
{
|
|
|
|
|
|
production.SetBuildings(Buildings((WorkJob.Farmer, 1)));
|
2026-10-31 20:52:28 +03:00
|
|
|
|
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));
|
2026-10-27 18:51:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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));
|
2026-10-31 19:34:33 +03:00
|
|
|
|
Assert.AreEqual(240, population.FreeWorkers);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
|
}
|
2026-11-04 23:36:14 +03:00
|
|
|
|
|
|
|
|
|
|
// ---------------- Офлайн по временным меткам (решения 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, "с ближайшего шага расчёта после конца стройки");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 17:45:12 +01:00
|
|
|
|
[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, "за уже посчитанное время ресурсов не прибавилось");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 23:36:14 +03:00
|
|
|
|
[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 минут + конец");
|
|
|
|
|
|
}
|
2026-10-27 18:51:35 +03:00
|
|
|
|
}
|
2026-11-05 18:29:31 +01:00
|
|
|
|
|
|
|
|
|
|
// Синие кристаллы при пределе 50 (Регламент редкости синих кристаллов от 05.11.2026, ред. 1.1, раздел 6)
|
|
|
|
|
|
public class CrystalRewardsTests
|
|
|
|
|
|
{
|
|
|
|
|
|
const float Cap = 50f;
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Crystals_49of50_Reward2_OneGiven_OneWaits()
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = new CrystalRewards();
|
|
|
|
|
|
Assert.AreEqual(1, r.Claim("q1", 2, 49, Cap), "49/50 + 2 -> выдан 1");
|
|
|
|
|
|
Assert.AreEqual(1, r.Left("q1"), "1 ждёт места");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Crystals_Full_OldReward15_NothingGiven_NothingLost()
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = new CrystalRewards();
|
|
|
|
|
|
Assert.AreEqual(0, r.Claim("old", 15, 50, Cap), "50/50 + 15 из старых данных -> 0 выдано");
|
|
|
|
|
|
Assert.AreEqual(15, r.Left("old"), "награда ждёт целиком, ничего не исчезло");
|
|
|
|
|
|
Assert.AreEqual(10, r.Claim("old", 15, 40, Cap), "освободили 10 -- выдано 10");
|
|
|
|
|
|
Assert.AreEqual(5, r.Left("old"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Crystals_RepeatClaim_NeverGivesTwice()
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = new CrystalRewards();
|
|
|
|
|
|
r.Claim("q", 2, 49, Cap); // выдан 1, ждёт 1
|
|
|
|
|
|
Assert.AreEqual(0, r.Claim("q", 2, 50, Cap), "повторное «Забрать» при 50/50 -- ничего");
|
|
|
|
|
|
Assert.AreEqual(1, r.Left("q"), "остаток не вырос: награда одна");
|
|
|
|
|
|
Assert.AreEqual(1, r.Claim("q", 2, 30, Cap), "место есть -- выдан только остаток 1, а не 2");
|
|
|
|
|
|
Assert.AreEqual(0, r.Left("q"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Crystals_AtMostTwoWaiting_ThirdNotStarted_FittingAlwaysOk()
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = new CrystalRewards();
|
|
|
|
|
|
r.Claim("a", 2, 50, Cap);
|
|
|
|
|
|
r.Claim("b", 2, 50, Cap);
|
|
|
|
|
|
Assert.AreEqual(2, r.WaitingCount);
|
|
|
|
|
|
Assert.IsFalse(r.CanStart("c", 2, 50, Cap), "третья награда при 50/50 не выдаётся -- задание остаётся «Забрать»");
|
|
|
|
|
|
Assert.IsTrue(r.CanStart("a", 2, 50, Cap), "ждущую можно дозабирать");
|
|
|
|
|
|
Assert.IsTrue(r.CanStart("c", 2, 40, Cap), "помещается целиком -- место ожидания не нужно");
|
|
|
|
|
|
Assert.IsTrue(r.CanStart("d", 0, 50, Cap), "награда без кристаллов -- всегда");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Crystals_Waiting_SurvivesSaveAndLoad()
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = new CrystalRewards();
|
|
|
|
|
|
r.Claim("m:m_th_3", 2, 50, Cap);
|
|
|
|
|
|
r.Claim("mail_7", 5, 48, Cap);
|
|
|
|
|
|
var copy = new CrystalRewards();
|
|
|
|
|
|
copy.Load(r.ToLines());
|
|
|
|
|
|
Assert.AreEqual(2, copy.Left("m:m_th_3"));
|
|
|
|
|
|
Assert.AreEqual(3, copy.Left("mail_7"));
|
|
|
|
|
|
Assert.AreEqual(2, copy.WaitingCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Crystals_NeverAboveCap()
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.AreEqual(0, CrystalRewards.Free(50, Cap));
|
|
|
|
|
|
Assert.AreEqual(0, CrystalRewards.Free(51, Cap), "выше предела (старые данные) -- места нет, а не минус");
|
|
|
|
|
|
Assert.AreEqual(3, CrystalRewards.Fits(10, 47, Cap));
|
|
|
|
|
|
Assert.AreEqual(10, CrystalRewards.Fits(10, 0, float.PositiveInfinity), "без предела -- целиком");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-06 16:13:43 +01:00
|
|
|
|
|
|
|
|
|
|
// Хранилище синих кристаллов: платное расширение по уровню Ратуши (регламент валют 05.11, разд. 8 и 12)
|
|
|
|
|
|
public class CrystalStorageTests
|
|
|
|
|
|
{
|
|
|
|
|
|
static CrystalStorage Storage() => new CrystalStorage(50, new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new CrystalStorage.Tier { TownHallLevel = 3, Capacity = 75, Cost = new[] { new ResourceAmount(ResourceType.Gold, 2000) } },
|
|
|
|
|
|
new CrystalStorage.Tier { TownHallLevel = 5, Capacity = 100, Cost = new[] { new ResourceAmount(ResourceType.Gold, 4000) } },
|
|
|
|
|
|
new CrystalStorage.Tier { TownHallLevel = 7, Capacity = 150, Cost = new[] { new ResourceAmount(ResourceType.Gold, 7000) } },
|
|
|
|
|
|
new CrystalStorage.Tier { TownHallLevel = 9, Capacity = 200, Cost = new[] { new ResourceAmount(ResourceType.Gold, 10000) } },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Storage_NotAutomatic_TownHallOnlyAllows()
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = Storage();
|
|
|
|
|
|
Assert.AreEqual(50, s.Capacity, "старт 50");
|
|
|
|
|
|
int spent = 0;
|
|
|
|
|
|
Assert.IsFalse(s.TryExpand(2, c => { spent++; return true; }), "Ратуша 2 -- расширения нет");
|
|
|
|
|
|
Assert.AreEqual(0, spent, "ничего не списано");
|
|
|
|
|
|
Assert.AreEqual(50, s.Capacity, "Ратуша 3 сама вместимость не меняет -- только разрешает");
|
|
|
|
|
|
Assert.IsNull(s.WhyNot(3));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Storage_50to75_PaidOnce_AndKeptAfterSaveLoad()
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = Storage();
|
|
|
|
|
|
float gold = 2500;
|
|
|
|
|
|
System.Func<IReadOnlyList<ResourceAmount>, bool> pay = cost =>
|
|
|
|
|
|
{
|
|
|
|
|
|
float need = 0;
|
|
|
|
|
|
foreach (var c in cost) if (c.Type == ResourceType.Gold) need += c.Amount;
|
|
|
|
|
|
if (gold < need) return false;
|
|
|
|
|
|
gold -= need;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
Assert.IsTrue(s.TryExpand(3, pay));
|
|
|
|
|
|
Assert.AreEqual(75, s.Capacity, "50 -> 75 после оплаты");
|
|
|
|
|
|
Assert.AreEqual(500, gold, 0.01, "списано один раз");
|
|
|
|
|
|
Assert.IsFalse(s.TryExpand(3, pay), "повторное нажатие на Ратуше 3 -- следующая ступень ещё закрыта");
|
|
|
|
|
|
Assert.AreEqual(500, gold, 0.01, "второй раз не списано");
|
|
|
|
|
|
|
|
|
|
|
|
var loaded = Storage();
|
|
|
|
|
|
loaded.SetLevel(s.Level);
|
|
|
|
|
|
Assert.AreEqual(75, loaded.Capacity, "после сохранения и загрузки -- 75");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Storage_NotEnoughResources_NothingChanges()
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = Storage();
|
|
|
|
|
|
Assert.IsFalse(s.TryExpand(9, c => false), "не хватило -- списание не прошло");
|
|
|
|
|
|
Assert.AreEqual(0, s.Level);
|
|
|
|
|
|
Assert.AreEqual(50, s.Capacity);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Storage_TiersInOrder_UpTo200()
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = Storage();
|
|
|
|
|
|
for (int i = 0; i < 4; i++) Assert.IsTrue(s.TryExpand(10, c => true));
|
|
|
|
|
|
Assert.AreEqual(200, s.Capacity);
|
|
|
|
|
|
Assert.IsNull(s.Next);
|
|
|
|
|
|
Assert.IsFalse(s.TryExpand(10, c => true), "дальше 200 -- нет");
|
|
|
|
|
|
s.SetLevel(99);
|
|
|
|
|
|
Assert.AreEqual(4, s.Level, "лишние ступени из сохранения обрезаются");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Storage_Waiting_ClaimedOnceAfterExpansion()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 49/50 + допустимая награда 2 -> 1 сразу и 1 ждёт; после расширения ждущий забирается один раз
|
|
|
|
|
|
var s = Storage();
|
|
|
|
|
|
var r = new CrystalRewards();
|
|
|
|
|
|
float have = 49;
|
|
|
|
|
|
have += r.Claim("q", 2, have, s.Capacity);
|
|
|
|
|
|
Assert.AreEqual(50, have, 0.01);
|
|
|
|
|
|
Assert.AreEqual(1, r.Left("q"));
|
|
|
|
|
|
s.TryExpand(3, c => true);
|
|
|
|
|
|
have += r.Claim("q", 2, have, s.Capacity);
|
|
|
|
|
|
Assert.AreEqual(51, have, 0.01, "после расширения выдан ждущий 1");
|
|
|
|
|
|
Assert.AreEqual(0, r.Left("q"), "больше ничего не ждёт -- выдан ровно один раз");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-06 16:24:16 +01:00
|
|
|
|
|
|
|
|
|
|
// Недельный PvE-предел синих кристаллов (регламент валют 05.11, разд. 5.2, 10, 12; ответ 06.11, п. 2)
|
|
|
|
|
|
public class CrystalQuotaTests
|
|
|
|
|
|
{
|
|
|
|
|
|
const long T0 = 1_800_000_000_000L;
|
|
|
|
|
|
const long Day = CrystalQuota.Day;
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Quota_5of6_Reward2_OneCrystal_OneCompensated()
|
|
|
|
|
|
{
|
|
|
|
|
|
var q = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
Assert.AreEqual(5, q.Take("a", 5, T0));
|
|
|
|
|
|
Assert.AreEqual(1, q.Take("b", 2, T0 + 1000), "5/6 + 2 -> 1 кристалл (второй -- компенсацией)");
|
|
|
|
|
|
Assert.AreEqual(0, q.Left(T0 + 1000));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Quota_6of6_Purchase_Blocked()
|
|
|
|
|
|
{
|
|
|
|
|
|
var q = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
q.Take("a", 6, T0);
|
|
|
|
|
|
Assert.IsFalse(q.CanTakeAll(1, T0 + 1), "6/6 -- покупка у торговца блокируется (золото не списывается)");
|
|
|
|
|
|
Assert.AreEqual(0, q.Take("b", 1, T0 + 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Quota_SameOperationTwice_CountedOnce()
|
|
|
|
|
|
{
|
|
|
|
|
|
var q = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
Assert.AreEqual(2, q.Take("event_5", 2, T0));
|
|
|
|
|
|
Assert.AreEqual(0, q.Take("event_5", 2, T0 + 5), "повторное «Забрать» -- квота не берётся второй раз");
|
|
|
|
|
|
Assert.AreEqual(2, q.Used(T0 + 5));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Quota_RollingWindow_FreesAfter7Days_NoCarryOver()
|
|
|
|
|
|
{
|
|
|
|
|
|
var q = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
q.Take("a", 6, T0);
|
|
|
|
|
|
Assert.AreEqual(0, q.Left(T0 + 7 * Day - 1), "ещё не прошло 7 суток");
|
|
|
|
|
|
Assert.AreEqual(T0 + 7 * Day, q.NextFreeMs(T0 + Day));
|
|
|
|
|
|
Assert.AreEqual(6, q.Left(T0 + 7 * Day), "через 7 суток квота снова 6, а не 6 + перенос");
|
|
|
|
|
|
Assert.AreEqual(6, q.Take("b", 9, T0 + 7 * Day), "сверх 6 -- компенсацией, на следующую неделю не переносится");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Quota_SurvivesSaveAndLoad()
|
|
|
|
|
|
{
|
|
|
|
|
|
var q = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
q.Take("a", 4, T0);
|
|
|
|
|
|
var copy = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
copy.Load(q.ToLines());
|
|
|
|
|
|
Assert.AreEqual(2, copy.Left(T0 + Day), "после перезапуска -- те же 4 из 6");
|
|
|
|
|
|
Assert.IsTrue(copy.Taken("a"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Quota_SeparateFromStorage_WaitingNotCountedTwice()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Квота есть, а места нет: выданное по квоте ждёт места и второй раз квоту не тратит
|
|
|
|
|
|
var q = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
var r = new CrystalRewards();
|
|
|
|
|
|
int byQuota = q.Take("q1", 2, T0);
|
|
|
|
|
|
Assert.AreEqual(2, byQuota);
|
|
|
|
|
|
Assert.AreEqual(0, r.Claim("q1", byQuota, 50, 50), "хранилище 50/50 -- ничего не начислено");
|
|
|
|
|
|
Assert.AreEqual(2, r.Left("q1"), "2 ждут места");
|
|
|
|
|
|
Assert.AreEqual(2, r.Claim("q1", 0, 40, 50), "место появилось -- выданы ждущие");
|
|
|
|
|
|
Assert.AreEqual(2, q.Used(T0 + 10), "квота потрачена один раз: 2, а не 4");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-06 16:27:27 +01:00
|
|
|
|
|
|
|
|
|
|
// Редкий PvE-торговец: синий кристалл за золото (регламент валют 05.11, разд. 3, 10, 12)
|
|
|
|
|
|
public class CrystalTraderTests
|
|
|
|
|
|
{
|
|
|
|
|
|
const long T0 = 1_800_000_000_000L;
|
|
|
|
|
|
const long Day = CrystalQuota.Day;
|
|
|
|
|
|
|
|
|
|
|
|
static System.Func<int, bool> Wallet(float[] gold) => price =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (gold[0] < price) return false;
|
|
|
|
|
|
gold[0] -= price;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Trader_Quota4of6_Buys_ChargesGold_CountsQuota_Max2()
|
|
|
|
|
|
{
|
|
|
|
|
|
var quota = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
quota.Take("old", 4, T0);
|
|
|
|
|
|
var t = new CrystalTrader(new[] { 30000, 60000 }, 7 * Day);
|
|
|
|
|
|
var gold = new[] { 100000f };
|
|
|
|
|
|
Assert.AreEqual(CrystalTrader.Result.Bought, t.TryBuy("b1", T0 + 1, quota, 10, Wallet(gold)));
|
|
|
|
|
|
Assert.AreEqual(70000f, gold[0], 0.01, "первая -- 30 000");
|
|
|
|
|
|
Assert.AreEqual(5, quota.Used(T0 + 1), "квота +1");
|
|
|
|
|
|
Assert.AreEqual(CrystalTrader.Result.Bought, t.TryBuy("b2", T0 + 2, quota, 10, Wallet(gold)));
|
|
|
|
|
|
Assert.AreEqual(10000f, gold[0], 0.01, "вторая -- 60 000");
|
|
|
|
|
|
Assert.AreEqual(CrystalTrader.Result.NoSlots, t.TryBuy("b3", T0 + 3, quota, 10, Wallet(gold)), "третьей нет");
|
|
|
|
|
|
Assert.AreEqual(10000f, gold[0], 0.01, "золото не списано");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Trader_Quota6of6_Blocked_NoGoldSpent()
|
|
|
|
|
|
{
|
|
|
|
|
|
var quota = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
quota.Take("old", 6, T0);
|
|
|
|
|
|
var t = new CrystalTrader(null, 7 * Day);
|
|
|
|
|
|
var gold = new[] { 50000f };
|
|
|
|
|
|
Assert.AreEqual(CrystalTrader.Result.NoQuota, t.TryBuy("b1", T0 + 1, quota, 10, Wallet(gold)));
|
|
|
|
|
|
Assert.AreEqual(50000f, gold[0], 0.01, "6/6 + покупка -> блок без расхода золота");
|
|
|
|
|
|
Assert.AreEqual(0, t.BoughtInWindow(T0 + 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Trader_NoSpace_Or_NoGold_NothingChanges()
|
|
|
|
|
|
{
|
|
|
|
|
|
var quota = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
var t = new CrystalTrader(null, 7 * Day);
|
|
|
|
|
|
var gold = new[] { 50000f };
|
|
|
|
|
|
Assert.AreEqual(CrystalTrader.Result.NoSpace, t.TryBuy("b1", T0, quota, 0, Wallet(gold)), "хранилище полное");
|
|
|
|
|
|
Assert.AreEqual(50000f, gold[0], 0.01);
|
|
|
|
|
|
var poor = new[] { 1000f };
|
|
|
|
|
|
Assert.AreEqual(CrystalTrader.Result.NoGold, t.TryBuy("b2", T0, quota, 10, Wallet(poor)), "не хватает золота");
|
|
|
|
|
|
Assert.AreEqual(0, quota.Used(T0), "квота не тронута");
|
|
|
|
|
|
Assert.AreEqual(30000, t.PriceNow(T0), "цена по-прежнему первая");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Trader_SameOperationTwice_OnePurchase()
|
|
|
|
|
|
{
|
|
|
|
|
|
var quota = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
var t = new CrystalTrader(null, 7 * Day);
|
|
|
|
|
|
var gold = new[] { 100000f };
|
|
|
|
|
|
t.TryBuy("b1", T0, quota, 10, Wallet(gold));
|
|
|
|
|
|
Assert.AreEqual(CrystalTrader.Result.Repeated, t.TryBuy("b1", T0 + 1, quota, 10, Wallet(gold)));
|
|
|
|
|
|
Assert.AreEqual(70000f, gold[0], 0.01, "повтор той же покупки -- ничего не списано");
|
|
|
|
|
|
Assert.AreEqual(1, quota.Used(T0 + 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Trader_WindowRolls_AndSurvivesSaveLoad()
|
|
|
|
|
|
{
|
|
|
|
|
|
var quota = new CrystalQuota(6, 7 * Day);
|
|
|
|
|
|
var t = new CrystalTrader(null, 7 * Day);
|
|
|
|
|
|
var gold = new[] { 1000000f };
|
|
|
|
|
|
t.TryBuy("b1", T0, quota, 10, Wallet(gold));
|
|
|
|
|
|
t.TryBuy("b2", T0 + Day, quota, 10, Wallet(gold));
|
|
|
|
|
|
var copy = new CrystalTrader(null, 7 * Day);
|
|
|
|
|
|
copy.Load(t.ToLines());
|
|
|
|
|
|
Assert.AreEqual(-1, copy.PriceNow(T0 + 2 * Day), "после перезапуска -- те же 2 покупки");
|
|
|
|
|
|
Assert.AreEqual(T0 + 7 * Day, copy.NextSlotMs(T0 + 2 * Day));
|
|
|
|
|
|
Assert.AreEqual(60000, copy.PriceNow(T0 + 7 * Day), "первая покупка вышла из окна -- следующая снова вторая по счёту");
|
|
|
|
|
|
Assert.AreEqual(30000, copy.PriceNow(T0 + 8 * Day), "обе вышли из окна -- снова 30 000");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-06 16:31:47 +01:00
|
|
|
|
|
|
|
|
|
|
// Кристальная шахта: 3 синих кристалла за 7 реальных суток (регламент кристаллов 05.11, разд. 2.1 и 6; ответ 06.11, п. 3)
|
|
|
|
|
|
public class CrystalMineTests
|
|
|
|
|
|
{
|
|
|
|
|
|
const long T0 = 1_800_000_000_000L;
|
|
|
|
|
|
const long Day = CrystalQuota.Day;
|
|
|
|
|
|
static readonly long[] OneMine = { long.MinValue };
|
|
|
|
|
|
|
|
|
|
|
|
static CrystalMine Started()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = new CrystalMine(3, 7 * Day);
|
|
|
|
|
|
m.Advance(T0, OneMine, 50); // первый расчёт -- точка отсчёта
|
|
|
|
|
|
return m;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Mine_7RealDays_FreeStorage_Exactly3()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Started();
|
|
|
|
|
|
int got = 0;
|
|
|
|
|
|
for (long t = T0; t < T0 + 7 * Day; t += 10 * 60 * 1000) got += m.Advance(t + 10 * 60 * 1000, OneMine, 50 - got);
|
|
|
|
|
|
Assert.AreEqual(3, got, "за 7 реальных суток -- ровно 3");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Mine_5RealMinutes_NotWeeklyAmount()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Started();
|
|
|
|
|
|
Assert.AreEqual(0, m.Advance(T0 + 5 * 60 * 1000, OneMine, 50), "5 реальных минут -- не недельный объём (×12 не действует)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Mine_49of50_AtMostOne_50of50_Paused_ThenContinues()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Started();
|
|
|
|
|
|
Assert.AreEqual(1, m.Advance(T0 + 7 * Day, OneMine, 1), "49/50 -- не больше 1");
|
|
|
|
|
|
Assert.AreEqual(0, m.ProgressMs, "лишнее не копится: шахта встала после последнего поместившегося");
|
|
|
|
|
|
Assert.AreEqual(0, m.Advance(T0 + 14 * Day, OneMine, 0), "50/50 -- пауза");
|
|
|
|
|
|
Assert.AreEqual(0, m.ProgressMs, "за паузу прогресс не вырос");
|
|
|
|
|
|
Assert.AreEqual(1, m.Advance(T0 + 14 * Day + m.MsPerCrystal, OneMine, 10), "место есть -- дальше с оставшегося прогресса, без начисления за паузу");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Mine_Reload_And_ClockBack_NoDouble()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Started();
|
|
|
|
|
|
Assert.AreEqual(1, m.Advance(T0 + m.MsPerCrystal, OneMine, 50));
|
|
|
|
|
|
var copy = new CrystalMine(3, 7 * Day);
|
|
|
|
|
|
copy.Load(m.ToLine());
|
|
|
|
|
|
Assert.AreEqual(0, copy.Advance(T0 + m.MsPerCrystal, OneMine, 50), "после загрузки то же время -- ничего");
|
|
|
|
|
|
Assert.AreEqual(0, copy.Advance(T0, OneMine, 50), "часы назад (сброс перемотки) -- ничего");
|
|
|
|
|
|
Assert.AreEqual(T0 + m.MsPerCrystal, copy.LastMs, "метка не ушла назад");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Mine_TwoMines_Double_BuiltOffline_CountsFromCompletion()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Started();
|
|
|
|
|
|
Assert.AreEqual(6, m.Advance(T0 + 7 * Day, new[] { long.MinValue, long.MinValue }, 50), "две шахты -- 6 за 7 суток");
|
|
|
|
|
|
var late = Started();
|
|
|
|
|
|
Assert.AreEqual(1, late.Advance(T0 + 3 * Day, new[] { T0 + 3 * Day - late.MsPerCrystal }, 50),
|
|
|
|
|
|
"шахта достроена офлайн -- добывает с конца стройки, а не с начала офлайна");
|
|
|
|
|
|
Assert.AreEqual(0, Started().Advance(T0 + Day, new long[0], 50), "шахт нет -- ничего");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|