Раненые и госпиталь (решения 31.10, п. 7): логика

Бой делит выбывших на погибших и раненых по соотношению сил стороны (70/45/30/20%), версия правил 6.
Раненые идут домой с войском; Армия кладёт их в госпиталь, лечит 4/4/6/8 игровых часов, лишние ждут места.
Места -- от IHospital (здания пока нет -- HospitalConfig.temporaryBeds), цена -- через Экономику (не утверждена).
Население считает раненых людьми. Здание госпиталя -- следующий шаг.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-01 13:32:25 +01:00
co-authored by Claude Opus 5.5
parent 482721a704
commit 0214bdb268
27 changed files with 623 additions and 26 deletions
+134 -1
View File
@@ -406,7 +406,8 @@ public class ModuleTests
Assert.AreEqual(def[i], o.DefenderUnitsBefore[i]);
Assert.AreEqual(att[i], o.AttackerSurvivorUnits[i] + o.AttackerDeadUnits[i] + o.AttackerWoundedUnits[i], "атакующий: до = выжившие + погибшие + раненые");
Assert.AreEqual(def[i], o.DefenderSurvivorUnits[i] + o.DefenderDeadUnits[i] + o.DefenderWoundedUnits[i], "защитник: то же");
Assert.AreEqual(0, o.AttackerWoundedUnits[i] + o.DefenderWoundedUnits[i], "раненых пока нет: доля не утверждена");
Assert.GreaterOrEqual(o.AttackerWoundedUnits[i], 0, "раненых не меньше нуля");
Assert.GreaterOrEqual(o.DefenderWoundedUnits[i], 0);
Assert.GreaterOrEqual(o.AttackerSurvivorUnits[i], 0);
Assert.GreaterOrEqual(o.DefenderSurvivorUnits[i], 0);
Assert.AreEqual(o.DefenderDeadUnits[i] + o.DefenderWoundedUnits[i], o.DefenderLossUnits[i]);
@@ -1788,4 +1789,136 @@ public class ModuleTests
legacy.ProcessTraining(1000);
Assert.AreEqual(6, legacy.GetTroops(2));
}
// Решения 31.10, п. 7 (регламент 31.10): выбывшие делятся на погибших и раненых по соотношению сил своей стороны
[Test]
public void Battle_DeadAndWounded_ShareBySideRatio()
{
var b = new BattleSystem(BattleRules.NoRandom);
BattleOutcome Fight(int att, int def) => b.Resolve(new BattleRequest { Attackers = att, Defenders = def });
BattleOutcome even = Fight(1000, 1000); // 1:1 -- выбыло по 450, погибших 30%
Assert.AreEqual(135, even.AttackerDeadUnits[0], "равный бой: 30% от 450");
Assert.AreEqual(315, even.AttackerWoundedUnits[0]);
Assert.AreEqual(135, even.DefenderDeadUnits[0]);
BattleOutcome rout = Fight(5000, 1000); // 5:1: слабая сторона выбыло 880, её соотношение 1:5 -- погибших 70%
Assert.AreEqual(616, rout.DefenderDeadUnits[0], "разгром: 70% от 880");
Assert.AreEqual(264, rout.DefenderWoundedUnits[0]);
Assert.AreEqual(110, rout.AttackerDeadUnits[0], "уверенная победа (5:1, за 3:1): 20% от 550");
Assert.AreEqual(440, rout.AttackerWoundedUnits[0]);
BattleOutcome twoToOne = Fight(1000, 2000); // нападающий 1:2 -- погибших 45%
int expected = twoToOne.AttackerLosses * 45 / 100; // 45% от 595 = 267,75 -- дробная часть решается броском
Assert.IsTrue(twoToOne.AttackerDeadUnits[0] == expected || twoToOne.AttackerDeadUnits[0] == expected + 1, "поражение 1:2: 45%");
// Выжившие и общие потери от раненых не меняются: раненые -- часть выбывших
Assert.AreEqual(1000 - 450, even.Survivors);
Assert.AreEqual(450, even.AttackerLosses);
}
// ---------------- Раненые и госпиталь (решения 31.10, п. 7) ----------------
// Армия с одним походом из 40 воинов (30 пехоты, 10 кавалерии), временные места -- 20
static ArmyService ArmyWithMarch(int marchId = 1)
{
var army = new ArmyService(2, new HospitalRules { TemporaryBeds = 20 });
GameServices.Register<IArmy>(army);
army.AddTroops(0, 30);
army.AddTroops(2, 10);
Assert.IsTrue(army.TakeForMarch(marchId, 40));
return army;
}
[Test]
public void Hospital_WoundedReturnAndHeal_ByBedsAndTime()
{
const long H = 5 * 60_000; // игровой час = 5 реальных минут
var army = ArmyWithMarch();
var healed = new List<SoldiersHealedEvent>();
EventBus.Subscribe<SoldiersHealedEvent>(e => healed.Add(e));
// Вернулись 10 пехоты, ранены 15 пехоты и 9 кавалерии; 6 погибли
Assert.IsTrue(army.ReturnUnits(1, new[] { 10, 0, 0, 0 }, new[] { 15, 0, 9, 0 }, 0));
Assert.AreEqual(10, army.SoldiersAtHome, "раненые не воюют и в городе не числятся");
Assert.AreEqual(24, army.WoundedTotal);
Assert.AreEqual(2, army.Healing.Count, "20 мест: 15 пехоты и 5 кавалерии");
Assert.AreEqual(4, army.GetWoundedWaiting(2), "4 кавалериста ждут места");
Assert.AreEqual(4 * H, army.Healing[0].EndMs, "пехота -- 4 игровых часа");
Assert.AreEqual(6 * H, army.Healing[1].EndMs, "кавалерия -- 6");
Assert.IsFalse(army.ReturnUnits(1, new[] { 10, 0, 0, 0 }, new[] { 15, 0, 9, 0 }, 0), "повторное возвращение -- никого");
Assert.AreEqual(1, army.ProcessHealing(4 * H));
Assert.AreEqual(25, army.GetTroops(0), "15 пехотинцев вылечены");
Assert.AreEqual(0, army.GetWoundedWaiting(2), "освободились места -- ждущие легли в тот же момент");
Assert.AreEqual(4 * H + 6 * H, army.Healing[1].EndMs, "их лечение -- от момента, когда освободилось место");
Assert.AreEqual(0, army.ProcessHealing(4 * H), "повторный расчёт ничего не лечит");
Assert.AreEqual(2, army.ProcessHealing(100 * H), "офлайн: остальные -- за один расчёт");
Assert.AreEqual(9, army.GetTroops(2), "все 9 раненых кавалеристов вылечены");
Assert.AreEqual(0, army.WoundedTotal);
Assert.AreEqual(3, healed.Count);
}
[Test]
public void Hospital_WoundedAreStillPeople_InPopulation()
{
var army = ArmyWithMarch();
GameServices.Register<IEconomy>(new PopulationTests.FakeEconomy());
GameServices.Register<ISettlement>(new PopulationTests.FakeSettlement { TownHallLevel = 1 });
var population = new PopulationService(new PopulationRules());
GameServices.Register<IPopulation>(population);
int before = population.Total;
army.ReturnUnits(1, new[] { 20, 0, 5, 0 }, new[] { 8, 0, 3, 0 }, 0); // 4 погибли
Assert.AreEqual(before - 4, population.Total, "погибшие ушли из людей, раненые -- нет");
army.ProcessHealing(long.MaxValue / 4);
Assert.AreEqual(before - 4, population.Total, "вылеченные -- не новые люди");
}
[Test]
public void Hospital_SaveRestore_AndHealCost()
{
var army = ArmyWithMarch();
army.ReturnUnits(1, null, new[] { 25, 0, 10, 0 }, 1000); // 20 лечатся, 15 ждут
string saved = army.Capture();
var copy = new ArmyService(2, new HospitalRules { TemporaryBeds = 20 });
copy.Restore(saved);
Assert.AreEqual(saved, copy.Capture(), "раненые, группы и сроки сохраняются");
Assert.AreEqual(35, copy.WoundedTotal);
// Цена лечения: не хватает -- раненые ждут, ничего не списано
GameServices.Clear();
var economy = new PopulationTests.FakeEconomy();
GameServices.Register<IEconomy>(economy);
var paid = new ArmyService(2, new HospitalRules
{
TemporaryBeds = 20,
HealCostPerSoldier = new[] { new[] { new ResourceAmount(ResourceType.Food, 2) }, null, null, null }
});
GameServices.Register<IArmy>(paid);
paid.AddTroops(0, 10);
paid.TakeForMarch(1, 10);
paid.ReturnUnits(1, null, new[] { 10, 0, 0, 0 }, 0);
Assert.AreEqual(0, paid.Healing.Count, "еды нет -- лечение не начато");
Assert.AreEqual(10, paid.GetWoundedWaiting(0));
economy.Set(ResourceType.Food, 100);
paid.ProcessHealing(1);
Assert.AreEqual(1, paid.Healing.Count, "еда появилась -- легли");
Assert.AreEqual(80, (int)economy.Get(ResourceType.Food), "2 еды за воина");
}
[Test]
public void World_BattleWounded_TravelHomeWithArmy()
{
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem(BattleRules.NoRandom));
MarchData m = world.StartMarch(LocalPlayer.Id, WorldMapLayout.PlayerCityId, WorldMapLayout.BossCastleId, 40, 0, out _);
world.AssignUnits(m, new[] { 40, 0, 0, 0 });
world.Advance(m.arriveMs, new List<WorldEvent>());
int wounded = 0;
foreach (var w in m.woundedUnits) wounded += w;
Assert.Greater(wounded, 0, "в бою есть раненые");
Assert.LessOrEqual(m.survivors + wounded, 40, "выжившие + раненые не больше ушедших");
}
}