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 @@ namespace SpaceGame.Api.Stations.Simulation;
internal sealed class InfrastructureSimulationService
{
private const float CommodityTargetLevelSeconds = 240f;
private const float EnergyTargetLevelSeconds = 240f;
internal void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
{
foreach (var claim in world.Claims)
@@ -259,16 +262,19 @@ internal sealed class InfrastructureSimulationService
var currentCount = CountModules(station.InstalledModules, objectiveModuleId);
var marginalOutputRate = EstimateMarginalOutputRate(world, station, objectiveModuleId, objectiveCommodityId);
var constructionImpact = EstimateConstructionBottleneckImpact(world, objectiveModuleId, constructionDemandByItem);
var score = 90f + commodity.ProjectedPressureScore + (marginalOutputRate * 900f) + constructionImpact;
var score = 90f
+ CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(objectiveCommodityId))
+ (marginalOutputRate * 900f)
+ constructionImpact;
if (currentCount == 0)
{
score += 80f;
}
if (!float.IsPositiveInfinity(commodity.ProjectedShortageHorizonSeconds))
if (commodity.LevelSeconds < GetTargetLevelSeconds(objectiveCommodityId))
{
score += MathF.Max(0f, 300f - commodity.ProjectedShortageHorizonSeconds) * 0.3f;
score += MathF.Max(0f, GetTargetLevelSeconds(objectiveCommodityId) - commodity.LevelSeconds) * 0.3f;
}
score *= EstimateObjectiveExpansionFeasibility(world, station, economy, objectiveModuleId, objectiveCommodityId);
@@ -287,16 +293,19 @@ internal sealed class InfrastructureSimulationService
var currentCount = CountModules(station.InstalledModules, "module_gen_prod_energycells_01");
var constructionImpact = EstimateConstructionBottleneckImpact(world, "module_gen_prod_energycells_01", constructionDemandByItem);
var readinessUnlock = EstimateSupportUnlockScore(world, station, economy, "module_gen_prod_energycells_01");
var score = 40f + energy.ProjectedPressureScore * 0.5f + constructionImpact + readinessUnlock;
var score = 40f
+ CommodityOperationalSignal.ComputeNeedScore(energy, EnergyTargetLevelSeconds) * 0.5f
+ constructionImpact
+ readinessUnlock;
if (currentCount == 0)
{
score += 70f;
}
if (!float.IsPositiveInfinity(energy.ProjectedShortageHorizonSeconds))
if (energy.LevelSeconds < EnergyTargetLevelSeconds)
{
score += MathF.Max(0f, 240f - energy.ProjectedShortageHorizonSeconds) * 0.2f;
score += MathF.Max(0f, EnergyTargetLevelSeconds - energy.LevelSeconds) * 0.2f;
}
return score - (currentCount * 40f);
@@ -433,17 +442,9 @@ internal sealed class InfrastructureSimulationService
foreach (var input in recipe.Inputs)
{
var inputCommodity = economy.GetCommodity(input.ItemId);
if (inputCommodity.AvailableStock <= 0.01f && inputCommodity.ProjectedProductionRatePerSecond <= 0.01f)
{
feasibility *= 0.65f;
continue;
}
if (!float.IsPositiveInfinity(inputCommodity.ProjectedShortageHorizonSeconds)
&& inputCommodity.ProjectedShortageHorizonSeconds < 180f)
{
feasibility *= 0.82f;
}
feasibility *= CommodityOperationalSignal.ComputeFeasibilityFactor(
inputCommodity,
GetTargetLevelSeconds(input.ItemId));
}
}
@@ -505,7 +506,8 @@ internal sealed class InfrastructureSimulationService
{
inputFactor *= 0.95f + (availableStockRatio * 0.05f);
}
else if (commodity.ProjectedProductionRatePerSecond > 0.01f)
else if (commodity.ProjectedProductionRatePerSecond > 0.01f
&& commodity.Level is not CommodityLevelKind.Critical)
{
inputFactor *= 0.82f + (availableStockRatio * 0.08f);
}
@@ -719,6 +721,11 @@ internal sealed class InfrastructureSimulationService
private static bool ObjectiveNeedsEnergy(SimulationWorld world, string objectiveCommodityId) =>
world.ProductionGraph.GetImmediateInputs(objectiveCommodityId).Contains("energycells", StringComparer.Ordinal);
private static float GetTargetLevelSeconds(string commodityId) =>
string.Equals(commodityId, "energycells", StringComparison.Ordinal) ? EnergyTargetLevelSeconds :
string.Equals(commodityId, "water", StringComparison.Ordinal) ? 300f :
CommodityTargetLevelSeconds;
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
{
var nextModuleId = GetNextStationModuleToBuild(station, world);

View File

@@ -44,7 +44,7 @@ internal sealed class StationSimulationService
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
&& FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships")
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
? 90f
: 0f;
@@ -163,14 +163,33 @@ internal sealed class StationSimulationService
var priority = (float)recipe.Priority;
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
var fleetPressure = FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships") ? 1f : 0f;
priority += GetStationRecipePriorityAdjustment(station, recipe, expansionPressure, fleetPressure);
var fleetPressure = FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military") ? 1f : 0f;
priority += GetStationRecipePriorityAdjustment(world, station, recipe, expansionPressure, fleetPressure);
return priority;
}
private static float GetStationRecipePriorityAdjustment(StationRuntime station, RecipeDefinition recipe, float expansionPressure, float fleetPressure)
private static float GetStationRecipePriorityAdjustment(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, float expansionPressure, float fleetPressure)
{
if (recipe.ShipOutputId is not null && world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition))
{
var shipPressure = GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind);
return shipDefinition.Kind switch
{
"military" => recipe.Id switch
{
"frigate-construction" => 320f * shipPressure,
"destroyer-construction" => 200f * shipPressure,
"cruiser-construction" => 120f * shipPressure,
_ => 160f * shipPressure,
},
"construction" => 260f * shipPressure,
"mining" => 250f * shipPressure,
"transport" => 230f * shipPressure,
_ => 0f,
};
}
var outputItemIds = recipe.Outputs
.Select(output => output.ItemId)
.ToHashSet(StringComparer.Ordinal);
@@ -201,9 +220,6 @@ internal sealed class StationSimulationService
{
"command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly"
=> 220f * MathF.Max(expansionPressure, fleetPressure),
"frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure),
"destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure),
"cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure),
"ammo-fabrication" => -80f * expansionPressure,
"trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly"
=> -120f * expansionPressure,
@@ -228,8 +244,7 @@ internal sealed class StationSimulationService
return false;
}
if (!string.Equals(shipDefinition.Kind, "military", StringComparison.Ordinal)
|| !FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships"))
if (!FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, shipDefinition.Kind))
{
return false;
}
@@ -431,16 +446,17 @@ internal sealed class StationSimulationService
private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve)
{
var commodity = economy.GetCommodity(itemId);
if (float.IsPositiveInfinity(commodity.ShortageHorizonSeconds))
if (commodity.Level == CommodityLevelKind.Critical)
{
return MathF.Max(0f, baseReserve);
return baseReserve * 1.6f;
}
return commodity.ShortageHorizonSeconds < 180f
? baseReserve * 1.5f
: commodity.ShortageHorizonSeconds < 360f
? baseReserve * 1.2f
: baseReserve;
return commodity.Level switch
{
CommodityLevelKind.Low => baseReserve * 1.25f,
CommodityLevelKind.Stable when commodity.ProjectedNetRatePerSecond < -0.01f => baseReserve * 1.1f,
_ => MathF.Max(0f, baseReserve),
};
}
private static float ScaleSupplyTriggerByEconomy(FactionEconomySnapshot economy, string itemId, float baseTrigger)
@@ -452,24 +468,36 @@ internal sealed class StationSimulationService
private static float ScaleDemandValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
{
var commodity = economy.GetCommodity(itemId);
if (float.IsPositiveInfinity(commodity.ShortageHorizonSeconds))
return commodity.Level switch
{
return commodity.ProductionRatePerSecond > 0.01f ? baseValuation : baseValuation * 1.3f;
}
return commodity.ShortageHorizonSeconds < 180f
? baseValuation * 1.5f
: commodity.ShortageHorizonSeconds < 360f
? baseValuation * 1.25f
: baseValuation;
CommodityLevelKind.Critical => baseValuation * 1.6f,
CommodityLevelKind.Low => baseValuation * 1.3f,
CommodityLevelKind.Stable when commodity.ProjectedNetRatePerSecond < -0.01f => baseValuation * 1.15f,
CommodityLevelKind.Surplus when commodity.ProjectedNetRatePerSecond > 0.01f => baseValuation * 0.9f,
_ => commodity.ProductionRatePerSecond > 0.01f ? baseValuation : baseValuation * 1.15f,
};
}
private static float ScaleSupplyValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
{
var commodity = economy.GetCommodity(itemId);
return commodity.NetRatePerSecond > 0.01f && commodity.ShortageHorizonSeconds > 600f
return commodity.Level == CommodityLevelKind.Surplus && commodity.NetRatePerSecond > 0.01f
? baseValuation * 0.75f
: baseValuation;
: commodity.Level == CommodityLevelKind.Critical
? baseValuation * 1.15f
: baseValuation;
}
private static float GetShipProductionPressure(SimulationWorld world, string factionId, string shipKind)
{
var factionCommander = FindFactionCommander(world, factionId);
var task = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.ProduceShips, shipKind);
if (task is null)
{
return 0f;
}
return task.State == FactionIssuedTaskState.Blocked ? 0.4f : 1f;
}
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)