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

@@ -16,26 +16,40 @@ public sealed class FactionPlanningState
public int EnemyShipCount { get; set; }
public int EnemyStationCount { get; set; }
public float OreStockpile { get; set; }
public float RefinedMetalsStockpile { get; set; }
public float RefinedMetalsProductionRate { get; set; }
public float RefinedMetalsShortageHorizonSeconds { get; set; }
public float HullpartsStockpile { get; set; }
public float HullpartsProductionRate { get; set; }
public float HullpartsShortageHorizonSeconds { get; set; }
public float ClaytronicsStockpile { get; set; }
public float ClaytronicsProductionRate { get; set; }
public float ClaytronicsShortageHorizonSeconds { get; set; }
public float WaterStockpile { get; set; }
public float WaterProductionRate { get; set; }
public float WaterShortageHorizonSeconds { get; set; }
public float RefinedMetalsAvailableStock { get; set; }
public float RefinedMetalsUsageRate { get; set; }
public float RefinedMetalsProjectedProductionRate { get; set; }
public float RefinedMetalsProjectedNetRate { get; set; }
public float RefinedMetalsLevelSeconds { get; set; }
public string RefinedMetalsLevel { get; set; } = "unknown";
public float HullpartsAvailableStock { get; set; }
public float HullpartsUsageRate { get; set; }
public float HullpartsProjectedProductionRate { get; set; }
public float HullpartsProjectedNetRate { get; set; }
public float HullpartsLevelSeconds { get; set; }
public string HullpartsLevel { get; set; } = "unknown";
public float ClaytronicsAvailableStock { get; set; }
public float ClaytronicsUsageRate { get; set; }
public float ClaytronicsProjectedProductionRate { get; set; }
public float ClaytronicsProjectedNetRate { get; set; }
public float ClaytronicsLevelSeconds { get; set; }
public string ClaytronicsLevel { get; set; } = "unknown";
public float WaterAvailableStock { get; set; }
public float WaterUsageRate { get; set; }
public float WaterProjectedProductionRate { get; set; }
public float WaterProjectedNetRate { get; set; }
public float WaterLevelSeconds { get; set; }
public string WaterLevel { get; set; } = "unknown";
public bool HasRefinedMetalsProduction => RefinedMetalsProductionRate > 0.01f;
public bool HasHullpartsProduction => HullpartsProductionRate > 0.01f;
public bool HasClaytronicsProduction => ClaytronicsProductionRate > 0.01f;
public bool HasWaterProduction => WaterProductionRate > 0.01f;
public bool HasRefinedMetalsProduction => RefinedMetalsProjectedProductionRate > 0.01f;
public bool HasHullpartsProduction => HullpartsProjectedProductionRate > 0.01f;
public bool HasClaytronicsProduction => ClaytronicsProjectedProductionRate > 0.01f;
public bool HasWaterProduction => WaterProjectedProductionRate > 0.01f;
public bool HasWarIndustrySupplyChain =>
HasRefinedMetalsProduction && HasHullpartsProduction && HasClaytronicsProduction;
IsCommodityOperational(RefinedMetalsProjectedProductionRate, RefinedMetalsProjectedNetRate, RefinedMetalsLevelSeconds, RefinedMetalsLevel, 240f)
&& IsCommodityOperational(HullpartsProjectedProductionRate, HullpartsProjectedNetRate, HullpartsLevelSeconds, HullpartsLevel, 240f)
&& IsCommodityOperational(ClaytronicsProjectedProductionRate, ClaytronicsProjectedNetRate, ClaytronicsLevelSeconds, ClaytronicsLevel, 240f);
public FactionPlanningState Clone() => (FactionPlanningState)MemberwiseClone();
@@ -44,6 +58,39 @@ public sealed class FactionPlanningState
var expansionDeficit = Math.Max(0, state.TargetSystemCount - state.ControlledSystemCount);
return Math.Max(3, (state.ControlledSystemCount * 2) + (expansionDeficit * 3) + Math.Min(4, state.EnemyFactionCount + state.EnemyStationCount));
}
internal static bool IsCommodityOperational(
float projectedProductionRate,
float projectedNetRate,
float levelSeconds,
string level,
float targetLevelSeconds) =>
projectedProductionRate > 0.01f
&& projectedNetRate >= -0.01f
&& levelSeconds >= targetLevelSeconds
&& (string.Equals(level, "stable", StringComparison.OrdinalIgnoreCase)
|| string.Equals(level, "surplus", StringComparison.OrdinalIgnoreCase));
internal static float ComputeCommodityNeed(
float projectedProductionRate,
float usageRate,
float projectedNetRate,
float levelSeconds,
string level,
float targetLevelSeconds)
{
var levelWeight = level switch
{
"critical" => 140f,
"low" => 80f,
"stable" => 20f,
_ => 0f,
};
var rateDeficit = MathF.Max(0f, usageRate - projectedProductionRate);
var levelDeficit = MathF.Max(0f, targetLevelSeconds - levelSeconds) / MathF.Max(targetLevelSeconds, 1f);
var instability = projectedNetRate < 0f ? MathF.Abs(projectedNetRate) * 80f : 0f;
return levelWeight + (rateDeficit * 140f) + (levelDeficit * 120f) + instability;
}
}
// ─── Goals ─────────────────────────────────────────────────────────────────────
@@ -63,12 +110,16 @@ public sealed class EnsureWarIndustryGoal : GoapGoal<FactionPlanningState>
}
var missingStages =
(state.HasRefinedMetalsProduction ? 0 : 1) +
(state.HasHullpartsProduction ? 0 : 1) +
(state.HasClaytronicsProduction ? 0 : 1) +
(FactionPlanningState.IsCommodityOperational(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f) ? 0 : 1) +
(FactionPlanningState.IsCommodityOperational(state.HullpartsProjectedProductionRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f) ? 0 : 1) +
(FactionPlanningState.IsCommodityOperational(state.ClaytronicsProjectedProductionRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f) ? 0 : 1) +
(state.HasShipFactory ? 0 : 1);
var supplyNeed =
FactionPlanningState.ComputeCommodityNeed(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsUsageRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f)
+ FactionPlanningState.ComputeCommodityNeed(state.HullpartsProjectedProductionRate, state.HullpartsUsageRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f)
+ FactionPlanningState.ComputeCommodityNeed(state.ClaytronicsProjectedProductionRate, state.ClaytronicsUsageRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f);
return missingStages <= 0 ? 0f : 125f + (missingStages * 18f);
return missingStages <= 0 && supplyNeed <= 0.01f ? 0f : 110f + (missingStages * 22f) + (supplyNeed * 0.18f);
}
}
@@ -77,21 +128,22 @@ public sealed class EnsureWaterSecurityGoal : GoapGoal<FactionPlanningState>
public override string Name => "ensure-water-security";
public override bool IsSatisfied(FactionPlanningState state) =>
state.HasWaterProduction && state.WaterShortageHorizonSeconds >= 300f;
FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f);
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
{
if (state.HasWaterProduction && state.WaterShortageHorizonSeconds >= 300f)
if (FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f))
{
return 0f;
}
if (float.IsPositiveInfinity(state.WaterShortageHorizonSeconds))
{
return state.HasWaterProduction ? 0f : 85f;
}
return 55f + MathF.Max(0f, 300f - state.WaterShortageHorizonSeconds) * 0.2f;
return 55f + FactionPlanningState.ComputeCommodityNeed(
state.WaterProjectedProductionRate,
state.WaterUsageRate,
state.WaterProjectedNetRate,
state.WaterLevelSeconds,
state.WaterLevel,
300f) * 0.25f;
}
}
@@ -170,159 +222,3 @@ public sealed class EnsureConstructionCapacityGoal : GoapGoal<FactionPlanningSta
return deficit <= 0 ? 0f : 60f + (deficit * 10f);
}
}
// ─── Actions ───────────────────────────────────────────────────────────────────
public sealed class OrderShipProductionAction : GoapAction<FactionPlanningState>
{
private readonly string shipKind;
private readonly string shipId;
public OrderShipProductionAction(string shipKind, string shipId)
{
this.shipKind = shipKind;
this.shipId = shipId;
}
public override string Name => $"order-{shipId}-production";
public override float Cost => 1f;
public override bool CheckPreconditions(FactionPlanningState state) =>
state.HasShipFactory && state.HasWarIndustrySupplyChain;
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
{
switch (shipKind)
{
case "military": state.MilitaryShipCount++; break;
case "mining": state.MinerShipCount++; break;
case "transport": state.TransportShipCount++; break;
case "construction": state.ConstructorShipCount++; break;
}
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
commander.ActiveDirectives.Add($"produce-{shipKind}-ships");
}
}
public sealed class PlanWarIndustryAction : GoapAction<FactionPlanningState>
{
public override string Name => "plan-war-industry";
public override float Cost => 2f;
public override bool CheckPreconditions(FactionPlanningState state) =>
state.EnemyFactionCount > 0 && (!state.HasWarIndustrySupplyChain || !state.HasShipFactory);
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
{
state.RefinedMetalsProductionRate = MathF.Max(state.RefinedMetalsProductionRate, 1f);
state.HullpartsProductionRate = MathF.Max(state.HullpartsProductionRate, 1f);
state.ClaytronicsProductionRate = MathF.Max(state.ClaytronicsProductionRate, 1f);
state.HasShipFactory = true;
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
commander.ActiveDirectives.Add("bootstrap-war-industry");
if (FactionIndustryPlanner.AnalyzeShipyardNeed(world, commander.FactionId) is not { } project)
{
return;
}
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}");
}
}
public sealed class PlanCommoditySupplyAction : GoapAction<FactionPlanningState>
{
private readonly string commodityId;
public PlanCommoditySupplyAction(string commodityId)
{
this.commodityId = commodityId;
}
public override string Name => $"plan-{commodityId}-supply";
public override float Cost => 2f;
public override bool CheckPreconditions(FactionPlanningState state) =>
commodityId switch
{
"water" => !state.HasWaterProduction || state.WaterShortageHorizonSeconds < 300f,
_ => false,
};
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
{
if (string.Equals(commodityId, "water", StringComparison.Ordinal))
{
state.WaterProductionRate = MathF.Max(state.WaterProductionRate, 1f);
state.WaterShortageHorizonSeconds = MathF.Max(state.WaterShortageHorizonSeconds, 600f);
}
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
if (FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, commodityId) is not { } project)
{
return;
}
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}");
}
}
public sealed class ExpandToSystemAction : GoapAction<FactionPlanningState>
{
public override string Name => "expand-to-system";
public override float Cost => 3f;
public override bool CheckPreconditions(FactionPlanningState state) =>
state.ConstructorShipCount > 0 && state.MilitaryShipCount >= 2 && state.HasWarIndustrySupplyChain;
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
{
state.ControlledSystemCount++;
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
commander.ActiveDirectives.Add("expand-territory");
}
}
public sealed class LaunchExterminationCampaignAction : GoapAction<FactionPlanningState>
{
public override string Name => "launch-extermination-campaign";
public override float Cost => 1f;
public override bool CheckPreconditions(FactionPlanningState state) =>
state.EnemyFactionCount > 0
&& state.HasShipFactory
&& state.MilitaryShipCount >= Math.Max(2, FactionPlanningState.ComputeTargetWarships(state) / 2);
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
{
state.EnemyShipCount = 0;
state.EnemyStationCount = 0;
state.EnemyFactionCount = 0;
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
commander.ActiveDirectives.Add("attack-rival");
commander.ActiveDirectives.Add("produce-military-ships");
}
}