Files
MVP1/Assets/Scripts/Modules/Tests/ModuleTests.cs
T
AlexanderandClaude Opus 5.5 15b06b8f42 Модули Чата и Круга, разворот чата на весь экран, кнопка «Почта» в Safe Area
Модуль Чата (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>
2026-10-19 19:07:45 +02:00

240 lines
9.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 _));
}
}