Скорость смешанного войска -- средняя по численности с модификаторами (решения 31.10, п. 4)

V = Σ(Nᵢ·Vᵢ·Mᵢ)/ΣNᵢ, общий модификатор -- один раз, без промежуточного округления.
MarchConfig: mixedUsesSlowest убран, добавлены typeSpeedBonusPercent и armySpeedBonusPercent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-01 13:05:57 +01:00
co-authored by Claude Opus 5.5
parent b9ce85cdbb
commit 20691e0511
7 changed files with 65 additions and 24 deletions
+2 -1
View File
@@ -16,7 +16,8 @@ MonoBehaviour:
archers: 9
cavalry: 12
siege: 4
mixedUsesSlowest: 1
typeSpeedBonusPercent: 00000000000000000000000000000000
armySpeedBonusPercent: 0
baseMarchSlots: 2
patrolsEnabled: 1
patrolMaxActive: 3
+8 -3
View File
@@ -20,8 +20,12 @@ public class MarchConfig : ScriptableObject
public float cavalry = 12f;
[Tooltip("Осадные. Регламент 31.10: 4")]
public float siege = 4f;
[Tooltip("Смешанное войско идёт со скоростью самого медленного типа. Выключено -- средняя скорость по числу воинов. Правило для одной армии регламент не задаёт -- открытый вопрос")]
public bool mixedUsesSlowest = true;
[Header("Модификаторы скорости, % (решения 31.10, п. 4: смешанное войско -- средняя по численности)")]
[Tooltip("По типам: пехота, лучники, кавалерия, осадные. +10 -- быстрее на 10%. Позже -- бонусы героев и технологий. ТЕСТ")]
public int[] typeSpeedBonusPercent = { 0, 0, 0, 0 };
[Tooltip("Общий модификатор всего войска, применяется один раз. ТЕСТ")]
public int armySpeedBonusPercent = 0;
[Header("Слоты похода")]
[Tooltip("Сколько походов игрока могут идти одновременно. Регламент 31.10: 2 (технология «Военная логистика» -- до 7)")]
@@ -57,7 +61,8 @@ public class MarchConfig : ScriptableObject
return new Bogatyri.World.MarchSpeedRules
{
SpeedMilliPerHour = new[] { Milli(infantry), Milli(archers), Milli(cavalry), Milli(siege) },
MixedUsesSlowest = mixedUsesSlowest
TypeBonusPercent = (int[])(typeSpeedBonusPercent ?? new int[4]).Clone(),
ArmyBonusPercent = armySpeedBonusPercent
};
}
+11 -5
View File
@@ -699,14 +699,20 @@ public class ModuleTests
Assert.AreEqual(d * 3_600_000 / 9000, Arrive(new[] { 0, 10, 0, 0 }), "лучники -- 9");
Assert.AreEqual(d * 3_600_000 / 12000, Arrive(new[] { 0, 0, 10, 0 }), "кавалерия -- 12");
Assert.AreEqual(d * 3_600_000 / 4000, Arrive(new[] { 0, 0, 0, 10 }), "осадные -- 4");
Assert.AreEqual(d * 3_600_000 / 4000, Arrive(new[] { 30, 0, 20, 1 }), "одна осадная машина тормозит всё войско");
Assert.AreEqual(d * 3_600_000L * 51 / 484_000, Arrive(new[] { 30, 0, 20, 1 }), "средняя: (30×8 + 20×12 + 1×4) / 51");
Assert.AreEqual(MarchRules.GetTravelTimeMs(home, boss), Arrive(null), "состав неизвестен -- как пехота");
Assert.Greater(d * 3_600_000 / 8000, 3_600_000L, "до замка больше часа -- марш в реальном времени");
// Средняя скорость по числу воинов -- если команда выберет её вместо «по самому медленному»
var weighted = new MarchSpeedRules { MixedUsesSlowest = false };
Assert.AreEqual(10000, weighted.SpeedFor(new[] { 10, 0, 10, 0 }), "(8 + 12) / 2 = 10");
Assert.AreEqual(8000, MarchSpeedRules.Default.SpeedFor(new[] { 10, 0, 10, 0 }), "по самому медленному -- 8");
// Контрольный пример решений 31.10, п. 4: средняя по численности, общий бафф -- один раз
Assert.AreEqual(10000, MarchSpeedRules.Default.SpeedFor(new[] { 10, 0, 10, 0 }), "(10×8 + 10×12) / 20 = 10");
Assert.AreEqual(11000, new MarchSpeedRules { ArmyBonusPercent = 10 }.SpeedFor(new[] { 10, 0, 10, 0 }), "+10% -- 11");
Assert.AreEqual(11000, new MarchSpeedRules { TypeBonusPercent = new[] { 10, 0, 10, 0 } }.SpeedFor(new[] { 10, 0, 10, 0 }),
"+10% каждому типу -- тоже 11");
Assert.AreEqual(12100, new MarchSpeedRules { ArmyBonusPercent = 10, TypeBonusPercent = new[] { 10, 0, 10, 0 } }.SpeedFor(new[] { 10, 0, 10, 0 }),
"бафф типа и общий бафф -- оба, но каждый по разу");
Assert.AreEqual(6000, new MarchSpeedRules { TypeBonusPercent = new[] { 0, 0, -50, 0 } }.SpeedFor(new[] { 0, 0, 10, 0 }), "штраф -50%: 12 -> 6");
// Без промежуточного округления: 1 пехотинец + 2 кавалериста = 32/3 клетки в час -- дробь доходит до времени пути
Assert.AreEqual(d * 3_600_000L * 3 / 32_000, world.TravelTimeMs(home, boss, new[] { 1, 0, 2, 0 }));
// После боя время прихода уже не меняется
MarchData fought = world.StartMarch("p", WorldMapLayout.PlayerCityId, "camp_1", 20, 0, out _);
+1 -1
View File
@@ -214,7 +214,7 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
error = "Не удалось собрать войско в поход";
return false;
}
// Состав похода -- для боя с типами войск и для скорости: время прихода по самому медленному типу
// Состав похода -- для боя с типами войск и для скорости: время прихода по средней скорости состава
World.AssignUnits(march, army.GetMarchUnits(march.id));
if (GameServices.TryGet(out ISaveSystem save)) save.SaveNow();
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace Bogatyri.World
{
@@ -53,37 +54,63 @@ namespace Bogatyri.World
// Скорости маршей (регламент 31.10, раздел 7, ТЕСТ). Физический марш идёт в РЕАЛЬНОМ времени (регламент, раздел 2):
// скорость -- единиц карты за 1 реальный час, единица карты = 1 клетка = 1000 милли-единиц координат.
// Время мира -- миллисекунды часов GameClock (реальное время + перемотка DEV).
//
// Смешанное войско (решения по 8 вопросам Саши, 31.10, п. 4) -- средняя скорость по численности с модификаторами:
// V = Σ(Nᵢ × Vᵢ × Mᵢ) / ΣNᵢ, общий модификатор войска -- один раз, без промежуточного округления.
// Пример: 10 пехоты + 10 кавалерии = (10×8 + 10×12) / 20 = 10 клеток в час; общий бафф +10% -- 11.
public sealed class MarchSpeedRules
{
// Милли-единиц карты за реальный час по типам войск (TroopType): пехота 8, лучники 9, кавалерия 12, осадные 4
public int[] SpeedMilliPerHour = { 8000, 9000, 12000, 4000 };
// Смешанное войско: true -- идёт со скоростью самого медленного типа; false -- средняя скорость,
// взвешенная по числу воинов. Регламент правило для одной армии не задаёт -- открытый вопрос команде.
public bool MixedUsesSlowest = true;
// Модификатор скорости каждого типа, в процентах: +10 -- быстрее на 10%, -20 -- медленнее.
// Позже сюда придут бонусы героев и технологий по типам; сейчас -- 0 (ТЕСТ, можно менять в MarchConfig)
public int[] TypeBonusPercent = { 0, 0, 0, 0 };
// Общий модификатор войска, в процентах -- применяется один раз ко всему войску
public int ArmyBonusPercent = 0;
public static MarchSpeedRules Default => new MarchSpeedRules();
// Скорость войска данного состава. Состав неизвестен (старый поход) -- как у пехоты.
public long SpeedFor(int[] units)
// Скорость войска как точная дробь num / den (милли-единиц в час). Состав неизвестен (старый поход) -- как у пехоты.
void Speed(int[] units, out BigInteger num, out BigInteger den)
{
long slowest = long.MaxValue, weighted = 0, count = 0;
num = BigInteger.Zero;
long count = 0;
for (int i = 0; units != null && i < units.Length && i < SpeedMilliPerHour.Length; i++)
{
if (units[i] <= 0) continue;
long s = Math.Max(1, SpeedMilliPerHour[i]);
slowest = Math.Min(slowest, s);
weighted += s * units[i];
num += (BigInteger)units[i] * Math.Max(1, SpeedMilliPerHour[i]) * TypeFactor(i);
count += units[i];
}
if (count == 0) return Math.Max(1, SpeedMilliPerHour[TroopType.Infantry]);
return MixedUsesSlowest ? slowest : Math.Max(1, weighted / count);
if (count == 0)
{
num = (BigInteger)Math.Max(1, SpeedMilliPerHour[TroopType.Infantry]) * TypeFactor(TroopType.Infantry);
count = 1;
}
num *= Math.Max(1, 100 + ArmyBonusPercent);
den = (BigInteger)count * 100 * 100;
if (num.IsZero) num = BigInteger.One;
}
// Время в пути в реальных миллисекундах
int TypeFactor(int type)
{
int bonus = TypeBonusPercent != null && type < TypeBonusPercent.Length ? TypeBonusPercent[type] : 0;
return Math.Max(1, 100 + bonus); // скорость не бывает нулевой или отрицательной
}
// Скорость войска данного состава, милли-единиц за реальный час (округлено вниз -- только для показа)
public long SpeedFor(int[] units)
{
Speed(units, out BigInteger num, out BigInteger den);
return Math.Max(1, (long)(num / den));
}
// Время в пути в реальных миллисекундах: расстояние / скорость, одно округление вниз в самом конце
public long TravelTimeMs(MapNode from, MapNode to, int[] units)
{
return (long)WorldMapLayout.Distance(from, to) * 3_600_000 / SpeedFor(units);
Speed(units, out BigInteger num, out BigInteger den);
return (long)((BigInteger)WorldMapLayout.Distance(from, to) * 3_600_000 * den / num);
}
}