feat(ai): improving agents planning and memory

This commit is contained in:
2026-03-20 02:12:29 -04:00
parent f5bf7d8e3f
commit a2f66b0dca
11 changed files with 2012 additions and 445 deletions

View File

@@ -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)
{