Скорости маршей по типам войск и слоты похода (регламент 31.10)

Мир (Bogatyri.World.Simulation):
- MarchSpeedRules: скорость по типам — пехота 8, лучники 9, кавалерия 12, осадные 4 клетки
  за реальный час (ТЕСТ); марш идёт в реальном времени
- смешанное войско — по самому медленному типу (настройка mixedUsesSlowest: средняя по числу воинов);
  правило для одной армии в регламенте не задано — вопрос команде
- WorldSimulation.TravelTimeMs, AssignUnits: время прихода пересчитывается по составу похода;
  обратный путь — по составу выживших
- MarchRules.GetTravelTimeMs — для похода без состава (как пехота)

Армия (Bogatyri.Army):
- слоты похода: IArmy.MarchSlots (база 2, ТЕСТ), ActiveMarches; TakeForMarch отказывает при занятых слотах;
  походы из старых сохранений сверх лимита дойдут, новых не будет

Таблица MarchConfig (Assets/Resources/MarchConfig.asset): скорости, режим смешанного войска, база слотов.
Карта мира: путь в окне атаки — по составу при выбранном числе воинов, «Все слоты похода заняты»,
верхняя строка «отрядов: 1 из 2».

Тесты: скорости по типам, смешанное войско, обратный путь, слоты; старые тесты мира переведены
на часовые марши. Все 49 тестов ModuleTests проходят вне Unity. Код Валдемара не менялся.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-10-31 16:05:55 +01:00
co-authored by Claude Opus 5.5
parent d9c0039bd2
commit e8b17a7efa
15 changed files with 333 additions and 40 deletions
+84 -10
View File
@@ -626,7 +626,7 @@ public class ModuleTests
Assert.IsNotNull(a, ea);
var events = new List<WorldEvent>();
world.Advance(10 * 60_000, events); // игра была закрыта 10 минут
world.Advance(7L * 24 * 3_600_000, events); // игра была закрыта неделю (марши идут часами)
Assert.AreEqual(4, events.Count);
for (int i = 1; i < events.Count; i++)
@@ -667,15 +667,88 @@ public class ModuleTests
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 минут -- снова на месте");
world.Advance(m.returnMs - 1, events);
Assert.AreEqual(1, events.Count);
world.Advance(m.returnMs, events);
Assert.AreEqual(2, events.Count, "возвращение ровно в срок");
}
// Скорости маршей по типам (регламент 31.10, ТЕСТ): пехота 8, лучники 9, кавалерия 12, осадные 4 клетки за реальный час
[Test]
public void World_MarchSpeed_ByTroopTypes()
{
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
MapNode home = world.GetNode(WorldMapLayout.PlayerCityId);
MapNode boss = world.GetNode(WorldMapLayout.BossCastleId);
long d = WorldMapLayout.Distance(home, boss); // в милли-единицах, 1000 = клетка
long Arrive(int[] units)
{
MarchData m = world.StartMarch("p", WorldMapLayout.PlayerCityId, WorldMapLayout.BossCastleId, 1, 0, out _);
Assert.IsTrue(world.AssignUnits(m, units));
world.CancelMarch(m.id);
return m.arriveMs;
}
Assert.AreEqual(d * 3_600_000 / 8000, Arrive(new[] { 10, 0, 0, 0 }), "пехота -- 8 клеток в час");
Assert.AreEqual(d * 3_600_000 / 9000, Arrive(new[] { 0, 10, 0, 0 }), "лучники -- 9");
Assert.AreEqual(d * 3_600_000 / 12000, Arrive(new[] { 0, 0, 10, 0 }), "кавалерия -- 12");
Assert.AreEqual(d * 3_600_000 / 4000, Arrive(new[] { 0, 0, 0, 10 }), "осадные -- 4");
Assert.AreEqual(d * 3_600_000 / 4000, Arrive(new[] { 30, 0, 20, 1 }), "одна осадная машина тормозит всё войско");
Assert.AreEqual(MarchRules.GetTravelTimeMs(home, boss), Arrive(null), "состав неизвестен -- как пехота");
Assert.Greater(d * 3_600_000 / 8000, 3_600_000L, "до замка больше часа -- марш в реальном времени");
// Средняя скорость по числу воинов -- если команда выберет её вместо «по самому медленному»
var weighted = new MarchSpeedRules { MixedUsesSlowest = false };
Assert.AreEqual(10000, weighted.SpeedFor(new[] { 10, 0, 10, 0 }), "(8 + 12) / 2 = 10");
Assert.AreEqual(8000, MarchSpeedRules.Default.SpeedFor(new[] { 10, 0, 10, 0 }), "по самому медленному -- 8");
// После боя время прихода уже не меняется
MarchData fought = world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_1", 20, 0, out _);
world.Advance(fought.arriveMs, new List<WorldEvent>());
Assert.IsFalse(world.AssignUnits(fought, new[] { 0, 0, 20, 0 }));
// Обратно идут выжившие со скоростью своего состава
MarchData mixed = world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_4", 21, 0, out _); // camp_1 уже разбит
world.AssignUnits(mixed, new[] { 20, 0, 0, 1 });
world.Advance(mixed.arriveMs, new List<WorldEvent>());
Assert.AreEqual(world.TravelTimeMs(world.GetNode("camp_4"), home, mixed.survivorUnits), mixed.returnMs - mixed.arriveMs);
}
// Слоты похода (регламент 31.10): база 2, третий поход не уходит, пока один не вернётся
[Test]
public void ArmyService_MarchSlots()
{
var army = new ArmyService(); // база 2
army.AddTroops(0, 30);
Assert.AreEqual(2, army.MarchSlots);
Assert.IsTrue(army.TakeForMarch(1, 5));
Assert.IsTrue(army.TakeForMarch(2, 5));
Assert.AreEqual(2, army.ActiveMarches);
Assert.IsFalse(army.TakeForMarch(3, 5), "оба слота заняты");
Assert.AreEqual(20, army.SoldiersAtHome, "отказ ничего не забрал");
Assert.IsTrue(army.ReturnFromMarch(1, 5));
Assert.IsTrue(army.TakeForMarch(3, 5), "слот освободился");
var bigger = new ArmyService(3); // из таблицы MarchConfig
bigger.AddTroops(0, 30);
for (int i = 1; i <= 3; i++) Assert.IsTrue(bigger.TakeForMarch(i, 5));
Assert.IsFalse(bigger.TakeForMarch(4, 5));
// Старое сохранение с тремя походами: все дойдут, новых не будет, пока не освободится слот
var old = new ArmyService();
old.Restore("troops=10,0,0,0\nmarch=1:1,0,0,0\nmarch=2:1,0,0,0\nmarch=3:1,0,0,0\n");
Assert.AreEqual(3, old.ActiveMarches);
Assert.IsFalse(old.TakeForMarch(4, 1));
Assert.IsTrue(old.ReturnFromMarch(1, 1));
Assert.IsTrue(old.ReturnFromMarch(2, 1));
Assert.IsTrue(old.TakeForMarch(4, 1));
}
// 0 и 1 солдат
@@ -761,20 +834,21 @@ public class ModuleTests
Assert.IsNotNull(first, e1);
Assert.IsNotNull(second, e2);
world.Advance(10 * 60_000, new List<WorldEvent>()); // через 10 минут оба похода закончились
long afterBoth = second.arriveMs; // оба дошли до лагеря (второй -- на 1 мс позже)
world.Advance(afterBoth, new List<WorldEvent>());
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 _), "разбитый лагерь не атакуется");
Assert.IsTrue(world.IsCampCleared(camp, afterBoth));
Assert.IsNull(world.StartMarch("p", WorldMapLayout.PlayerCityId, camp, 30, afterBoth, out _), "разбитый лагерь не атакуется");
// Сохранение: другой мир с теми же данными помнит разбитый лагерь
var loaded = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
loaded.LoadCampStates(world.GetCampStates());
Assert.IsTrue(loaded.IsCampCleared(camp, 10 * 60_000));
Assert.IsTrue(loaded.IsCampCleared(camp, afterBoth));
long respawn = first.arriveMs + CampRules.RespawnMs;
Assert.IsFalse(world.IsCampCleared(camp, respawn), "лагерь появился снова");
@@ -791,7 +865,7 @@ public class ModuleTests
Assert.IsNotNull(march, error);
var events = new List<WorldEvent>();
world.Advance(3_600_000, events); // игра была закрыта час
world.Advance(24L * 3_600_000, events); // игра была закрыта сутки (до замка -- больше часа пути)
Assert.AreEqual(2, events.Count, "бой и возвращение за один вызов");
Assert.AreEqual(WorldEventType.BattleResolved, events[0].Type);