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

@@ -6,8 +6,9 @@ internal sealed class CommanderPlanningService
{
private const float FactionCommanderReplanInterval = 10f;
private const float ShipCommanderReplanInterval = 5f;
private readonly FactionObjectivePlanner _objectivePlanner = new();
private readonly FactionObjectiveExecutor _objectiveExecutor = new();
private static readonly GoapPlanner<FactionPlanningState> _factionPlanner = new(s => s.Clone());
private static readonly GoapPlanner<ShipPlanningState> _shipPlanner = new(s => s.Clone());
private static readonly IReadOnlyList<GoapGoal<FactionPlanningState>> _factionGoals =
@@ -76,12 +77,9 @@ internal sealed class CommanderPlanningService
commander.ReplanTimer = FactionCommanderReplanInterval;
commander.NeedsReplan = false;
commander.PlanningCycle += 1;
var state = BuildFactionPlanningState(world, commander.FactionId);
var actions = BuildFactionActions(world);
// Clear stale directives — actions will re-assert what is still needed.
commander.ActiveDirectives.Clear();
var rankedGoals = _factionGoals
.Select(g => (goal: g, priority: g.ComputePriority(state, world, commander)))
@@ -89,28 +87,15 @@ internal sealed class CommanderPlanningService
.OrderByDescending(x => x.priority)
.ToList();
commander.LastPlanningState = state;
commander.LastGoalPriorities = rankedGoals.Select(x => (x.goal.Name, x.priority)).ToList();
// Execute the first action of each active goal's plan (top 3 to avoid conflicts).
foreach (var (goal, _) in rankedGoals.Take(3))
{
var plan = _factionPlanner.Plan(state, goal, actions);
plan?.CurrentAction?.Execute(engine, world, commander);
}
if (FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId) is null)
{
if (rankedGoals.Any(entry => string.Equals(entry.goal.Name, "ensure-war-industry", StringComparison.Ordinal)))
{
TryQueueFactionExpansionProject(world, commander, SelectGoalDrivenWarIndustryProject(world, state, commander.FactionId));
}
else if (rankedGoals.Any(entry => string.Equals(entry.goal.Name, "ensure-water-security", StringComparison.Ordinal)))
{
TryQueueFactionExpansionProject(world, commander, FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water"));
}
}
commander.LastStrategicAssessment = state;
commander.LastStrategicPriorities = rankedGoals.Select(x => (x.goal.Name, x.priority)).ToList();
_objectivePlanner.UpdateBlackboard(world, commander, state);
_objectivePlanner.RefreshObjectives(
world,
commander,
state,
rankedGoals.Select(entry => (entry.goal.Name, entry.priority)).ToList());
_objectiveExecutor.Execute(engine, world, commander, state);
}
private void UpdateShipCommander(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
@@ -172,18 +157,30 @@ internal sealed class CommanderPlanningService
TargetSystemCount = Math.Max(1, Math.Min(StationSimulationService.StrategicControlTargetSystems, world.Systems.Count)),
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
OreStockpile = economy.GetCommodity("ore").OnHand,
RefinedMetalsStockpile = refinedMetals.OnHand,
RefinedMetalsProductionRate = refinedMetals.ProjectedProductionRatePerSecond,
RefinedMetalsShortageHorizonSeconds = refinedMetals.ProjectedShortageHorizonSeconds,
HullpartsStockpile = hullparts.OnHand,
HullpartsProductionRate = hullparts.ProjectedProductionRatePerSecond,
HullpartsShortageHorizonSeconds = hullparts.ProjectedShortageHorizonSeconds,
ClaytronicsStockpile = claytronics.OnHand,
ClaytronicsProductionRate = claytronics.ProjectedProductionRatePerSecond,
ClaytronicsShortageHorizonSeconds = claytronics.ProjectedShortageHorizonSeconds,
WaterStockpile = water.OnHand,
WaterProductionRate = water.ProjectedProductionRatePerSecond,
WaterShortageHorizonSeconds = water.ProjectedShortageHorizonSeconds,
RefinedMetalsAvailableStock = refinedMetals.AvailableStock,
RefinedMetalsUsageRate = refinedMetals.OperationalUsageRatePerSecond,
RefinedMetalsProjectedProductionRate = refinedMetals.ProjectedProductionRatePerSecond,
RefinedMetalsProjectedNetRate = refinedMetals.ProjectedNetRatePerSecond,
RefinedMetalsLevelSeconds = refinedMetals.LevelSeconds,
RefinedMetalsLevel = refinedMetals.Level.ToString().ToLowerInvariant(),
HullpartsAvailableStock = hullparts.AvailableStock,
HullpartsUsageRate = hullparts.OperationalUsageRatePerSecond,
HullpartsProjectedProductionRate = hullparts.ProjectedProductionRatePerSecond,
HullpartsProjectedNetRate = hullparts.ProjectedNetRatePerSecond,
HullpartsLevelSeconds = hullparts.LevelSeconds,
HullpartsLevel = hullparts.Level.ToString().ToLowerInvariant(),
ClaytronicsAvailableStock = claytronics.AvailableStock,
ClaytronicsUsageRate = claytronics.OperationalUsageRatePerSecond,
ClaytronicsProjectedProductionRate = claytronics.ProjectedProductionRatePerSecond,
ClaytronicsProjectedNetRate = claytronics.ProjectedNetRatePerSecond,
ClaytronicsLevelSeconds = claytronics.LevelSeconds,
ClaytronicsLevel = claytronics.Level.ToString().ToLowerInvariant(),
WaterAvailableStock = water.AvailableStock,
WaterUsageRate = water.OperationalUsageRatePerSecond,
WaterProjectedProductionRate = water.ProjectedProductionRatePerSecond,
WaterProjectedNetRate = water.ProjectedNetRatePerSecond,
WaterLevelSeconds = water.LevelSeconds,
WaterLevel = water.Level.ToString().ToLowerInvariant(),
};
}
@@ -192,16 +189,23 @@ internal sealed class CommanderPlanningService
ShipRuntime ship,
CommanderRuntime commander)
{
var factionCommander = world.Commanders.FirstOrDefault(c =>
c.FactionId == commander.FactionId &&
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
var factionCommander = FindFactionCommander(world, commander.FactionId);
var enemyTarget = SelectEnemyTarget(world, ship);
var tradeRoute = SelectTradeRoute(world, ship.FactionId);
var expansionTask = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.ExpandIndustry);
var attackTask = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.AttackFactionAssets);
var shipyardExpansionTask = factionCommander?.IssuedTasks
.Where(task =>
task.Kind == FactionIssuedTaskKind.ExpandIndustry
&& task.State is FactionIssuedTaskState.Planned or FactionIssuedTaskState.Active
&& string.Equals(task.ModuleId, "module_gen_build_l_01", StringComparison.Ordinal))
.OrderByDescending(task => task.Priority)
.FirstOrDefault();
var expansionProject = FactionIndustryPlanner.GetActiveExpansionProject(world, ship.FactionId);
if (commander.ActiveBehavior is not null)
{
commander.ActiveBehavior.AreaSystemId = enemyTarget?.SystemId;
commander.ActiveBehavior.AreaSystemId = attackTask?.TargetSystemId ?? expansionTask?.TargetSystemId ?? enemyTarget?.SystemId;
commander.ActiveBehavior.TargetEntityId = enemyTarget?.EntityId;
if (string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal))
{
@@ -209,12 +213,12 @@ internal sealed class CommanderPlanningService
commander.ActiveBehavior.StationId = tradeRoute?.SourceStationId;
commander.ActiveBehavior.TargetEntityId = tradeRoute?.DestinationStationId;
}
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal) && expansionProject is not null)
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal) && (expansionTask is not null || expansionProject is not null))
{
commander.ActiveBehavior.StationId = expansionProject.SupportStationId;
commander.ActiveBehavior.TargetEntityId = expansionProject.SiteId;
commander.ActiveBehavior.ModuleId = expansionProject.ModuleId;
commander.ActiveBehavior.AreaSystemId = expansionProject.SystemId;
commander.ActiveBehavior.StationId = expansionProject?.SupportStationId;
commander.ActiveBehavior.TargetEntityId = expansionTask?.TargetSiteId ?? expansionProject?.SiteId;
commander.ActiveBehavior.ModuleId = expansionTask?.ModuleId ?? expansionProject?.ModuleId;
commander.ActiveBehavior.AreaSystemId = expansionTask?.TargetSystemId ?? expansionProject?.SystemId;
}
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal))
{
@@ -229,15 +233,13 @@ internal sealed class CommanderPlanningService
ShipKind = ship.Definition.Kind,
HasMiningCapability = HasShipCapabilities(ship.Definition, "mining"),
FactionWantsOre = true,
FactionWantsCombat = factionCommander?.ActiveDirectives.Contains("attack-rival", StringComparer.Ordinal) ?? false,
FactionWantsExpansion = factionCommander?.ActiveDirectives
.Contains("expand-territory", StringComparer.Ordinal) ?? false,
FactionNeedsShipyard = !(factionCommander?.ActiveDirectives.Contains("bootstrap-war-industry", StringComparer.Ordinal) ?? false)
? false
: !world.Stations.Any(station =>
string.Equals(station.FactionId, ship.FactionId, StringComparison.Ordinal)
&& station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
TargetEnemySystemId = enemyTarget?.SystemId,
FactionWantsCombat = attackTask is not null,
FactionWantsExpansion = expansionTask is not null,
FactionNeedsShipyard = shipyardExpansionTask is not null
&& !world.Stations.Any(station =>
string.Equals(station.FactionId, ship.FactionId, StringComparison.Ordinal)
&& station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
TargetEnemySystemId = attackTask?.TargetSystemId ?? enemyTarget?.SystemId,
TargetEnemyEntityId = enemyTarget?.EntityId,
TradeItemId = tradeRoute?.ItemId,
TradeSourceStationId = tradeRoute?.SourceStationId,
@@ -245,70 +247,33 @@ internal sealed class CommanderPlanningService
};
}
private static IReadOnlyList<GoapAction<FactionPlanningState>> BuildFactionActions(SimulationWorld world)
{
var actions = new List<GoapAction<FactionPlanningState>>();
internal static CommanderRuntime? FindFactionCommander(SimulationWorld world, string factionId) =>
world.Commanders.FirstOrDefault(c =>
c.FactionId == factionId &&
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
actions.Add(new PlanWarIndustryAction());
actions.Add(new PlanCommoditySupplyAction("water"));
internal static bool FactionCommanderHasIssuedTask(
SimulationWorld world,
string factionId,
FactionIssuedTaskKind kind,
string? shipRole = null) =>
FindFactionCommander(world, factionId)?
.IssuedTasks.Any(task =>
task.Kind == kind
&& task.State is FactionIssuedTaskState.Planned or FactionIssuedTaskState.Active or FactionIssuedTaskState.Blocked
&& (shipRole is null || string.Equals(task.ShipRole, shipRole, StringComparison.Ordinal))) ?? false;
foreach (var (shipId, def) in world.ShipDefinitions)
{
actions.Add(new OrderShipProductionAction(def.Kind, shipId));
}
actions.Add(new LaunchExterminationCampaignAction());
actions.Add(new ExpandToSystemAction());
return actions;
}
internal static bool FactionCommanderHasDirective(SimulationWorld world, string factionId, string directive) =>
world.Commanders.FirstOrDefault(c =>
c.FactionId == factionId &&
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal))
?.ActiveDirectives.Contains(directive, StringComparer.Ordinal) ?? false;
private static void TryQueueFactionExpansionProject(
SimulationWorld world,
CommanderRuntime commander,
IndustryExpansionProject? project)
{
if (project is null)
{
return;
}
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}");
}
private static IndustryExpansionProject? SelectGoalDrivenWarIndustryProject(
SimulationWorld world,
FactionPlanningState state,
string factionId)
{
if (!state.HasRefinedMetalsProduction || state.RefinedMetalsShortageHorizonSeconds < 240f)
{
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "refinedmetals");
}
if (!state.HasHullpartsProduction || state.HullpartsShortageHorizonSeconds < 240f)
{
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "hullparts");
}
if (!state.HasClaytronicsProduction || state.ClaytronicsShortageHorizonSeconds < 240f)
{
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "claytronics");
}
if (!state.HasShipFactory)
{
return FactionIndustryPlanner.CreateShipyardFoundationProject(world, factionId);
}
return null;
}
internal static FactionIssuedTaskRuntime? GetHighestPriorityIssuedTask(
CommanderRuntime? factionCommander,
FactionIssuedTaskKind kind,
string? shipRole = null) =>
factionCommander?.IssuedTasks
.Where(task =>
task.Kind == kind
&& task.State is FactionIssuedTaskState.Planned or FactionIssuedTaskState.Active or FactionIssuedTaskState.Blocked
&& (shipRole is null || string.Equals(task.ShipRole, shipRole, StringComparison.Ordinal)))
.OrderByDescending(task => task.Priority)
.FirstOrDefault();
private static (string EntityId, string SystemId)? SelectEnemyTarget(SimulationWorld world, ShipRuntime ship)
{

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");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
namespace SpaceGame.Api.Factions.Contracts;
public sealed record FactionGoapStateSnapshot(
public sealed record FactionPlanningStateSnapshot(
int MilitaryShipCount,
int MinerShipCount,
int TransportShipCount,
@@ -9,17 +9,134 @@ public sealed record FactionGoapStateSnapshot(
int TargetSystemCount,
bool HasShipFactory,
float OreStockpile,
float RefinedMetalsStockpile,
float RefinedMetalsProductionRate,
float HullpartsStockpile,
float HullpartsProductionRate,
float ClaytronicsStockpile,
float ClaytronicsProductionRate,
float WaterStockpile,
float WaterProductionRate,
float WaterShortageHorizonSeconds);
float RefinedMetalsAvailableStock,
float RefinedMetalsUsageRate,
float RefinedMetalsProjectedProductionRate,
float RefinedMetalsProjectedNetRate,
float RefinedMetalsLevelSeconds,
string RefinedMetalsLevel,
float HullpartsAvailableStock,
float HullpartsUsageRate,
float HullpartsProjectedProductionRate,
float HullpartsProjectedNetRate,
float HullpartsLevelSeconds,
string HullpartsLevel,
float ClaytronicsAvailableStock,
float ClaytronicsUsageRate,
float ClaytronicsProjectedProductionRate,
float ClaytronicsProjectedNetRate,
float ClaytronicsLevelSeconds,
string ClaytronicsLevel,
float WaterAvailableStock,
float WaterUsageRate,
float WaterProjectedProductionRate,
float WaterProjectedNetRate,
float WaterLevelSeconds,
string WaterLevel);
public sealed record FactionGoapPrioritySnapshot(string GoalName, float Priority);
public sealed record FactionStrategicPrioritySnapshot(string GoalName, float Priority);
public sealed record FactionCommoditySignalSnapshot(
string ItemId,
float AvailableStock,
float OnHand,
float ProductionRatePerSecond,
float CommittedProductionRatePerSecond,
float UsageRatePerSecond,
float NetRatePerSecond,
float ProjectedNetRatePerSecond,
float LevelSeconds,
string Level,
float ProjectedProductionRatePerSecond,
float BuyBacklog,
float ReservedForConstruction);
public sealed record FactionThreatSignalSnapshot(
string ScopeId,
string ScopeKind,
int EnemyShipCount,
int EnemyStationCount);
public sealed record FactionBlackboardSnapshot(
int PlanCycle,
DateTimeOffset UpdatedAtUtc,
int TargetWarshipCount,
bool HasWarIndustrySupplyChain,
bool HasShipyard,
bool HasActiveExpansionProject,
string? ActiveExpansionCommodityId,
string? ActiveExpansionModuleId,
string? ActiveExpansionSiteId,
string? ActiveExpansionSystemId,
int EnemyFactionCount,
int EnemyShipCount,
int EnemyStationCount,
int MilitaryShipCount,
int MinerShipCount,
int TransportShipCount,
int ConstructorShipCount,
int ControlledSystemCount,
IReadOnlyList<FactionCommoditySignalSnapshot> CommoditySignals,
IReadOnlyList<FactionThreatSignalSnapshot> ThreatSignals);
public sealed record FactionPlanStepSnapshot(
string Id,
string Kind,
string Status,
float Priority,
string? CommodityId,
string? ModuleId,
string? TargetFactionId,
string? TargetSiteId,
string? BlockingReason,
string? Notes,
int LastEvaluatedCycle,
IReadOnlyList<string> DependencyStepIds,
IReadOnlyList<string> RequiredFacts,
IReadOnlyList<string> ProducedFacts,
IReadOnlyList<string> AssignedAssets,
IReadOnlyList<string> IssuedTaskIds);
public sealed record FactionIssuedTaskSnapshot(
string Id,
string Kind,
string State,
string ObjectiveId,
string StepId,
float Priority,
string? ShipRole,
string? CommodityId,
string? ModuleId,
string? TargetFactionId,
string? TargetSystemId,
string? TargetSiteId,
int CreatedAtCycle,
int UpdatedAtCycle,
string? BlockingReason,
string? Notes,
IReadOnlyList<string> AssignedAssets);
public sealed record FactionObjectiveSnapshot(
string Id,
string Kind,
string State,
float Priority,
string? ParentObjectiveId,
string? TargetFactionId,
string? TargetSystemId,
string? TargetSiteId,
string? TargetRegionId,
string? CommodityId,
string? ModuleId,
int BudgetWeight,
int SlotCost,
int CreatedAtCycle,
int UpdatedAtCycle,
string? InvalidationReason,
string? BlockingReason,
IReadOnlyList<string> PrerequisiteObjectiveIds,
IReadOnlyList<string> AssignedAssets,
IReadOnlyList<FactionPlanStepSnapshot> Steps);
public sealed record FactionSnapshot(
string Id,
@@ -32,8 +149,11 @@ public sealed record FactionSnapshot(
int ShipsBuilt,
int ShipsLost,
string? DefaultPolicySetId,
FactionGoapStateSnapshot? GoapState,
IReadOnlyList<FactionGoapPrioritySnapshot>? GoapPriorities);
FactionPlanningStateSnapshot? StrategicAssessment,
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
FactionBlackboardSnapshot? Blackboard,
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);
public sealed record FactionDelta(
string Id,
@@ -46,5 +166,8 @@ public sealed record FactionDelta(
int ShipsBuilt,
int ShipsLost,
string? DefaultPolicySetId,
FactionGoapStateSnapshot? GoapState,
IReadOnlyList<FactionGoapPrioritySnapshot>? GoapPriorities);
FactionPlanningStateSnapshot? StrategicAssessment,
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
FactionBlackboardSnapshot? Blackboard,
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);

View File

@@ -27,7 +27,6 @@ public sealed class CommanderRuntime
public string? PolicySetId { get; set; }
public string? Doctrine { get; set; }
public List<string> Goals { get; } = [];
public HashSet<string> ActiveDirectives { get; } = new(StringComparer.Ordinal);
public string? ActiveGoalName { get; set; }
public string? ActiveActionName { get; set; }
public float ReplanTimer { get; set; }
@@ -37,8 +36,194 @@ public sealed class CommanderRuntime
public CommanderTaskRuntime? ActiveTask { get; set; }
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
public bool IsAlive { get; set; } = true;
public FactionPlanningState? LastPlanningState { get; set; }
public IReadOnlyList<(string Name, float Priority)>? LastGoalPriorities { get; set; }
public FactionPlanningState? LastStrategicAssessment { get; set; }
public IReadOnlyList<(string Name, float Priority)>? LastStrategicPriorities { get; set; }
public FactionBlackboardRuntime? FactionBlackboard { get; set; }
public List<FactionObjectiveRuntime> Objectives { get; } = [];
public List<FactionIssuedTaskRuntime> IssuedTasks { get; } = [];
public int PlanningCycle { get; set; }
}
public enum FactionObjectiveKind
{
DestroyFaction,
BootstrapWarIndustry,
BuildShipyard,
BuildAttackFleet,
EnsureCommoditySupply,
EnsureWaterSecurity,
EnsureMiningCapacity,
EnsureConstructionCapacity,
EnsureTransportCapacity,
}
public enum FactionObjectiveState
{
Planned,
Active,
Blocked,
Complete,
Failed,
Cancelled,
}
public enum FactionPlanStepKind
{
EnsureCommodityProduction,
EnsureShipyardSite,
ProduceFleet,
AttackFactionAssets,
EnsureWaterSupply,
EnsureMiningCapacity,
EnsureConstructionCapacity,
EnsureTransportCapacity,
MonitorExpansionProject,
}
public enum FactionPlanStepStatus
{
Planned,
Ready,
Running,
Blocked,
Complete,
Failed,
Cancelled,
}
public enum FactionIssuedTaskKind
{
ExpandIndustry,
ProduceShips,
AttackFactionAssets,
SustainWarIndustry,
}
public enum FactionIssuedTaskState
{
Planned,
Active,
Blocked,
Complete,
Cancelled,
}
public sealed class FactionObjectiveRuntime
{
public required string Id { get; init; }
public required string MergeKey { get; init; }
public required FactionObjectiveKind Kind { get; init; }
public FactionObjectiveState State { get; set; } = FactionObjectiveState.Planned;
public float Priority { get; set; }
public string? ParentObjectiveId { get; set; }
public string? TargetFactionId { get; set; }
public string? TargetSystemId { get; set; }
public string? TargetSiteId { get; set; }
public string? TargetRegionId { get; set; }
public string? CommodityId { get; set; }
public string? ModuleId { get; set; }
public int BudgetWeight { get; set; }
public int SlotCost { get; set; } = 1;
public int CreatedAtCycle { get; init; }
public int UpdatedAtCycle { get; set; }
public string? InvalidationReason { get; set; }
public string? BlockingReason { get; set; }
public HashSet<string> PrerequisiteObjectiveIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
public List<FactionPlanStepRuntime> Steps { get; } = [];
}
public sealed class FactionPlanStepRuntime
{
public required string Id { get; init; }
public required string ObjectiveId { get; init; }
public required FactionPlanStepKind Kind { get; init; }
public FactionPlanStepStatus Status { get; set; } = FactionPlanStepStatus.Planned;
public float Priority { get; set; }
public string? CommodityId { get; set; }
public string? ModuleId { get; set; }
public string? TargetFactionId { get; set; }
public string? TargetSiteId { get; set; }
public string? BlockingReason { get; set; }
public string? Notes { get; set; }
public int LastEvaluatedCycle { get; set; }
public HashSet<string> DependencyStepIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> RequiredFacts { get; } = new(StringComparer.Ordinal);
public HashSet<string> ProducedFacts { get; } = new(StringComparer.Ordinal);
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> IssuedTaskIds { get; } = new(StringComparer.Ordinal);
}
public sealed class FactionIssuedTaskRuntime
{
public required string Id { get; init; }
public required string MergeKey { get; init; }
public required FactionIssuedTaskKind Kind { get; init; }
public required string ObjectiveId { get; init; }
public required string StepId { get; init; }
public FactionIssuedTaskState State { get; set; } = FactionIssuedTaskState.Planned;
public float Priority { get; set; }
public string? ShipRole { get; set; }
public string? CommodityId { get; set; }
public string? ModuleId { get; set; }
public string? TargetFactionId { get; set; }
public string? TargetSystemId { get; set; }
public string? TargetSiteId { get; set; }
public int CreatedAtCycle { get; init; }
public int UpdatedAtCycle { get; set; }
public string? BlockingReason { get; set; }
public string? Notes { get; set; }
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
}
public sealed class FactionBlackboardRuntime
{
public int PlanCycle { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; }
public int TargetWarshipCount { get; set; }
public bool HasWarIndustrySupplyChain { get; set; }
public bool HasShipyard { get; set; }
public bool HasActiveExpansionProject { get; set; }
public string? ActiveExpansionCommodityId { get; set; }
public string? ActiveExpansionModuleId { get; set; }
public string? ActiveExpansionSiteId { get; set; }
public string? ActiveExpansionSystemId { get; set; }
public int EnemyFactionCount { get; set; }
public int EnemyShipCount { get; set; }
public int EnemyStationCount { get; set; }
public int MilitaryShipCount { get; set; }
public int MinerShipCount { get; set; }
public int TransportShipCount { get; set; }
public int ConstructorShipCount { get; set; }
public int ControlledSystemCount { get; set; }
public List<FactionCommoditySignalRuntime> CommoditySignals { get; } = [];
public List<FactionThreatSignalRuntime> ThreatSignals { get; } = [];
public HashSet<string> AvailableShipIds { get; } = new(StringComparer.Ordinal);
}
public sealed class FactionCommoditySignalRuntime
{
public required string ItemId { get; init; }
public float AvailableStock { get; set; }
public float OnHand { get; set; }
public float ProductionRatePerSecond { get; set; }
public float CommittedProductionRatePerSecond { get; set; }
public float UsageRatePerSecond { get; set; }
public float NetRatePerSecond { get; set; }
public float ProjectedNetRatePerSecond { get; set; }
public float LevelSeconds { get; set; }
public string Level { get; set; } = "unknown";
public float ProjectedProductionRatePerSecond { get; set; }
public float BuyBacklog { get; set; }
public float ReservedForConstruction { get; set; }
}
public sealed class FactionThreatSignalRuntime
{
public required string ScopeId { get; init; }
public required string ScopeKind { get; init; }
public int EnemyShipCount { get; set; }
public int EnemyStationCount { get; set; }
}
public sealed class CommanderBehaviorRuntime