using System; // МОДУЛЬ БОЕВОЙ СИСТЕМЫ: урон, потери, исход. Документ: Docs/Modules/Battle.md // Без Unity и без float: этот же код без изменений сможет считать бои на сервере. // // Правила (стартовые числа -- в BattleRules, утверждает команда): // - сила стороны = сумма по типам: воины x сила типа x (100% + контры + осадный бонус) x (100% + бонусы стороны); // - контры: пехота сильнее кавалерии, кавалерия сильнее лучников, лучники сильнее пехоты; // бонус типа против врага взвешивается по составу врага; // - осадные: против стен сильнее, в поле слабее; стены усиливают защитников; // - случайность: каждой стороне от -5% до +5%, считается из Seed -- тот же Seed даёт тот же итог; // - побеждает тот, у кого итоговая сила больше; ничья -- защитнику; // - проигравший теряет половину каждого типа; победитель -- до половины, тем больше, чем ровнее был бой. // Это не окончательная формула потерь (план от 27.10.2026): раненых пока нет, все выбывшие -- погибшие. public sealed class BattleSystem : IBattleSystem { private readonly BattleRules rules; private readonly int[] power; 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]; } // Контра: насколько тип 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)); } // Прогноз: бой при крайних значениях случайности. Итог конкретного боя не раскрывается. public BattleForecast Forecast(BattleRequest request) { int r = rules.RandomPercent * 10; var f = new BattleForecast { AttackerPowerMin = long.MaxValue, DefenderPowerMin = long.MaxValue, AttackerLossesMin = int.MaxValue }; bool anyWin = false, anyLoss = false; foreach (int attLuck in new[] { -r, r }) foreach (int defLuck in new[] { -r, r }) { BattleOutcome o = Fight(request, attLuck, defLuck); if (o.Victory) anyWin = true; else anyLoss = true; f.AttackerPowerMin = Math.Min(f.AttackerPowerMin, o.AttackerPower); f.AttackerPowerMax = Math.Max(f.AttackerPowerMax, o.AttackerPower); f.DefenderPowerMin = Math.Min(f.DefenderPowerMin, o.DefenderPower); f.DefenderPowerMax = Math.Max(f.DefenderPowerMax, o.DefenderPower); f.AttackerLossesMin = Math.Min(f.AttackerLossesMin, o.AttackerLosses); f.AttackerLossesMax = Math.Max(f.AttackerLossesMax, o.AttackerLosses); } f.Chance = !anyLoss ? BattleChance.Advantage : !anyWin ? BattleChance.HighRisk : BattleChance.Uncertain; return f; } BattleOutcome Fight(BattleRequest request, int attLuckPermille, int defLuckPermille) { int[] att = Units(request.AttackerUnits, request.Attackers); int[] def = Units(request.DefenderUnits, request.Defenders); int attCondition = ConditionPercent(request.AttackerConditions); int defCondition = ConditionPercent(request.DefenderConditions); long attPower = SidePower(att, def, attacking: true, request.DefenderFortified, request.AttackerBonusPercent, attCondition, attLuckPermille); long defPower = SidePower(def, att, attacking: false, request.DefenderFortified, request.DefenderBonusPercent + (request.DefenderFortified ? rules.WallsPercent : 0), defCondition, defLuckPermille); bool victory = attPower > defPower; // ничья -- в пользу защитника // Потери в процентах: проигравший -- LoserLossPercent; победитель -- то же x (сила проигравшего / сила победителя) long stronger = Math.Max(attPower, defPower); long weaker = Math.Min(attPower, defPower); int[] attDead = Losses(att, weaker, stronger, isLoser: !victory); int[] defDead = Losses(def, weaker, stronger, isLoser: victory); // Раненых пока нет: доля раненых и погибших не утверждена (план от 27.10.2026) int[] attWounded = new int[TroopType.Count]; int[] defWounded = new int[TroopType.Count]; 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] - attDead[i] - attWounded[i]; defAlive[i] = def[i] - defDead[i] - defWounded[i]; defOut[i] = defDead[i] + defWounded[i]; } return new BattleOutcome { Victory = victory, BattleId = request.Seed, RulesVersion = rules.Version, AttackerLosses = Sum(attDead) + Sum(attWounded), DefenderLosses = Sum(defOut), Survivors = Sum(attAlive), AttackerPower = attPower, DefenderPower = defPower, AttackerLuckPermille = attLuckPermille, DefenderLuckPermille = defLuckPermille, AttackerConditionPercent = attCondition, DefenderConditionPercent = defCondition, AttackerUnitsBefore = att, DefenderUnitsBefore = def, AttackerSurvivorUnits = attAlive, DefenderSurvivorUnits = defAlive, AttackerDeadUnits = attDead, DefenderDeadUnits = defDead, AttackerWoundedUnits = attWounded, DefenderWoundedUnits = defWounded, DefenderLossUnits = defOut }; } // Сколько силы оставляют состояния стороны, в процентах. Штрафы -- последовательно: голод и усталость -- 40% public int ConditionPercent(BattleCondition conditions) { int percent = 100; if ((conditions & BattleCondition.Famine) != 0) percent = percent * rules.FamineStrengthPercent / 100; if ((conditions & BattleCondition.Fatigue) != 0) percent = percent * rules.FatigueStrengthPercent / 100; return Math.Max(0, percent); } // Сила стороны в сотых долях (x100), чтобы проценты считались в целых числах. // Порядок: контры и осадные -> бонусы стороны -> состояния (голод, усталость) -> случайность // (порядок бонусов и случайности -- открытый вопрос плана). long SidePower(int[] units, int[] enemy, bool attacking, bool fortified, int bonusPercent, int conditionPercent, int luckPermille) { int enemyTotal = Sum(enemy); long total = 0; for (int i = 0; i < TroopType.Count; i++) { if (units[i] <= 0) continue; long counterSum = 0; for (int j = 0; j < TroopType.Count; j++) counterSum += (long)Counter(i, j) * enemy[j]; long counterPercent = enemyTotal > 0 ? counterSum / enemyTotal : 0; int siegePercent = 0; if (i == TroopType.Siege) siegePercent = attacking && fortified ? rules.SiegeVsWallsPercent : rules.SiegeInFieldPercent; total += (long)units[i] * power[i] * (100 + counterPercent + siegePercent); } total = Math.Max(0, total * (100 + bonusPercent) / 100); total = total * conditionPercent / 100; return total * (1000 + luckPermille) / 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 = (ulong)seed + 0x9E3779B97F4A7C15UL * (ulong)side; x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL; x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL; x ^= x >> 31; return (int)(x % (ulong)(2 * range + 1)) - range; } // Потери каждого типа: проигравший -- LoserLossPercent; победитель -- LoserLossPercent x ratioNum / ratioDen. // Округление вниз: одиночный проигравший не гибнет -- открытый вопрос (Battle.md). int[] Losses(int[] units, long ratioNum, long ratioDen, bool isLoser) { var lost = new int[TroopType.Count]; for (int i = 0; i < TroopType.Count; i++) { if (units[i] <= 0) continue; long l = isLoser ? (long)units[i] * rules.LoserLossPercent / 100 : ratioDen > 0 ? (long)units[i] * rules.LoserLossPercent * ratioNum / (100 * ratioDen) : 0; lost[i] = (int)Math.Max(0, Math.Min(units[i], l)); } return lost; } 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; } }