Раненые и госпиталь (решения 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
+56 -3
View File
@@ -31,6 +31,7 @@ public sealed class BattleSystem : IBattleSystem
private readonly BattleRules rules;
private readonly int[] power;
private readonly int[] lossRatio, strongLoss, weakLoss; // кривая потерь (проверенная копия из правил)
private readonly int[] deadRatio, deadShare; // доля погибших среди выбывших (проверенная копия)
public BattleRules Rules => rules;
@@ -61,6 +62,16 @@ public sealed class BattleSystem : IBattleSystem
lossRatio = (int[])src.LossRatioPermille.Clone();
strongLoss = (int[])src.StrongLossPermille.Clone();
weakLoss = (int[])src.WeakLossPermille.Clone();
// Доля погибших: та же проверка -- ошибка в таблице не ломает бой
bool deadOk = r.DeadRatioPermille != null && r.DeadSharePermille != null && r.DeadRatioPermille.Length > 0
&& r.DeadRatioPermille.Length == r.DeadSharePermille.Length;
for (int i = 0; deadOk && i < r.DeadRatioPermille.Length; i++)
deadOk = r.DeadRatioPermille[i] > 0 && r.DeadSharePermille[i] >= 0 && r.DeadSharePermille[i] <= 1000
&& (i == 0 || r.DeadRatioPermille[i] > r.DeadRatioPermille[i - 1]);
var deadSrc = deadOk ? r : BattleRules.Default;
deadRatio = (int[])deadSrc.DeadRatioPermille.Clone();
deadShare = (int[])deadSrc.DeadSharePermille.Clone();
// Предел не ниже последней точки и не выше 100%
weakLossMax = Math.Min(1000, Math.Max(weakLoss[weakLoss.Length - 1], r.WeakLossMaxPermille));
}
@@ -167,12 +178,19 @@ public sealed class BattleSystem : IBattleSystem
// Потери по кривой (№14): победитель -- сильная сторона, проигравший -- слабая; при ничьей соотношение 1:1
Fraction weaker = victory ? defPower : attPower;
Fraction stronger = victory ? attPower : defPower;
int[] attDead = Losses(att, weaker, stronger, isWeak: !victory, request.Seed, side: 1, rounding);
int[] defDead = Losses(def, weaker, stronger, isWeak: victory, request.Seed, side: 2, rounding);
int[] attOut = Losses(att, weaker, stronger, isWeak: !victory, request.Seed, side: 1, rounding);
int[] defOut0 = Losses(def, weaker, stronger, isWeak: victory, request.Seed, side: 2, rounding);
// Раненых пока нет: доля погибших от тяжести поражения (№15) ждёт чисел от команды
// Выбывшие -- погибшие и раненые (решения 31.10, п. 7): доля погибших -- от соотношения сил своей стороны
int[] attDead = Dead(attOut, attPower, defPower, request.Seed, side: 1, rounding);
int[] defDead = Dead(defOut0, defPower, attPower, request.Seed, side: 2, rounding);
int[] attWounded = new int[TroopType.Count];
int[] defWounded = new int[TroopType.Count];
for (int i = 0; i < TroopType.Count; i++)
{
attWounded[i] = attOut[i] - attDead[i];
defWounded[i] = defOut0[i] - defDead[i];
}
var attAlive = new int[TroopType.Count];
var defAlive = new int[TroopType.Count];
@@ -322,6 +340,41 @@ public sealed class BattleSystem : IBattleSystem
return lost;
}
// Погибшие среди выбывших каждого типа: доля по соотношению сил стороны (own / enemy), дробная часть -- бросок
// из своего потока (не тот, что у потерь и удачи). Прогноз: Floor -- меньше погибших, Ceiling -- больше.
int[] Dead(int[] outUnits, Fraction own, Fraction enemy, long seed, int side, Rounding rounding)
{
var dead = new int[TroopType.Count];
DeadShare(own, enemy, out BigInteger shareNum, out BigInteger shareDen);
for (int i = 0; i < TroopType.Count; i++)
{
if (outUnits[i] <= 0) continue;
BigInteger num = (BigInteger)outUnits[i] * shareNum;
BigInteger den = 1000 * shareDen;
BigInteger whole = BigInteger.DivRem(num, den, out BigInteger rest);
bool extra = !rest.IsZero && (rounding == Rounding.Ceiling
|| rounding == Rounding.Roll && (BigInteger)LossRoll(seed, side + 8, i) * den < rest * (BigInteger.One << 32));
dead[i] = (int)BigInteger.Min(outUnits[i], whole + (extra ? 1 : 0));
}
return dead;
}
// Доля погибших (в десятых долях процента, дробью) при соотношении сил стороны R = 1000 * own / enemy
void DeadShare(Fraction own, Fraction enemy, out BigInteger num, out BigInteger den)
{
int last = deadRatio.Length - 1;
if (enemy.N.IsZero) { num = deadShare[last]; den = 1; return; } // у врага нет силы -- как при самом большом перевесе
BigInteger rn = 1000 * own.N * enemy.D;
BigInteger rd = own.D * enemy.N;
if (rn <= deadRatio[0] * rd) { num = deadShare[0]; den = 1; return; }
if (rn >= deadRatio[last] * rd) { num = deadShare[last]; den = 1; return; }
int i = 0;
while (rn >= deadRatio[i + 1] * rd) i++;
long span = deadRatio[i + 1] - deadRatio[i];
num = (BigInteger)deadShare[i] * span * rd + (BigInteger)(deadShare[i + 1] - deadShare[i]) * (rn - deadRatio[i] * rd);
den = span * rd;
}
static int[] Units(int[] units, int count)
{
var result = new int[TroopType.Count];