2026-10-19 18:39:28 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Bogatyri.World;
|
|
|
|
|
|
using NUnit.Framework;
|
|
|
|
|
|
|
|
|
|
|
|
// Тесты модулей без Unity-сцены. Запуск: Window -> General -> Test Runner -> EditMode -> Run All.
|
|
|
|
|
|
// Список тестов, которые команда решила добавить (план от 19.10.2026), -- в Docs/Modules/README.md.
|
|
|
|
|
|
public class ModuleTests
|
|
|
|
|
|
{
|
|
|
|
|
|
[SetUp]
|
|
|
|
|
|
public void ResetCore()
|
|
|
|
|
|
{
|
|
|
|
|
|
GameServices.Clear();
|
|
|
|
|
|
EventBus.Clear();
|
|
|
|
|
|
SaveRegistry.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Армия: один солдат не может быть в двух местах ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyMath_TakeProportional_NeverDoubleCountsOrLosesSoldiers()
|
|
|
|
|
|
{
|
|
|
|
|
|
var rnd = new Random(1);
|
|
|
|
|
|
for (int run = 0; run < 2000; run++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var troops = new int[4];
|
|
|
|
|
|
int total = 0;
|
|
|
|
|
|
for (int i = 0; i < 4; i++) { troops[i] = rnd.Next(0, 50); total += troops[i]; }
|
|
|
|
|
|
if (total == 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
int count = rnd.Next(1, total + 1);
|
|
|
|
|
|
int[] taken = ArmyMath.TakeProportional(troops, count);
|
|
|
|
|
|
|
|
|
|
|
|
int sum = 0;
|
|
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.That(taken[i], Is.InRange(0, troops[i]), "нельзя взять больше, чем есть в типе");
|
|
|
|
|
|
sum += taken[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
Assert.AreEqual(count, sum, "в поход уходит ровно столько, сколько просили");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyMath_TakeMoreThanExists_ReturnsNull()
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.IsNull(ArmyMath.TakeProportional(new[] { 1, 1, 0, 0 }, 3));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyMath_SplitSurvivors_ReturnsExactlySurvivors()
|
|
|
|
|
|
{
|
|
|
|
|
|
int[] back = ArmyMath.SplitSurvivors(new[] { 10, 5, 0, 1 }, 9);
|
|
|
|
|
|
int sum = 0;
|
|
|
|
|
|
foreach (var b in back) { Assert.GreaterOrEqual(b, 0); sum += b; }
|
|
|
|
|
|
Assert.AreEqual(9, sum);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-23 18:33:04 +02:00
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyService_MarchNeverCreatesOrLosesSoldiers()
|
|
|
|
|
|
{
|
|
|
|
|
|
var army = new ArmyService();
|
|
|
|
|
|
army.AddTroops(0, 20); army.AddTroops(1, 7); army.AddTroops(3, 3);
|
|
|
|
|
|
int total = army.SoldiersAtHome;
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(army.TakeForMarch(1, 12));
|
|
|
|
|
|
Assert.AreEqual(total, army.SoldiersAtHome + army.TroopsInMarches, "солдат либо дома, либо в походе");
|
|
|
|
|
|
|
|
|
|
|
|
army.ReturnFromMarch(1, 12);
|
|
|
|
|
|
Assert.AreEqual(total, army.SoldiersAtHome);
|
|
|
|
|
|
Assert.AreEqual(0, army.TroopsInMarches);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsFalse(army.TakeForMarch(2, total + 1), "нельзя отправить больше, чем есть");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyService_LoseFraction_FloorsPerType()
|
|
|
|
|
|
{
|
|
|
|
|
|
var army = new ArmyService();
|
|
|
|
|
|
army.AddTroops(0, 10); army.AddTroops(1, 5);
|
|
|
|
|
|
int lost = army.LoseFraction(0.25);
|
|
|
|
|
|
Assert.AreEqual(2 + 1, lost); // floor(2.5) + floor(1.25)
|
|
|
|
|
|
Assert.AreEqual(8, army.GetTroops(0));
|
|
|
|
|
|
Assert.AreEqual(4, army.GetTroops(1));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyService_SaveRestore_KeepsTroopsAndMarches()
|
|
|
|
|
|
{
|
|
|
|
|
|
var a = new ArmyService();
|
|
|
|
|
|
a.AddTroops(0, 9); a.AddTroops(2, 4);
|
|
|
|
|
|
a.TakeForMarch(5, 6);
|
|
|
|
|
|
|
|
|
|
|
|
var b = new ArmyService();
|
|
|
|
|
|
b.Restore(a.Capture());
|
|
|
|
|
|
Assert.IsTrue(b.RestoredFromSave);
|
|
|
|
|
|
Assert.AreEqual(a.SoldiersAtHome, b.SoldiersAtHome);
|
|
|
|
|
|
Assert.AreEqual(a.TroopsInMarches, b.TroopsInMarches);
|
|
|
|
|
|
|
|
|
|
|
|
b.ReturnFromMarch(5, 6);
|
|
|
|
|
|
Assert.AreEqual(13, b.SoldiersAtHome, "состав похода сохранился");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyService_ImportLegacy_AndReset()
|
|
|
|
|
|
{
|
|
|
|
|
|
var army = new ArmyService();
|
|
|
|
|
|
int events = 0;
|
|
|
|
|
|
EventBus.Subscribe<TroopsChangedEvent>(_ => events++);
|
|
|
|
|
|
|
|
|
|
|
|
army.ImportLegacy(new[] { 5, 0, 2, 0 }, new[] { new KeyValuePair<int, int[]>(3, new[] { 1, 1, 0, 0 }) });
|
|
|
|
|
|
Assert.AreEqual(7, army.SoldiersAtHome);
|
|
|
|
|
|
Assert.AreEqual(2, army.TroopsInMarches);
|
|
|
|
|
|
Assert.Greater(events, 0, "изменение войск сообщается событием");
|
|
|
|
|
|
|
|
|
|
|
|
army.ResetState();
|
|
|
|
|
|
Assert.AreEqual(0, army.SoldiersAtHome + army.TroopsInMarches);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 16:54:49 +01:00
|
|
|
|
// Решение команды от 19.10.2026: один солдат -- в одном месте. 30 солдат, 10 в гарнизон -> 20 доступны, всего 30.
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void ArmyService_Garrison_OneSoldierInOnePlace()
|
|
|
|
|
|
{
|
|
|
|
|
|
var army = new ArmyService();
|
|
|
|
|
|
army.AddTroops(0, 20); army.AddTroops(1, 10);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(10, army.SetGarrison(10));
|
|
|
|
|
|
Assert.AreEqual(10, army.GarrisonTotal);
|
|
|
|
|
|
Assert.AreEqual(20, army.AvailableForMarch);
|
|
|
|
|
|
Assert.AreEqual(30, army.SoldiersAtHome, "всего воинов столько же");
|
|
|
|
|
|
Assert.AreEqual(7, army.GetGarrison(0), "гарнизон по составу: 2/3 пехоты");
|
|
|
|
|
|
Assert.AreEqual(3, army.GetGarrison(1), "и 1/3 лучников");
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsFalse(army.TakeForMarch(1, 21), "гарнизон в поход не уходит");
|
|
|
|
|
|
Assert.IsTrue(army.TakeForMarch(1, 20));
|
|
|
|
|
|
Assert.AreEqual(0, army.AvailableForMarch);
|
|
|
|
|
|
Assert.AreEqual(10, army.GarrisonTotal, "гарнизон остался в городе");
|
|
|
|
|
|
Assert.AreEqual(30, army.SoldiersAtHome + army.TroopsInMarches);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(10, army.SetGarrison(99), "гарнизон не больше воинов в городе");
|
|
|
|
|
|
|
|
|
|
|
|
army.LoseFraction(0.5);
|
|
|
|
|
|
Assert.LessOrEqual(army.GarrisonTotal, army.SoldiersAtHome, "после потерь гарнизон не больше войск");
|
|
|
|
|
|
for (int i = 0; i < 4; i++) Assert.LessOrEqual(army.GetGarrison(i), army.GetTroops(i));
|
|
|
|
|
|
|
|
|
|
|
|
var loaded = new ArmyService();
|
|
|
|
|
|
loaded.Restore(army.Capture());
|
|
|
|
|
|
Assert.AreEqual(army.GarrisonTotal, loaded.GarrisonTotal, "гарнизон сохраняется");
|
|
|
|
|
|
|
|
|
|
|
|
loaded.Restore("troops=5,0,0,0\n"); // старое сохранение без строки гарнизона
|
|
|
|
|
|
Assert.AreEqual(0, loaded.GarrisonTotal);
|
|
|
|
|
|
Assert.AreEqual(5, loaded.AvailableForMarch);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// ---------------- Боевая система ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Battle_MoreSoldiersWins_TieGoesToDefender()
|
|
|
|
|
|
{
|
|
|
|
|
|
var battle = new BattleSystem();
|
|
|
|
|
|
Assert.IsTrue(battle.Resolve(new BattleRequest { Attackers = 30, Defenders = 20 }).Victory);
|
|
|
|
|
|
Assert.IsFalse(battle.Resolve(new BattleRequest { Attackers = 20, Defenders = 20 }).Victory);
|
|
|
|
|
|
Assert.AreEqual(20, battle.Resolve(new BattleRequest { Attackers = 30, Defenders = 20 }).Survivors);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 16:34:58 +01:00
|
|
|
|
[Test]
|
|
|
|
|
|
public void Battle_Counters_ArchersBeatInfantry_InfantryBeatsCavalry()
|
|
|
|
|
|
{
|
|
|
|
|
|
var battle = new BattleSystem();
|
|
|
|
|
|
// Одинаковая "сырая" сила: 25 лучников x5 = 125 против ~31 пехоты x4 = 124
|
|
|
|
|
|
var archersVsInfantry = battle.Resolve(new BattleRequest
|
|
|
|
|
|
{ AttackerUnits = new[] { 0, 25, 0, 0 }, DefenderUnits = new[] { 32, 0, 0, 0 } });
|
|
|
|
|
|
Assert.IsTrue(archersVsInfantry.Victory, "лучники с контрой бьют чуть более сильную пехоту");
|
|
|
|
|
|
|
|
|
|
|
|
var cavalryVsInfantry = battle.Resolve(new BattleRequest
|
|
|
|
|
|
{ AttackerUnits = new[] { 0, 0, 14, 0 }, DefenderUnits = new[] { 30, 0, 0, 0 } });
|
|
|
|
|
|
Assert.IsFalse(cavalryVsInfantry.Victory, "пехота с контрой держит чуть более сильную кавалерию");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Battle_WallsHelpDefenders_SiegeBreaksWalls()
|
|
|
|
|
|
{
|
|
|
|
|
|
var battle = new BattleSystem();
|
|
|
|
|
|
var field = battle.Resolve(new BattleRequest { AttackerUnits = new[] { 22, 0, 0, 0 }, DefenderUnits = new[] { 20, 0, 0, 0 } });
|
|
|
|
|
|
var walls = battle.Resolve(new BattleRequest { AttackerUnits = new[] { 22, 0, 0, 0 }, DefenderUnits = new[] { 20, 0, 0, 0 }, DefenderFortified = true });
|
|
|
|
|
|
Assert.IsTrue(field.Victory);
|
|
|
|
|
|
Assert.IsFalse(walls.Victory, "те же войска за стенами устояли");
|
|
|
|
|
|
|
|
|
|
|
|
var siege = battle.Resolve(new BattleRequest { AttackerUnits = new[] { 0, 0, 0, 6 }, DefenderUnits = new[] { 20, 0, 0, 0 }, DefenderFortified = true });
|
|
|
|
|
|
var siegeField = battle.Resolve(new BattleRequest { AttackerUnits = new[] { 0, 0, 0, 6 }, DefenderUnits = new[] { 20, 0, 0, 0 } });
|
|
|
|
|
|
Assert.Greater(siege.AttackerPower, siegeField.AttackerPower, "осадные сильнее против стен, чем в поле");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Battle_LossesPerType_NeverExceedUnits_AndSumUp()
|
|
|
|
|
|
{
|
|
|
|
|
|
var battle = new BattleSystem();
|
|
|
|
|
|
var rnd = new Random(7);
|
|
|
|
|
|
for (int run = 0; run < 500; run++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var att = new int[4]; var def = new int[4];
|
|
|
|
|
|
for (int i = 0; i < 4; i++) { att[i] = rnd.Next(0, 30); def[i] = rnd.Next(0, 30); }
|
|
|
|
|
|
var o = battle.Resolve(new BattleRequest { AttackerUnits = att, DefenderUnits = def, DefenderFortified = rnd.Next(2) == 0 });
|
|
|
|
|
|
|
|
|
|
|
|
int survivors = 0, defLost = 0;
|
|
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.That(o.AttackerSurvivorUnits[i], Is.InRange(0, att[i]));
|
|
|
|
|
|
Assert.That(o.DefenderLossUnits[i], Is.InRange(0, def[i]));
|
|
|
|
|
|
survivors += o.AttackerSurvivorUnits[i]; defLost += o.DefenderLossUnits[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
Assert.AreEqual(o.Survivors, survivors);
|
|
|
|
|
|
Assert.AreEqual(o.DefenderLosses, defLost);
|
|
|
|
|
|
Assert.AreEqual(att[0] + att[1] + att[2] + att[3] - o.AttackerLosses, o.Survivors);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void World_GarrisonComposition_SumsToGarrison_AndIsStable()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var node in WorldMapLayout.CreateDefault())
|
|
|
|
|
|
{
|
|
|
|
|
|
int[] a = WorldMapLayout.GarrisonUnits(node);
|
|
|
|
|
|
int[] b = WorldMapLayout.GarrisonUnits(node);
|
|
|
|
|
|
int sum = 0;
|
|
|
|
|
|
for (int i = 0; i < 4; i++) { Assert.GreaterOrEqual(a[i], 0); Assert.AreEqual(a[i], b[i]); sum += a[i]; }
|
|
|
|
|
|
Assert.AreEqual(node.garrison, sum, node.displayName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 16:46:54 +01:00
|
|
|
|
// ---------------- Глобальный мир: лагеря разбойников ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void World_Camps_AreSpreadOut_FightInField_WithoutSiege()
|
|
|
|
|
|
{
|
|
|
|
|
|
var nodes = WorldMapLayout.CreateDefault();
|
|
|
|
|
|
int camps = 0;
|
|
|
|
|
|
foreach (var camp in nodes)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (camp.type != MapNodeType.Camp) continue;
|
|
|
|
|
|
camps++;
|
|
|
|
|
|
Assert.IsFalse(WorldMapLayout.IsFortified(camp), "лагерь -- бой в поле");
|
|
|
|
|
|
Assert.AreEqual(0, WorldMapLayout.GarrisonUnits(camp)[TroopType.Siege], "у разбойников нет осадных");
|
|
|
|
|
|
Assert.That(camp.level, Is.InRange(1, WorldMapLayout.CampLevels));
|
|
|
|
|
|
foreach (var other in nodes)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (other == camp) continue;
|
|
|
|
|
|
int min = other.type == MapNodeType.BossCastle ? 4500 : 2600;
|
|
|
|
|
|
Assert.GreaterOrEqual(WorldMapLayout.Distance(camp, other), min - 1, camp.id + " слишком близко к " + other.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Assert.AreEqual(WorldMapLayout.CampCount, camps);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void World_CampCleared_SecondMarchFindsItEmpty_ThenItRespawns()
|
|
|
|
|
|
{
|
|
|
|
|
|
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
|
|
|
|
|
|
const string camp = "camp_1"; // 1 уровень, 6..9 разбойников
|
|
|
|
|
|
MarchData first = world.StartMarch("p", WorldMapLayout.PlayerCityId, camp, 30, 0, out string e1);
|
|
|
|
|
|
MarchData second = world.StartMarch("p", WorldMapLayout.PlayerCityId, camp, 30, 1, out string e2);
|
|
|
|
|
|
Assert.IsNotNull(first, e1);
|
|
|
|
|
|
Assert.IsNotNull(second, e2);
|
|
|
|
|
|
|
|
|
|
|
|
world.Advance(10 * 60_000, new List<WorldEvent>()); // через 10 минут оба похода закончились
|
|
|
|
|
|
Assert.IsTrue(first.victory);
|
|
|
|
|
|
Assert.IsNotEmpty(first.loot);
|
|
|
|
|
|
Assert.IsTrue(second.targetEmpty, "второе войско пришло к уже разбитому лагерю");
|
|
|
|
|
|
Assert.AreEqual(30, second.survivors);
|
|
|
|
|
|
Assert.IsEmpty(second.loot);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(world.IsCampCleared(camp, 10 * 60_000));
|
|
|
|
|
|
Assert.IsNull(world.StartMarch("p", WorldMapLayout.PlayerCityId, camp, 30, 10 * 60_000, out _), "разбитый лагерь не атакуется");
|
|
|
|
|
|
|
|
|
|
|
|
// Сохранение: другой мир с теми же данными помнит разбитый лагерь
|
|
|
|
|
|
var loaded = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
|
|
|
|
|
|
loaded.LoadCampStates(world.GetCampStates());
|
|
|
|
|
|
Assert.IsTrue(loaded.IsCampCleared(camp, 10 * 60_000));
|
|
|
|
|
|
|
|
|
|
|
|
long respawn = first.arriveMs + CampRules.RespawnMs;
|
|
|
|
|
|
Assert.IsFalse(world.IsCampCleared(camp, respawn), "лагерь появился снова");
|
|
|
|
|
|
Assert.IsNotNull(world.StartMarch("p", WorldMapLayout.PlayerCityId, camp, 30, respawn, out _));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// ---------------- Глобальный мир: поход до конца, в том числе офлайн ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void World_MarchResolvesAndReturns_EvenAfterLongOffline()
|
|
|
|
|
|
{
|
|
|
|
|
|
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
|
|
|
|
|
|
MarchData march = world.StartMarch("p", WorldMapLayout.PlayerCityId, WorldMapLayout.BossCastleId, 30, 0, out string error);
|
|
|
|
|
|
Assert.IsNotNull(march, error);
|
|
|
|
|
|
|
|
|
|
|
|
var events = new List<WorldEvent>();
|
|
|
|
|
|
world.Advance(3_600_000, events); // игра была закрыта час
|
|
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(2, events.Count, "бой и возвращение за один вызов");
|
|
|
|
|
|
Assert.AreEqual(WorldEventType.BattleResolved, events[0].Type);
|
|
|
|
|
|
Assert.AreEqual(WorldEventType.MarchReturned, events[1].Type);
|
|
|
|
|
|
Assert.AreEqual(0, world.Marches.Count);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 19:07:45 +02:00
|
|
|
|
// ---------------- Чат ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Chat_EmptyMessage_IsRejected()
|
|
|
|
|
|
{
|
|
|
|
|
|
var chat = new ChatService(0);
|
|
|
|
|
|
int before = chat.GetMessages(ChatChannel.World).Count;
|
|
|
|
|
|
Assert.IsFalse(chat.Send(ChatChannel.World, " "));
|
|
|
|
|
|
Assert.AreEqual(before, chat.GetMessages(ChatChannel.World).Count);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Chat_Send_TrimsCutsAndPublishesEvent()
|
|
|
|
|
|
{
|
|
|
|
|
|
var chat = new ChatService(0);
|
|
|
|
|
|
ChatMessage received = null;
|
|
|
|
|
|
EventBus.Subscribe<ChatMessageReceivedEvent>(e => received = e.Message);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(chat.Send(ChatChannel.Circle, " " + new string('а', 500) + " "));
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(received, "событие о новом сообщении");
|
|
|
|
|
|
Assert.AreEqual(ChatChannel.Circle, received.channel);
|
|
|
|
|
|
Assert.AreEqual(LocalPlayer.Id, received.senderId);
|
|
|
|
|
|
Assert.AreEqual(chat.MaxMessageLength, received.text.Length, "длинное сообщение обрезается");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Chat_KeepsOnlyLastMessagesPerChannel()
|
|
|
|
|
|
{
|
|
|
|
|
|
var chat = new ChatService(0);
|
|
|
|
|
|
for (int i = 0; i < 250; i++) chat.Send(ChatChannel.World, "сообщение " + i);
|
|
|
|
|
|
|
|
|
|
|
|
var world = chat.GetMessages(ChatChannel.World);
|
|
|
|
|
|
Assert.AreEqual(ChatService.MaxStoredPerChannel, world.Count);
|
|
|
|
|
|
Assert.AreEqual("сообщение 249", world[world.Count - 1].text);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Круг ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Circle_CreateMakesPlayerLeader_AndCannotJoinSecond()
|
|
|
|
|
|
{
|
|
|
|
|
|
var circle = new CircleService();
|
|
|
|
|
|
Assert.IsFalse(circle.IsInCircle);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(circle.TryCreate(" Дружина ", out string error), error);
|
|
|
|
|
|
Assert.IsTrue(circle.IsInCircle);
|
|
|
|
|
|
Assert.IsTrue(circle.IsLeader);
|
|
|
|
|
|
Assert.AreEqual("Дружина", circle.Current.name);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsFalse(circle.TryJoin("circle_1", out _), "нельзя состоять в двух Кругах");
|
|
|
|
|
|
Assert.IsFalse(circle.TryCreate("Ещё один", out _));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Circle_NameValidation()
|
|
|
|
|
|
{
|
|
|
|
|
|
var circle = new CircleService();
|
|
|
|
|
|
Assert.IsFalse(circle.TryCreate("ab", out _), "слишком короткое");
|
|
|
|
|
|
Assert.IsFalse(circle.TryCreate(new string('я', 30), out _), "слишком длинное");
|
|
|
|
|
|
Assert.IsFalse(circle.TryCreate("Круг Перуна", out _), "такое название уже есть");
|
|
|
|
|
|
Assert.IsFalse(circle.TryCreate("a|b<c>", out _), "недопустимые символы");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Circle_JoinFullCircle_IsRejected()
|
|
|
|
|
|
{
|
|
|
|
|
|
var circle = new CircleService();
|
|
|
|
|
|
CircleInfo full = circle.GetCircles()[circle.GetCircles().Count - 1];
|
|
|
|
|
|
Assert.AreEqual(circle.MaxMembers, full.members.Count);
|
|
|
|
|
|
Assert.IsFalse(circle.TryJoin(full.id, out _));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Circle_LeaveAlone_Disbands_LeaveAsMember_KeepsCircle()
|
|
|
|
|
|
{
|
|
|
|
|
|
var circle = new CircleService();
|
|
|
|
|
|
int before = circle.GetCircles().Count;
|
|
|
|
|
|
bool disbanded = false;
|
|
|
|
|
|
EventBus.Subscribe<CircleLeftEvent>(e => disbanded = e.Disbanded);
|
|
|
|
|
|
|
|
|
|
|
|
circle.TryCreate("Дружина", out _);
|
|
|
|
|
|
circle.Leave();
|
|
|
|
|
|
Assert.IsTrue(disbanded);
|
|
|
|
|
|
Assert.AreEqual(before, circle.GetCircles().Count, "свой Круг распущен");
|
|
|
|
|
|
|
|
|
|
|
|
circle.TryJoin("circle_1", out _);
|
|
|
|
|
|
int members = circle.Current.members.Count;
|
|
|
|
|
|
circle.Leave();
|
|
|
|
|
|
Assert.IsFalse(disbanded);
|
|
|
|
|
|
Assert.AreEqual(members - 1, circle.GetCircles()[0].members.Count);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Circle_SaveAndRestore_KeepsMembership()
|
|
|
|
|
|
{
|
|
|
|
|
|
var a = new CircleService();
|
|
|
|
|
|
a.TryCreate("Дружина", out _);
|
|
|
|
|
|
string saved = a.Capture();
|
|
|
|
|
|
|
|
|
|
|
|
var b = new CircleService();
|
|
|
|
|
|
b.Restore(saved);
|
|
|
|
|
|
Assert.IsTrue(b.IsInCircle);
|
|
|
|
|
|
Assert.IsTrue(b.IsLeader);
|
|
|
|
|
|
Assert.AreEqual("Дружина", b.Current.name);
|
|
|
|
|
|
|
|
|
|
|
|
var c = new CircleService();
|
|
|
|
|
|
c.TryJoin("circle_3", out _);
|
|
|
|
|
|
var d = new CircleService();
|
|
|
|
|
|
d.Restore(c.Capture());
|
|
|
|
|
|
Assert.AreEqual("circle_3", d.Current.id);
|
|
|
|
|
|
Assert.IsFalse(d.IsLeader);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void Chat_CircleChannel_OnlyWhenInCircle()
|
|
|
|
|
|
{
|
|
|
|
|
|
var circle = new CircleService();
|
|
|
|
|
|
GameServices.Register<ICircle>(circle);
|
|
|
|
|
|
var chat = new ChatService(0);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.IsFalse(chat.Send(ChatChannel.Circle, "привет"), "без Круга писать нельзя");
|
|
|
|
|
|
circle.TryJoin("circle_1", out _);
|
|
|
|
|
|
Assert.AreEqual(1, chat.GetMessages(ChatChannel.Circle).Count, "системное сообщение о вступлении");
|
|
|
|
|
|
Assert.IsTrue(chat.Send(ChatChannel.Circle, "привет"));
|
|
|
|
|
|
Assert.IsTrue(chat.Send(ChatChannel.World, "всем привет"), "общий чат доступен всегда");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// ---------------- Ядро ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void EventBus_FailingSubscriberDoesNotBreakOthers()
|
|
|
|
|
|
{
|
|
|
|
|
|
int delivered = 0;
|
|
|
|
|
|
Exception caught = null;
|
|
|
|
|
|
EventBus.OnHandlerError = e => caught = e;
|
|
|
|
|
|
EventBus.Subscribe<MarchReturnedEvent>(_ => throw new InvalidOperationException("boom"));
|
|
|
|
|
|
EventBus.Subscribe<MarchReturnedEvent>(_ => delivered++);
|
|
|
|
|
|
|
|
|
|
|
|
EventBus.Publish(new MarchReturnedEvent { Survivors = 5 });
|
|
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(1, delivered);
|
|
|
|
|
|
Assert.IsNotNull(caught);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
|
public void GameServices_MissingModule_ThrowsClearError()
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.Throws<InvalidOperationException>(() => GameServices.Get<IArmy>());
|
|
|
|
|
|
Assert.IsFalse(GameServices.TryGet(out IArmy _));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|