Модуль Чата (Bogatyri.Chat, без Unity): - чат вынесен из Social в отдельный модуль; контракт IChat в ядре, событие ChatMessageReceivedEvent, LocalPlayer (id и имя игрока) - панель чата работает только через контракт и события - разворот чата на весь экран: кнопка «Развернуть/Свернуть» или Esc Модуль Круга (Bogatyri.Circle, без Unity): - основать Круг, вступить, выйти; роли главы и участника; 30 мест - уход главы передаёт главенство, последний участник распускает Круг - секция сохранения "circle", участвует в сбросе сохранений - окно «Круг» (кнопка внизу карты мира), демо-Круги до появления сервера - канал «Круг» в чате — только для участников Круга Прочее: - кнопка «Почта» привязана к правому краю нижней панели (в безопасной зоне она уезжала за экран) - тесты Чата и Круга; документы Chat.md, Circle.md, обновлены README, Core, Save Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
9.0 KiB
C#
240 lines
9.0 KiB
C#
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<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);
|
||
}
|
||
|
||
// ---------------- Чат ----------------
|
||
|
||
[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, "всем привет"), "общий чат доступен всегда");
|
||
}
|
||
|
||
// ---------------- Ядро ----------------
|
||
|
||
[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 _));
|
||
}
|
||
}
|