diff --git a/Assets/Resources/BattleConfig.asset b/Assets/Resources/BattleConfig.asset index 8fde4df..f99ce49 100644 --- a/Assets/Resources/BattleConfig.asset +++ b/Assets/Resources/BattleConfig.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 7264ce138a844adc81fa69b77a27c8a6, type: 3} m_Name: BattleConfig m_EditorClassIdentifier: - version: 4 + version: 5 counterPercent: 25 wallsPercent: 20 siegeVsWallsPercent: 50 @@ -24,11 +24,18 @@ MonoBehaviour: - 1000 - 3000 - 5000 + - 7000 + - 10000 strongLossPermille: - 450 - 160 - 110 + - 110 + - 110 weakLossPermille: - 450 - 740 - 880 + - 920 + - 950 + weakLossMaxPermille: 990 diff --git a/Assets/Scripts/App/BattleConfig.cs b/Assets/Scripts/App/BattleConfig.cs index f07437e..c855ba2 100644 --- a/Assets/Scripts/App/BattleConfig.cs +++ b/Assets/Scripts/App/BattleConfig.cs @@ -28,11 +28,13 @@ public class BattleConfig : ScriptableObject [Header("Кривая потерь (решение №14, регламент 31.10, ТЕСТ)")] [Tooltip("Опорные точки соотношения сил (сильная / слабая) в десятых долях процента: 1000 = 1:1, 3000 = 3:1. Первая -- обязательно 1000, дальше по возрастанию")] - public int[] lossRatioPermille = { 1000, 3000, 5000 }; + public int[] lossRatioPermille = { 1000, 3000, 5000, 7000, 10000 }; [Tooltip("Доля выбывших у сильной стороны в каждой точке, десятые доли процента: 450 = 45%")] - public int[] strongLossPermille = { 450, 160, 110 }; + public int[] strongLossPermille = { 450, 160, 110, 110, 110 }; [Tooltip("Доля выбывших у слабой стороны в каждой точке, десятые доли процента: 740 = 74%")] - public int[] weakLossPermille = { 450, 740, 880 }; + public int[] weakLossPermille = { 450, 740, 880, 920, 950 }; + [Tooltip("Разгром дальше последней точки: слабая сторона плавно стремится к этому пределу, десятые доли процента. Решения 31.10, п. 5: 990 = 99%")] + public int weakLossMaxPermille = 990; [Tooltip("Голод: сколько силы остаётся, %. Утверждено 27.10.2026: -50% (остаётся 50). Проверяется на момент боя")] public int famineStrengthPercent = 50; @@ -67,6 +69,7 @@ public class BattleConfig : ScriptableObject LossRatioPermille = lossRatioPermille, StrongLossPermille = strongLossPermille, WeakLossPermille = weakLossPermille, + WeakLossMaxPermille = weakLossMaxPermille, RandomPercent = Mathf.Max(0, randomPercent), FamineStrengthPercent = Mathf.Clamp(famineStrengthPercent, 0, 100), FatigueStrengthPercent = Mathf.Clamp(fatigueStrengthPercent, 0, 100) diff --git a/Assets/Scripts/Modules/Battle/BattleRules.cs b/Assets/Scripts/Modules/Battle/BattleRules.cs index 9c48dd9..a0afe8e 100644 --- a/Assets/Scripts/Modules/Battle/BattleRules.cs +++ b/Assets/Scripts/Modules/Battle/BattleRules.cs @@ -6,7 +6,8 @@ // итог боя запоминает версию, по которой посчитан. public sealed class BattleRules { - public int Version = 4; // 2 -- голод и усталость; 3 -- сила без округления, потери шансом (№12, №13); 4 -- кривая потерь (№14) + public int Version = 5; // 2 -- голод и усталость; 3 -- сила без округления, потери шансом (№12, №13); 4 -- кривая потерь (№14); + // 5 -- разгром дальше 5:1 (решения по 8 вопросам Саши, 31.10, п. 5) // Сила одного воина каждого типа. В игре -- из GameData.Troops (там же цена и время обучения). public int[] UnitPower = { 4, 5, 9, 16 }; @@ -19,10 +20,14 @@ public sealed class BattleRules // Кривая потерь (решение №14, регламент 31.10, ТЕСТ): доля выбывших от каждого типа в зависимости от // соотношения итоговых сил (сильная / слабая), в десятых долях процента. Между опорными точками -- плавно - // (линейно), без порогов-переключателей. За последней точкой -- значения последней (рост разгрома дальше 5:1 не задан). - public int[] LossRatioPermille = { 1000, 3000, 5000 }; // 1:1, 3:1, 5:1 - public int[] StrongLossPermille = { 450, 160, 110 }; // сильная сторона: 45%, 16%, 11% - public int[] WeakLossPermille = { 450, 740, 880 }; // слабая сторона: 45%, 74%, 88% + // (линейно), без порогов-переключателей. + // Разгром (решения 31.10, п. 5): 7:1 -- 92%, 10:1 -- 95%, дальше слабая сторона плавно стремится к WeakLossMaxPermille (99%): + // v = max - (max - v_last) * R_last / R -- при 20:1 97%, при 100:1 98,6%; 100% не достигается. + // Сильная сторона за последней точкой -- значение последней точки (документ числа для неё не задаёт). + public int[] LossRatioPermille = { 1000, 3000, 5000, 7000, 10000 }; // 1:1, 3:1, 5:1, 7:1, 10:1 + public int[] StrongLossPermille = { 450, 160, 110, 110, 110 }; // сильная сторона: 45%, 16%, 11%, 11%, 11% + public int[] WeakLossPermille = { 450, 740, 880, 920, 950 }; // слабая сторона: 45%, 74%, 88%, 92%, 95% + public int WeakLossMaxPermille = 990; // верхний предел ожидаемых потерь слабой стороны: 99% // Состояния (утверждено 27.10.2026): сколько силы остаётся. Применяются последовательно: 50% x 80% = 40% public int FamineStrengthPercent = 50; // голод: -50% diff --git a/Assets/Scripts/Modules/Battle/BattleSystem.cs b/Assets/Scripts/Modules/Battle/BattleSystem.cs index feff21a..270307f 100644 --- a/Assets/Scripts/Modules/Battle/BattleSystem.cs +++ b/Assets/Scripts/Modules/Battle/BattleSystem.cs @@ -61,8 +61,12 @@ public sealed class BattleSystem : IBattleSystem lossRatio = (int[])src.LossRatioPermille.Clone(); strongLoss = (int[])src.StrongLossPermille.Clone(); weakLoss = (int[])src.WeakLossPermille.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) { @@ -70,8 +74,8 @@ public sealed class BattleSystem : IBattleSystem int last = lossRatio.Length - 1; if (weaker.N.IsZero) { - // У слабой стороны нулевая сила: сильная не теряет никого, слабая -- как при самом большом перевесе - num = isWeak ? values[last] : 0; + // У слабой стороны нулевая сила: сильная не теряет никого, слабая -- предел разгрома + num = isWeak ? weakLossMax : 0; den = 1; return; } @@ -80,7 +84,14 @@ public sealed class BattleSystem : IBattleSystem 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) { num = values[last]; 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] diff --git a/Assets/Scripts/Modules/Tests/ModuleTests.cs b/Assets/Scripts/Modules/Tests/ModuleTests.cs index e034e48..6070dde 100644 --- a/Assets/Scripts/Modules/Tests/ModuleTests.cs +++ b/Assets/Scripts/Modules/Tests/ModuleTests.cs @@ -556,6 +556,13 @@ public class ModuleTests Assert.AreEqual(550, five.AttackerLosses, "5:1 -- сильная теряет 11%"); Assert.AreEqual(880, five.DefenderLosses, "5:1 -- слабая теряет 88%"); + // Разгром дальше 5:1 (решения 31.10, п. 5): 7:1 -- 92%, 10:1 -- 95%, дальше -- плавно к 99%, но не 100% + Assert.AreEqual(920, Fight(7000, 1000).DefenderLosses, "7:1 -- 92%"); + Assert.AreEqual(950, Fight(10000, 1000).DefenderLosses, "10:1 -- 95%"); + Assert.AreEqual(970, Fight(20000, 1000).DefenderLosses, "20:1 -- 99 - 4 x 10/20 = 97%"); + Assert.AreEqual(986, Fight(100000, 1000).DefenderLosses, "100:1 -- 98,6%"); + Assert.AreEqual(1100, Fight(10000, 1000).AttackerLosses, "сильная сторона дальше 5:1 -- 11%"); + // Симметрия: тот же перевес у защитника -- те же доли (одинаково для PvE, PvP и Круга) BattleOutcome threeDef = Fight(1000, 3000); Assert.AreEqual(740, threeDef.AttackerLosses); @@ -563,7 +570,7 @@ public class ModuleTests // Плавность: при росте перевеса сильная теряет всё меньше, слабая -- всё больше, без скачков int prevStrong = 1000, prevWeak = -1; - for (int att = 1010; att <= 6000; att += 10) + for (int att = 1010; att <= 30000; att += 10) { BattleOutcome o = Fight(att, 1000); int strongPermille = (int)((long)o.AttackerLosses * 1000 / att); @@ -577,7 +584,7 @@ public class ModuleTests // Кривая из таблицы: другие точки -- другие потери; ошибка в таблице не ломает бой var custom = BattleRules.NoRandom; - custom.StrongLossPermille = new[] { 300, 100, 50 }; + custom.StrongLossPermille = new[] { 300, 100, 50, 50, 50 }; Assert.AreEqual(300, new BattleSystem(custom).Resolve(new BattleRequest { Attackers = 1000, Defenders = 1000 }).DefenderLosses); var broken = BattleRules.NoRandom; broken.LossRatioPermille = new[] { 3000, 1000 }; // не по возрастанию и разной длины diff --git a/Docs/Modules/Battle.md b/Docs/Modules/Battle.md index 61af562..410f598 100644 --- a/Docs/Modules/Battle.md +++ b/Docs/Modules/Battle.md @@ -62,7 +62,7 @@ | Голод и усталость вместе | Последовательно: 50% × 80% = **40%** силы (а не 30%). | | Порядок | Контры и осадные → бонусы стороны → состояния (голод, усталость) → случайность. | | Исход | Побеждает тот, у кого итоговая сила больше; ничья — защитнику. | -| Потери (решение №14, регламент 31.10) | Доля выбывших каждого типа зависит от соотношения итоговых сил (сильная / слабая). Опорные точки (ТЕСТ): **1:1 — 45% / 45%**, **3:1 — 16% / 74%**, **5:1 — 11% / 88%**. Между точками — плавно (линейно по соотношению), без порогов. Одинаково для PvE, PvP и Круга. За 5:1 — как при 5:1. Точки — в `BattleConfig` (`lossRatioPermille`, `strongLossPermille`, `weakLossPermille`). | +| Потери (решение №14, регламент 31.10) | Доля выбывших каждого типа зависит от соотношения итоговых сил (сильная / слабая). Опорные точки (ТЕСТ): **1:1 — 45% / 45%**, **3:1 — 16% / 74%**, **5:1 — 11% / 88%**. Между точками — плавно (линейно по соотношению), без порогов. Одинаково для PvE, PvP и Круга. Разгром (решения по 8 вопросам Саши, 31.10, п. 5): **7:1 — 92%**, **10:1 — 95%** у слабой стороны, дальше плавно к пределу 99% (`v = 99% − 4% × 10 / R`: 20:1 — 97%, 100:1 — 98,6%), 100% не достигается. Небольшая армия всё равно может погибнуть целиком из-за округления шансом (№13). Сильная сторона после 5:1 — 11%. Точки — в `BattleConfig` (`lossRatioPermille`, `strongLossPermille`, `weakLossPermille`, предел `weakLossMaxPermille`). | | Округление потерь | Решение №13. Целая часть гарантирована, дробная — шанс ещё одной потери. Бросок — из отдельного потока случайности (своя «соль» на сторону и тип войск, не пересекается с удачей), воспроизводим по номеру боя. Одиночка-проигравший (потеря 0,5) гибнет примерно в половине боёв; 3 проигравших (1,5) теряют 1 или 2. | Примеры (замок Змея Горыныча, гарнизон 20: пехота 8, лучники 6, кавалерия 4, осадные 2, за стенами): @@ -108,7 +108,7 @@ - **Командный план и ревью от 27.10.2026** (`Bogatyri_Plan_razrabotki.docx`, `Bogatyri_Battle_Army_Review_27-10-2026.docx`): случайность ±5% каждой стороне (воспроизводимая), прогноз риска вместо точного итога, потери от соотношения сил, раненые и погибшие, голод −50%, усталость −20%, числа — в настройки. Делается на этапе 2 плана. - **Малые отряды** решены (№13): одиночка-проигравший гибнет с шансом по дробной части потерь. - **Сделано 31.10.2026: кривая потерь (№14)** по опорным точкам регламента 31.10. Раненых пока нет: все выбывшие считаются погибшими. -- **Открыто: разгром дальше 5:1.** Регламент даёт точки до 5:1; дальше доли держатся на 11% / 88%. Растёт ли разгром выше (например, до 100% при 10:1) — решает команда. +- **Сделано 01.11.2026: разгром дальше 5:1** по решениям 31.10, п. 5. **Открыто:** потери сильной стороны после 5:1 документ не задаёт — пока 11%, как при 5:1. - **Следующий шаг — раненые (№15)** вместе с госпиталем. Доля погибших среди выбывших (ТЕСТ регламента): уверенная победа ~20%, близкий бой ~30%, поражение 2:1 ~45%, разгром 5:1 ~70%. Чтобы раненые не «пропали» из людей города, их должно учитывать Население (Валдемар) — договориться о контракте до реализации. - **Утвердить формулу** (контры, стены, осадные, потери) или поправить числа в `BattleSystem.cs`. @@ -120,6 +120,7 @@ | Дата | Кто | Что | | --- | --- | --- | +| 01.11.2026 | Claude | Разгром дальше 5:1 (решения 31.10, п. 5): точки 7:1 — 92% и 10:1 — 95%, дальше слабая сторона плавно стремится к 99% (`weakLossMaxPermille`). Версия правил 5. Тест кривой продлён до 30:1. | | 31.10.2026 | Claude | Кривая потерь (решение №14, регламент 31.10): доля выбывших от соотношения итоговых сил по опорным точкам 1:1 — 45% / 45%, 3:1 — 16% / 74%, 5:1 — 11% / 88%, между ними плавно; вместо «проигравший 50%». Точки в `BattleConfig`, неверная таблица — значения по умолчанию. Версия правил 4. Тест `Battle_LossCurve_ControlPointsAndSmoothness`; тесты округления пересчитаны под кривую. | | 30.10.2026 | Claude | Комплект требований 29.10, решения №11–13: точный шанс победы в прогнозе (`BattleForecast.WinChancePermille`); сила — точной дробью без промежуточного округления; потери — целая часть гарантирована, дробная — шанс из отдельного потока случайности. Версия правил 3. Тесты: шанс совпадает с настоящими боями, сила без округления, округление потерь. | | 28.10.2026 | Claude | Голод и усталость: `BattleCondition` (флаги), `BattleRequest.AttackerConditions` / `DefenderConditions`, `BattleOutcome.…ConditionPercent`; голод −50%, усталость −20%, вместе 40% (последовательно); числа в `BattleConfig` и `BattleRules` (версия 2). Тесты: штрафы, голод переворачивает бой у нападающего и у защитника, прогноз с голодом. | diff --git a/Docs/Modules/README.md b/Docs/Modules/README.md index 346bf0b..6e2fdfd 100644 --- a/Docs/Modules/README.md +++ b/Docs/Modules/README.md @@ -105,6 +105,7 @@ EditMode-тесты: `Assets/Scripts/Modules/Tests/ModuleTests.cs`. Запуск | Дата | Кто | Что | | --- | --- | --- | +| 01.11.2026 | Claude | Решения 31.10, п. 5: разгром дальше 5:1 (92% / 95% / до 99%). Обновлён Battle. | | 01.11.2026 | Claude | Решения 31.10, п. 4: средняя скорость смешанного войска. Обновлён World. | | 01.11.2026 | Claude | Решения по 8 вопросам Саши (31.10), п. 1: гарнизоны и запасы точек видны только в зоне обзора. Обновлён Intelligence. | | 31.10.2026 | Claude | Окна зданий: подписи над зданиями убраны, у каждого здания (добывающие, Дом, Склад, Амбар, Ратуша, Кристалл) своё окно по тапу — состояние, добыча и склад, улучшение и его цена. Казармы тоже получили окно и уровни (открывают новые войска вместе с Ратушей; уровни 1/2/3/5 — ТЕСТ). Ратуша улучшается кнопкой в окне, а не тапом. Обновлён Settlement. |