Мир по документу 02: события строго по времени, время и номер боя в событиях, единицы таймеров

Порядок событий (офлайн — в причинно-следственном порядке):
- WorldSimulation.Advance каждый раз берёт самое раннее событие — бой или возвращение;
  раньше за один вызов шли сначала все бои, потом все возвращения, и войско,
  вернувшееся раньше чужого боя, попадало в события после него
- при равном времени — бой раньше возвращения, затем по номеру похода
- WorldEvent.TimeMs — когда событие случилось, а не когда его заметили

События похода (ядро):
- TimeMs у MarchStartedEvent, MarchBattleResolvedEvent, MarchReturnedEvent
- BattleId у MarchBattleResolvedEvent

Таймеры: явные единицы (реальные секунды/минуты часов GameClock), таблица в World.md;
скорость игрового календаря (№7) не утверждена — ускорения нет.

Тесты «Готово, когда» (документ 02), наша часть: порядок после офлайна, повторный вызов
без дублей, границы таймеров (ровно в срок, не раньше), 0 и 1 солдат.
Все 46 тестов ModuleTests проходят вне Unity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-10-30 17:11:15 +01:00
co-authored by Claude Opus 5.5
parent 6772292650
commit 8e9dbdf1cf
7 changed files with 149 additions and 22 deletions
@@ -559,6 +559,91 @@ public class ModuleTests
Assert.AreEqual(100, p.attackerConditionPercent);
}
// ---------------- Мир: критерии «Готово, когда» (документ 02 комплекта от 29.10.2026) ----------------
// Офлайн -- в причинно-следственном порядке: события по времени, а не «сначала все бои, потом все возвращения»
[Test]
public void World_Offline_EventsInStrictTimeOrder()
{
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
MapNode home = world.GetNode(WorldMapLayout.PlayerCityId);
// B -- к ближнему лагерю сразу; A -- к замку позже, так что A дойдёт до замка уже после возвращения B
MarchData b = world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_12", 30, 0, out string eb);
long bRound = 2 * MarchRules.GetTravelTimeMs(home, world.GetNode("camp_12"));
MarchData a = world.StartMarch("p", WorldMapLayout.PlayerCityId, WorldMapLayout.BossCastleId, 30, bRound, out string ea);
Assert.IsNotNull(b, eb);
Assert.IsNotNull(a, ea);
var events = new List<WorldEvent>();
world.Advance(10 * 60_000, events); // игра была закрыта 10 минут
Assert.AreEqual(4, events.Count);
for (int i = 1; i < events.Count; i++)
Assert.GreaterOrEqual(events[i].TimeMs, events[i - 1].TimeMs, "события строго по времени");
Assert.AreSame(b, events[0].March); Assert.AreEqual(WorldEventType.BattleResolved, events[0].Type);
Assert.AreSame(b, events[1].March); Assert.AreEqual(WorldEventType.MarchReturned, events[1].Type, "B вернулся раньше, чем A дошёл до замка");
Assert.AreSame(a, events[2].March); Assert.AreEqual(WorldEventType.BattleResolved, events[2].Type);
Assert.AreEqual(b.arriveMs, events[0].TimeMs, "время события -- когда это случилось, а не когда заметили");
Assert.AreEqual(b.returnMs, events[1].TimeMs);
}
// Двойная доставка: повторный вызов с тем же временем ничего не создаёт заново
[Test]
public void World_AdvanceTwice_NoDuplicateEvents()
{
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
MarchData m = world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_1", 20, 0, out _);
var first = new List<WorldEvent>();
var again = new List<WorldEvent>();
world.Advance(m.arriveMs, first);
world.Advance(m.arriveMs, again);
Assert.AreEqual(1, first.Count);
Assert.AreEqual(0, again.Count, "бой не считается второй раз");
world.Advance(m.returnMs, first);
world.Advance(m.returnMs, again);
Assert.AreEqual(2, first.Count);
Assert.AreEqual(0, again.Count, "возвращение не повторяется");
}
// Истечение таймеров: ровно в назначенную миллисекунду, не раньше
[Test]
public void World_TimerBoundaries()
{
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
MarchData m = world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_1", 20, 0, out _);
var events = new List<WorldEvent>();
world.Advance(m.arriveMs - 1, events);
Assert.AreEqual(0, events.Count, "за миллисекунду до прихода боя нет");
world.Advance(m.arriveMs, events);
Assert.AreEqual(1, events.Count, "бой ровно в момент прихода");
world.Advance(m.returnMs - 1, events);
Assert.AreEqual(1, events.Count);
world.Advance(m.returnMs, events);
Assert.AreEqual(2, events.Count, "возвращение ровно в срок");
Assert.IsTrue(m.victory);
long respawn = m.arriveMs + CampRules.RespawnMs;
Assert.IsTrue(world.IsCampCleared("camp_1", respawn - 1), "за миллисекунду до появления лагерь ещё разбит");
Assert.IsFalse(world.IsCampCleared("camp_1", respawn), "ровно через 30 минут -- снова на месте");
}
// 0 и 1 солдат
[Test]
public void World_ZeroAndOneSoldier()
{
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
Assert.IsNull(world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_1", 0, 0, out string error), "поход без солдат");
Assert.IsNotEmpty(error);
Assert.AreEqual(0, world.Marches.Count);
MarchData one = world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_1", 1, 0, out _);
Assert.IsNotNull(one);
world.Advance(one.returnMs + one.arriveMs * 3, new List<WorldEvent>());
Assert.IsFalse(one.victory, "один против стана разбойников -- поражение");
Assert.That(one.survivors, Is.InRange(0, 1));
Assert.AreEqual(0, world.Marches.Count);
}
[Test]
public void World_MarchBattle_IsReproducible()
{