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); } // ---------------- Боевая система ---------------- [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); } // ---------------- Глобальный мир: поход до конца, в том числе офлайн ---------------- [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(); 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); } // ---------------- Чат ---------------- [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(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", 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(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(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, "всем привет"), "общий чат доступен всегда"); } // ---------------- Ядро ---------------- [Test] public void EventBus_FailingSubscriberDoesNotBreakOthers() { int delivered = 0; Exception caught = null; EventBus.OnHandlerError = e => caught = e; EventBus.Subscribe(_ => throw new InvalidOperationException("boom")); EventBus.Subscribe(_ => delivered++); EventBus.Publish(new MarchReturnedEvent { Survivors = 5 }); Assert.AreEqual(1, delivered); Assert.IsNotNull(caught); } [Test] public void GameServices_MissingModule_ThrowsClearError() { Assert.Throws(() => GameServices.Get()); Assert.IsFalse(GameServices.TryGet(out IArmy _)); } }