feat(ai): improving agents planning and memory
This commit is contained in:
54
apps/backend/Industry/Planning/CommodityOperationalSignal.cs
Normal file
54
apps/backend/Industry/Planning/CommodityOperationalSignal.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace SpaceGame.Api.Industry.Planning;
|
||||
|
||||
internal static class CommodityOperationalSignal
|
||||
{
|
||||
internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||
{
|
||||
var productionDeficit = MathF.Max(0f, commodity.ConsumptionRatePerSecond - commodity.ProjectedProductionRatePerSecond);
|
||||
var levelDeficit = MathF.Max(0f, targetLevelSeconds - commodity.LevelSeconds) / MathF.Max(targetLevelSeconds, 1f);
|
||||
var backlogPressure = MathF.Max(0f, commodity.BuyBacklog + commodity.ReservedForConstruction - commodity.AvailableStock);
|
||||
|
||||
var levelWeight = commodity.Level switch
|
||||
{
|
||||
CommodityLevelKind.Critical => 140f,
|
||||
CommodityLevelKind.Low => 80f,
|
||||
CommodityLevelKind.Stable => 20f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
return levelWeight
|
||||
+ (productionDeficit * 140f)
|
||||
+ (levelDeficit * 120f)
|
||||
+ backlogPressure;
|
||||
}
|
||||
|
||||
internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||
commodity.ProjectedProductionRatePerSecond > 0.01f
|
||||
&& commodity.ProjectedNetRatePerSecond >= -0.01f
|
||||
&& commodity.LevelSeconds >= targetLevelSeconds
|
||||
&& commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus;
|
||||
|
||||
internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||
!IsOperational(commodity, targetLevelSeconds)
|
||||
|| commodity.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low;
|
||||
|
||||
internal static float ComputeFeasibilityFactor(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||
{
|
||||
if (commodity.AvailableStock <= 0.01f && commodity.ProjectedProductionRatePerSecond <= 0.01f)
|
||||
{
|
||||
return 0.65f;
|
||||
}
|
||||
|
||||
if (commodity.Level is CommodityLevelKind.Critical)
|
||||
{
|
||||
return 0.72f;
|
||||
}
|
||||
|
||||
if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds)
|
||||
{
|
||||
return 0.84f;
|
||||
}
|
||||
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
@@ -41,50 +41,27 @@ internal sealed class FactionCommoditySnapshot
|
||||
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
|
||||
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||
internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f);
|
||||
internal float LevelSeconds => AvailableStock <= 0.01f
|
||||
? 0f
|
||||
: AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f);
|
||||
|
||||
internal float ShortageHorizonSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ConsumptionRatePerSecond <= 0.01f && BuyBacklog <= 0.01f)
|
||||
internal CommodityLevelKind Level =>
|
||||
LevelSeconds switch
|
||||
{
|
||||
return float.PositiveInfinity;
|
||||
}
|
||||
<= 60f => CommodityLevelKind.Critical,
|
||||
<= 180f => CommodityLevelKind.Low,
|
||||
<= 480f => CommodityLevelKind.Stable,
|
||||
_ => CommodityLevelKind.Surplus,
|
||||
};
|
||||
}
|
||||
|
||||
if (NetRatePerSecond >= -0.01f)
|
||||
{
|
||||
return float.PositiveInfinity;
|
||||
}
|
||||
|
||||
return AvailableStock / MathF.Max(0.01f, -NetRatePerSecond);
|
||||
}
|
||||
}
|
||||
|
||||
internal float ProjectedShortageHorizonSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ConsumptionRatePerSecond <= 0.01f && BuyBacklog <= 0.01f)
|
||||
{
|
||||
return float.PositiveInfinity;
|
||||
}
|
||||
|
||||
if (ProjectedNetRatePerSecond >= -0.01f)
|
||||
{
|
||||
return float.PositiveInfinity;
|
||||
}
|
||||
|
||||
return AvailableStock / MathF.Max(0.01f, -ProjectedNetRatePerSecond);
|
||||
}
|
||||
}
|
||||
|
||||
internal float PressureScore =>
|
||||
MathF.Max(0f, (BuyBacklog + ReservedForConstruction) - (OnHand + Inbound))
|
||||
+ MathF.Max(0f, ConsumptionRatePerSecond - ProductionRatePerSecond) * 120f;
|
||||
|
||||
internal float ProjectedPressureScore =>
|
||||
MathF.Max(0f, (BuyBacklog + ReservedForConstruction) - (OnHand + Inbound))
|
||||
+ MathF.Max(0f, ConsumptionRatePerSecond - ProjectedProductionRatePerSecond) * 120f;
|
||||
internal enum CommodityLevelKind
|
||||
{
|
||||
Critical,
|
||||
Low,
|
||||
Stable,
|
||||
Surplus,
|
||||
}
|
||||
|
||||
internal static class FactionEconomyAnalyzer
|
||||
|
||||
@@ -4,6 +4,9 @@ using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
|
||||
internal static class FactionIndustryPlanner
|
||||
{
|
||||
private const float CommodityTargetLevelSeconds = 240f;
|
||||
private const float WaterTargetLevelSeconds = 300f;
|
||||
|
||||
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId)
|
||||
{
|
||||
if (HasActiveExpansionProject(world, factionId))
|
||||
@@ -64,14 +67,13 @@ internal static class FactionIndustryPlanner
|
||||
.Select(itemId => new
|
||||
{
|
||||
ItemId = itemId,
|
||||
HasProducer = FactionHasProducerForCommodity(world, factionId, itemId),
|
||||
Pressure = GetCommodityPressure(world, factionId, itemId),
|
||||
Stockpile = GetCommodityStockpile(world, factionId, itemId),
|
||||
Commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId),
|
||||
})
|
||||
.Where(entry => !entry.HasProducer || entry.Pressure > 0.01f || entry.Stockpile < 120f)
|
||||
.OrderByDescending(entry => !entry.HasProducer ? 1 : 0)
|
||||
.ThenByDescending(entry => entry.Pressure)
|
||||
.ThenBy(entry => entry.Stockpile)
|
||||
.Where(entry => entry.Commodity.ProjectedProductionRatePerSecond <= 0.01f
|
||||
|| CommodityOperationalSignal.IsStrained(entry.Commodity, GetTargetLevelSeconds(entry.ItemId)))
|
||||
.OrderByDescending(entry => entry.Commodity.ProjectedProductionRatePerSecond <= 0.01f ? 1 : 0)
|
||||
.ThenByDescending(entry => CommodityOperationalSignal.ComputeNeedScore(entry.Commodity, GetTargetLevelSeconds(entry.ItemId)))
|
||||
.ThenBy(entry => entry.Commodity.AvailableStock)
|
||||
.Select(entry => entry.ItemId)
|
||||
.FirstOrDefault();
|
||||
|
||||
@@ -301,14 +303,20 @@ internal static class FactionIndustryPlanner
|
||||
.GroupBy(order => order.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(order => order.RemainingAmount), StringComparer.Ordinal);
|
||||
|
||||
if (CommanderPlanningService.FactionCommanderHasDirective(world, factionId, "produce-military-ships"))
|
||||
if (CommanderPlanningService.FactionCommanderHasIssuedTask(world, factionId, FactionIssuedTaskKind.ProduceShips, "military"))
|
||||
{
|
||||
demandByItem["hullparts"] = demandByItem.GetValueOrDefault("hullparts") + 120f;
|
||||
demandByItem["claytronics"] = demandByItem.GetValueOrDefault("claytronics") + 90f;
|
||||
}
|
||||
|
||||
return demandByItem
|
||||
.Select(entry => (ItemId: ResolveBottleneckCommodity(world, factionId, entry.Key), Score: entry.Value))
|
||||
.Select(entry =>
|
||||
{
|
||||
var itemId = ResolveBottleneckCommodity(world, factionId, entry.Key);
|
||||
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId);
|
||||
var score = entry.Value + CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(itemId));
|
||||
return (ItemId: itemId, Score: score);
|
||||
})
|
||||
.Where(entry => entry.ItemId is not null)
|
||||
.GroupBy(entry => entry.ItemId!, StringComparer.Ordinal)
|
||||
.Select(group => (ItemId: group.Key, Score: group.Sum(entry => entry.Score)))
|
||||
@@ -358,7 +366,11 @@ internal static class FactionIndustryPlanner
|
||||
|
||||
var weakestUnproducedInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
||||
.Where(inputId => !FactionHasProducerForCommodity(world, factionId, inputId))
|
||||
.Select(inputId => (ItemId: inputId, Score: GetCommodityPressure(world, factionId, inputId), Stockpile: GetCommodityStockpile(world, factionId, inputId)))
|
||||
.Select(inputId =>
|
||||
{
|
||||
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(inputId);
|
||||
return (ItemId: inputId, Score: CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(inputId)), Stockpile: commodity.AvailableStock);
|
||||
})
|
||||
.OrderByDescending(entry => entry.Score)
|
||||
.ThenBy(entry => entry.Stockpile)
|
||||
.FirstOrDefault();
|
||||
@@ -370,11 +382,15 @@ internal static class FactionIndustryPlanner
|
||||
}
|
||||
|
||||
var weakestInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
||||
.Select(inputId => (ItemId: inputId, Score: GetCommodityPressure(world, factionId, inputId)))
|
||||
.Select(inputId =>
|
||||
{
|
||||
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(inputId);
|
||||
return (ItemId: inputId, Score: CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(inputId)));
|
||||
})
|
||||
.OrderByDescending(entry => entry.Score)
|
||||
.FirstOrDefault();
|
||||
|
||||
return weakestInput.Score > GetCommodityPressure(world, factionId, itemId) * 0.6f
|
||||
return weakestInput.Score > GetCommodityNeedScore(world, factionId, itemId) * 0.6f
|
||||
? ResolveBottleneckCommodity(world, factionId, weakestInput.ItemId, visited)
|
||||
: itemId;
|
||||
}
|
||||
@@ -419,13 +435,14 @@ internal static class FactionIndustryPlanner
|
||||
&& string.Equals(site.TargetKind, "station-foundation", StringComparison.Ordinal)
|
||||
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
||||
|
||||
private static float GetCommodityPressure(SimulationWorld world, string factionId, string itemId)
|
||||
private static float GetCommodityNeedScore(SimulationWorld world, string factionId, string itemId)
|
||||
{
|
||||
return FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId).ProjectedPressureScore;
|
||||
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId);
|
||||
return CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(itemId));
|
||||
}
|
||||
|
||||
private static float GetCommodityStockpile(SimulationWorld world, string factionId, string itemId) =>
|
||||
FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId).AvailableStock;
|
||||
private static float GetTargetLevelSeconds(string itemId) =>
|
||||
string.Equals(itemId, "water", StringComparison.Ordinal) ? WaterTargetLevelSeconds : CommodityTargetLevelSeconds;
|
||||
|
||||
private static CelestialRuntime? SelectFoundationCelestial(SimulationWorld world, string factionId, string commodityId)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user