Бой: отдельный статус среди выбывших (доля не утверждена -- 0, выключено), версия правил 8. Армия: пропавшие из похода -- в группу; окно 6 игровых часов после обратного пути, возвращение 2-4 группами, 70% здоровыми / 30% ранеными, остальные списываются; сохранение без повторного возвращения. Население: занимают места, но не едят. Окна: сколько пропало и сколько ещё может вернуться. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
425 lines
23 KiB
C#
425 lines
23 KiB
C#
using System;
|
|
using System.Numerics;
|
|
|
|
// МОДУЛЬ БОЕВОЙ СИСТЕМЫ: урон, потери, исход. Документ: Docs/Modules/Battle.md
|
|
// Без Unity и без float: этот же код без изменений сможет считать бои на сервере.
|
|
//
|
|
// Правила (стартовые числа -- в BattleRules; решения №11-13 комплекта требований от 29.10.2026):
|
|
// - сила стороны = сумма по типам: воины x сила типа x (100% + контры + осадный бонус)
|
|
// x (100% + бонусы стороны) x состояния (голод, усталость) x (100% + удача);
|
|
// - контры: пехота сильнее кавалерии, кавалерия сильнее лучников, лучники сильнее пехоты;
|
|
// бонус типа против врага взвешивается по составу врага;
|
|
// - осадные: против стен сильнее, в поле слабее; стены усиливают защитников;
|
|
// - удача: каждой стороне от -5% до +5%, ПОСЛЕ всех модификаторов, из Seed -- тот же Seed даёт тот же итог (№12);
|
|
// - сила считается точной дробью (BigInteger) и округляется один раз -- только для показа (№12);
|
|
// - побеждает тот, у кого итоговая сила больше; ничья -- защитнику;
|
|
// - потери (№14): доля выбывших каждого типа -- плавная функция соотношения итоговых сил (сильная / слабая)
|
|
// по опорным точкам BattleRules: 1:1 -- 45% / 45%, 3:1 -- 16% / 74%, 5:1 -- 11% / 88%. Одинаково для PvE, PvP и Круга;
|
|
// - округление потерь: целая часть гарантирована, дробная -- шанс ещё одной потери,
|
|
// из отдельного потока случайности (№13). Раненых пока нет, все выбывшие -- погибшие.
|
|
public sealed class BattleSystem : IBattleSystem
|
|
{
|
|
// Точная неотрицательная дробь: сила x100 = N / D
|
|
struct Fraction
|
|
{
|
|
public BigInteger N, D;
|
|
public long Floor => (long)(N / D);
|
|
}
|
|
|
|
enum Rounding { Roll, Floor, Ceiling } // бой -- бросок по №13; прогноз -- лучший и худший случай
|
|
|
|
private readonly BattleRules rules;
|
|
private readonly int[] power;
|
|
private readonly int[] lossRatio, strongLoss, weakLoss; // кривая потерь (проверенная копия из правил)
|
|
private readonly int[] deadRatio, deadShare; // доля погибших среди выбывших (проверенная копия)
|
|
|
|
public BattleRules Rules => rules;
|
|
|
|
public BattleSystem() : this(BattleRules.Default) { }
|
|
|
|
public BattleSystem(BattleRules rules)
|
|
{
|
|
this.rules = rules ?? BattleRules.Default;
|
|
power = new int[TroopType.Count];
|
|
var defaults = BattleRules.Default.UnitPower;
|
|
for (int i = 0; i < TroopType.Count; i++)
|
|
power[i] = this.rules.UnitPower != null && i < this.rules.UnitPower.Length && this.rules.UnitPower[i] > 0
|
|
? this.rules.UnitPower[i] : defaults[i];
|
|
|
|
// Кривая из таблицы, если она корректна, иначе -- значения по умолчанию (ошибка в Inspector не ломает бой)
|
|
var r = this.rules;
|
|
bool ok = r.LossRatioPermille != null && r.StrongLossPermille != null && r.WeakLossPermille != null
|
|
&& r.LossRatioPermille.Length > 0
|
|
&& r.LossRatioPermille.Length == r.StrongLossPermille.Length
|
|
&& r.LossRatioPermille.Length == r.WeakLossPermille.Length
|
|
&& r.LossRatioPermille[0] == 1000;
|
|
for (int i = 0; ok && i < r.LossRatioPermille.Length; i++)
|
|
{
|
|
ok = r.StrongLossPermille[i] >= 0 && r.StrongLossPermille[i] <= 1000 && r.WeakLossPermille[i] >= 0 && r.WeakLossPermille[i] <= 1000
|
|
&& (i == 0 || r.LossRatioPermille[i] > r.LossRatioPermille[i - 1]);
|
|
}
|
|
var src = ok ? r : BattleRules.Default;
|
|
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));
|
|
}
|
|
|
|
private int weakLossMax;
|
|
|
|
// Доля выбывших стороны (в десятых долях процента, дробью) при соотношении сил stronger / weaker (решение №14)
|
|
void LossShare(Fraction weaker, Fraction stronger, bool isWeak, out BigInteger num, out BigInteger den)
|
|
{
|
|
int[] values = isWeak ? weakLoss : strongLoss;
|
|
int last = lossRatio.Length - 1;
|
|
if (weaker.N.IsZero)
|
|
{
|
|
// У слабой стороны нулевая сила: сильная не теряет никого, слабая -- предел разгрома
|
|
num = isWeak ? weakLossMax : 0;
|
|
den = 1;
|
|
return;
|
|
}
|
|
|
|
// Соотношение сил в десятых долях процента: R = 1000 * stronger / weaker = rn / rd
|
|
BigInteger rn = 1000 * stronger.N * weaker.D;
|
|
BigInteger rd = stronger.D * weaker.N;
|
|
if (rn <= lossRatio[0] * rd) { num = values[0]; den = 1; return; }
|
|
if (rn >= lossRatio[last] * rd)
|
|
{
|
|
if (!isWeak) { num = values[last]; den = 1; return; }
|
|
// Дальше последней точки слабая сторона плавно стремится к пределу: v = max - (max - v_last) * R_last / R
|
|
num = (BigInteger)weakLossMax * rn - (BigInteger)(weakLossMax - values[last]) * lossRatio[last] * rd;
|
|
den = rn;
|
|
return;
|
|
}
|
|
|
|
int i = 0;
|
|
while (rn >= lossRatio[i + 1] * rd) i++; // lossRatio[i] <= R < lossRatio[i + 1]
|
|
long span = lossRatio[i + 1] - lossRatio[i];
|
|
// v = v_i + (v_{i+1} - v_i) * (R - p_i) / span -- точной дробью
|
|
num = (BigInteger)values[i] * span * rd + (BigInteger)(values[i + 1] - values[i]) * (rn - lossRatio[i] * rd);
|
|
den = span * rd;
|
|
}
|
|
|
|
// Контра: насколько тип attackerType сильнее против defenderType, в процентах
|
|
public int Counter(int attackerType, int defenderType)
|
|
{
|
|
if (attackerType == TroopType.Infantry && defenderType == TroopType.Cavalry) return rules.CounterPercent;
|
|
if (attackerType == TroopType.Cavalry && defenderType == TroopType.Archers) return rules.CounterPercent;
|
|
if (attackerType == TroopType.Archers && defenderType == TroopType.Infantry) return rules.CounterPercent;
|
|
return 0;
|
|
}
|
|
|
|
public BattleOutcome Resolve(BattleRequest request)
|
|
{
|
|
return Fight(request, Luck(request.Seed, 1), Luck(request.Seed, 2), Rounding.Roll);
|
|
}
|
|
|
|
// Прогноз (№11): качественный риск и точный шанс победы. Итог конкретного боя не раскрывается.
|
|
public BattleForecast Forecast(BattleRequest request)
|
|
{
|
|
int r = Math.Max(0, rules.RandomPercent * 10);
|
|
var f = new BattleForecast
|
|
{
|
|
AttackerPowerMin = long.MaxValue, DefenderPowerMin = long.MaxValue,
|
|
AttackerLossesMin = int.MaxValue
|
|
};
|
|
foreach (int attLuck in new[] { -r, r })
|
|
foreach (int defLuck in new[] { -r, r })
|
|
{
|
|
BattleOutcome best = Fight(request, attLuck, defLuck, Rounding.Floor);
|
|
BattleOutcome worst = Fight(request, attLuck, defLuck, Rounding.Ceiling);
|
|
f.AttackerPowerMin = Math.Min(f.AttackerPowerMin, best.AttackerPower);
|
|
f.AttackerPowerMax = Math.Max(f.AttackerPowerMax, best.AttackerPower);
|
|
f.DefenderPowerMin = Math.Min(f.DefenderPowerMin, best.DefenderPower);
|
|
f.DefenderPowerMax = Math.Max(f.DefenderPowerMax, best.DefenderPower);
|
|
f.AttackerLossesMin = Math.Min(f.AttackerLossesMin, best.AttackerLosses);
|
|
f.AttackerLossesMax = Math.Max(f.AttackerLossesMax, worst.AttackerLosses);
|
|
}
|
|
|
|
// Шанс победы: все сочетания удачи сторон равновероятны -- считаем, при скольких нападающий сильнее
|
|
Sides(request, out Fraction att, out Fraction def, out _, out _, out _, out _);
|
|
BigInteger x = att.N * def.D, y = def.N * att.D; // нападающий побеждает, если x * (1000 + a) > y * (1000 + d)
|
|
long wins = 0, total = (long)(2 * r + 1) * (2 * r + 1);
|
|
for (int a = -r; a <= r; a++)
|
|
{
|
|
BigInteger t = x * (1000 + a);
|
|
if (y.IsZero) { if (t > 0) wins += 2 * r + 1; continue; }
|
|
// нужно y * (1000 + d) < t, то есть 1000 + d <= ceil(t / y) - 1
|
|
BigInteger dMax = (t + y - 1) / y - 1 - 1000;
|
|
if (dMax < -r) continue;
|
|
wins += dMax >= r ? 2 * r + 1 : (long)dMax + r + 1;
|
|
}
|
|
f.WinChancePermille = (int)((wins * 1000 + total / 2) / total);
|
|
f.Chance = wins == total ? BattleChance.Advantage : wins == 0 ? BattleChance.HighRisk : BattleChance.Uncertain;
|
|
return f;
|
|
}
|
|
|
|
BattleOutcome Fight(BattleRequest request, int attLuckPermille, int defLuckPermille, Rounding rounding)
|
|
{
|
|
Sides(request, out Fraction attBase, out Fraction defBase, out int[] att, out int[] def,
|
|
out int attCondition, out int defCondition);
|
|
Fraction attPower = WithLuck(attBase, attLuckPermille);
|
|
Fraction defPower = WithLuck(defBase, defLuckPermille);
|
|
|
|
bool victory = attPower.N * defPower.D > defPower.N * attPower.D; // ничья -- в пользу защитника
|
|
|
|
// Потери по кривой (№14): победитель -- сильная сторона, проигравший -- слабая; при ничьей соотношение 1:1
|
|
Fraction weaker = victory ? defPower : attPower;
|
|
Fraction stronger = victory ? attPower : defPower;
|
|
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);
|
|
|
|
// Выбывшие: сначала пропавшие без вести (решения 01.11, п. 5), остальные -- погибшие и раненые (решения 31.10, п. 7):
|
|
// доля погибших -- от соотношения сил своей стороны. Солдат -- ровно в одном статусе
|
|
int[] attMissing = Missing(attOut, request.Seed, side: 1, rounding);
|
|
int[] defMissing = Missing(defOut0, request.Seed, side: 2, rounding);
|
|
int[] attRest = new int[TroopType.Count], defRest = new int[TroopType.Count];
|
|
for (int i = 0; i < TroopType.Count; i++) { attRest[i] = attOut[i] - attMissing[i]; defRest[i] = defOut0[i] - defMissing[i]; }
|
|
int[] attDead = Dead(attRest, attPower, defPower, request.Seed, side: 1, rounding);
|
|
int[] defDead = Dead(defRest, 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] = attRest[i] - attDead[i];
|
|
defWounded[i] = defRest[i] - defDead[i];
|
|
}
|
|
|
|
var attAlive = new int[TroopType.Count];
|
|
var defAlive = new int[TroopType.Count];
|
|
var defOut = new int[TroopType.Count];
|
|
for (int i = 0; i < TroopType.Count; i++)
|
|
{
|
|
attAlive[i] = att[i] - attOut[i];
|
|
defAlive[i] = def[i] - defOut0[i];
|
|
defOut[i] = defOut0[i];
|
|
}
|
|
|
|
return new BattleOutcome
|
|
{
|
|
Victory = victory,
|
|
BattleId = request.Seed,
|
|
RulesVersion = rules.Version,
|
|
AttackerLosses = Sum(attOut),
|
|
DefenderLosses = Sum(defOut),
|
|
Survivors = Sum(attAlive),
|
|
AttackerPower = attPower.Floor, // округление -- только для показа
|
|
DefenderPower = defPower.Floor,
|
|
AttackerLuckPermille = attLuckPermille,
|
|
DefenderLuckPermille = defLuckPermille,
|
|
AttackerConditionPercent = attCondition,
|
|
DefenderConditionPercent = defCondition,
|
|
AttackerUnitsBefore = att,
|
|
DefenderUnitsBefore = def,
|
|
AttackerSurvivorUnits = attAlive,
|
|
DefenderSurvivorUnits = defAlive,
|
|
AttackerDeadUnits = attDead,
|
|
DefenderDeadUnits = defDead,
|
|
AttackerWoundedUnits = attWounded,
|
|
DefenderWoundedUnits = defWounded,
|
|
AttackerMissingUnits = attMissing,
|
|
DefenderMissingUnits = defMissing,
|
|
DefenderLossUnits = defOut
|
|
};
|
|
}
|
|
|
|
// Силы сторон без удачи (точные дроби) и состав сторон
|
|
void Sides(BattleRequest request, out Fraction attPower, out Fraction defPower, out int[] att, out int[] def,
|
|
out int attCondition, out int defCondition)
|
|
{
|
|
att = Units(request.AttackerUnits, request.Attackers);
|
|
def = Units(request.DefenderUnits, request.Defenders);
|
|
attPower = SidePower(att, def, attacking: true, request.DefenderFortified, request.AttackerBonusPercent,
|
|
request.AttackerConditions, out attCondition);
|
|
defPower = SidePower(def, att, attacking: false, request.DefenderFortified,
|
|
request.DefenderBonusPercent + (request.DefenderFortified ? rules.WallsPercent : 0),
|
|
request.DefenderConditions, out defCondition);
|
|
}
|
|
|
|
// Сколько силы оставляют состояния стороны, в процентах (для показа). Штрафы -- последовательно: голод и усталость -- 40%
|
|
public int ConditionPercent(BattleCondition conditions)
|
|
{
|
|
ConditionFactor(conditions, out BigInteger num, out BigInteger den);
|
|
return (int)(num * 100 / den);
|
|
}
|
|
|
|
void ConditionFactor(BattleCondition conditions, out BigInteger num, out BigInteger den)
|
|
{
|
|
num = 1; den = 1;
|
|
if ((conditions & BattleCondition.Famine) != 0) { num *= Math.Max(0, rules.FamineStrengthPercent); den *= 100; }
|
|
if ((conditions & BattleCondition.Fatigue) != 0) { num *= Math.Max(0, rules.FatigueStrengthPercent); den *= 100; }
|
|
}
|
|
|
|
// Сила стороны x100 точной дробью. Порядок: контры и осадные -> бонусы стороны -> состояния; удача -- в WithLuck.
|
|
// Контра по составу врага (counterSum / enemyTotal) остаётся дробью -- без промежуточного округления (№12).
|
|
Fraction SidePower(int[] units, int[] enemy, bool attacking, bool fortified, int bonusPercent,
|
|
BattleCondition conditions, out int conditionPercent)
|
|
{
|
|
int enemyTotal = Sum(enemy);
|
|
int e = Math.Max(1, enemyTotal);
|
|
BigInteger raw = 0;
|
|
for (int i = 0; i < TroopType.Count; i++)
|
|
{
|
|
if (units[i] <= 0) continue;
|
|
|
|
long counterSum = 0;
|
|
if (enemyTotal > 0)
|
|
for (int j = 0; j < TroopType.Count; j++) counterSum += (long)Counter(i, j) * enemy[j];
|
|
|
|
int siegePercent = 0;
|
|
if (i == TroopType.Siege) siegePercent = attacking && fortified ? rules.SiegeVsWallsPercent : rules.SiegeInFieldPercent;
|
|
|
|
// воины x сила x (100 + осадный% + контра%), где контра% = counterSum / e
|
|
BigInteger term = (BigInteger)units[i] * power[i] * ((100L + siegePercent) * e + counterSum);
|
|
if (term > 0) raw += term;
|
|
}
|
|
|
|
ConditionFactor(conditions, out BigInteger condNum, out BigInteger condDen);
|
|
conditionPercent = (int)(condNum * 100 / condDen);
|
|
return new Fraction
|
|
{
|
|
N = raw * Math.Max(0, 100 + bonusPercent) * condNum,
|
|
D = (BigInteger)e * 100 * condDen
|
|
};
|
|
}
|
|
|
|
static Fraction WithLuck(Fraction power, int luckPermille)
|
|
{
|
|
return new Fraction { N = power.N * (1000 + luckPermille), D = power.D * 1000 };
|
|
}
|
|
|
|
// Удача стороны в десятых долях процента: от -RandomPercent*10 до +RandomPercent*10.
|
|
// Считается из Seed (SplitMix64) -- одинаково на любой платформе, System.Random для этого не годится.
|
|
public int Luck(long seed, int side)
|
|
{
|
|
int range = rules.RandomPercent * 10;
|
|
if (range <= 0) return 0;
|
|
ulong x = Mix((ulong)seed + 0x9E3779B97F4A7C15UL * (ulong)side);
|
|
return (int)(x % (ulong)(2 * range + 1)) - range;
|
|
}
|
|
|
|
// Отдельный поток случайности для округления потерь (№13): своя "соль" на каждую сторону и тип войск,
|
|
// не пересекается с удачей. Возвращает число от 0 до 2^32 - 1.
|
|
static ulong LossRoll(long seed, int side, int type)
|
|
{
|
|
ulong x = Mix(((ulong)seed ^ 0xD1B54A32D192ED03UL) + 0x9E3779B97F4A7C15UL * (ulong)(16 * side + type + 1));
|
|
return x >> 32;
|
|
}
|
|
|
|
static ulong Mix(ulong x)
|
|
{
|
|
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL;
|
|
x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL;
|
|
return x ^ (x >> 31);
|
|
}
|
|
|
|
// Потери каждого типа (дробью): units x доля выбывших по кривой (№14).
|
|
// Целая часть гарантирована, дробная -- шанс ещё одной потери (№13).
|
|
int[] Losses(int[] units, Fraction weaker, Fraction stronger, bool isWeak, long seed, int side, Rounding rounding)
|
|
{
|
|
var lost = new int[TroopType.Count];
|
|
LossShare(weaker, stronger, isWeak, out BigInteger shareNum, out BigInteger shareDen);
|
|
for (int i = 0; i < TroopType.Count; i++)
|
|
{
|
|
if (units[i] <= 0) continue;
|
|
|
|
BigInteger num = (BigInteger)units[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, i) * den < rest * (BigInteger.One << 32));
|
|
BigInteger l = whole + (extra ? 1 : 0);
|
|
lost[i] = (int)BigInteger.Min(units[i], l);
|
|
}
|
|
return lost;
|
|
}
|
|
|
|
// Пропавшие без вести среди выбывших каждого типа: доля из правил (не утверждена; 0 -- никого), дробная часть -- бросок
|
|
// из своего потока. Прогноз: Floor -- меньше пропавших, Ceiling -- больше
|
|
int[] Missing(int[] outUnits, long seed, int side, Rounding rounding)
|
|
{
|
|
var missing = new int[TroopType.Count];
|
|
int share = Math.Max(0, Math.Min(1000, rules.MissingSharePermille));
|
|
if (share == 0) return missing;
|
|
for (int i = 0; i < TroopType.Count; i++)
|
|
{
|
|
if (outUnits[i] <= 0) continue;
|
|
long num = (long)outUnits[i] * share;
|
|
long whole = num / 1000, rest = num % 1000;
|
|
bool extra = rest != 0 && (rounding == Rounding.Ceiling
|
|
|| rounding == Rounding.Roll && (LossRoll(seed, side + 24, i) * 1000UL >> 32) < (ulong)rest);
|
|
missing[i] = (int)Math.Min(outUnits[i], whole + (extra ? 1 : 0));
|
|
}
|
|
return missing;
|
|
}
|
|
|
|
// Погибшие среди выбывших каждого типа: доля по соотношению сил стороны (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];
|
|
if (units != null)
|
|
{
|
|
for (int i = 0; i < TroopType.Count && i < units.Length; i++) result[i] = Math.Max(0, units[i]);
|
|
}
|
|
else
|
|
{
|
|
result[TroopType.Infantry] = Math.Max(0, count); // состав неизвестен -- считаем пехотой
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int Sum(int[] values)
|
|
{
|
|
int n = 0;
|
|
foreach (var v in values) n += v;
|
|
return n;
|
|
}
|
|
}
|