Files
space-game/apps/backend/Factions/AI/FactionObjectivePlanning.cs

1170 lines
43 KiB
C#

namespace SpaceGame.Api.Factions.AI;
internal sealed class FactionObjectivePlanner
{
private readonly ObjectiveDependencyResolver _dependencyResolver = new();
private readonly ObjectiveStepFactory _stepFactory = new();
internal FactionBlackboardRuntime UpdateBlackboard(
SimulationWorld world,
CommanderRuntime commander,
FactionPlanningState state)
{
var blackboard = commander.FactionBlackboard ?? new FactionBlackboardRuntime();
var economy = FactionEconomyAnalyzer.Build(world, commander.FactionId);
var activeProject = FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId);
blackboard.PlanCycle = commander.PlanningCycle;
blackboard.UpdatedAtUtc = world.GeneratedAtUtc;
blackboard.TargetWarshipCount = FactionPlanningState.ComputeTargetWarships(state);
blackboard.HasWarIndustrySupplyChain = state.HasWarIndustrySupplyChain;
blackboard.HasShipyard = state.HasShipFactory;
blackboard.HasActiveExpansionProject = activeProject is not null;
blackboard.ActiveExpansionCommodityId = activeProject?.CommodityId;
blackboard.ActiveExpansionModuleId = activeProject?.ModuleId;
blackboard.ActiveExpansionSiteId = activeProject?.SiteId;
blackboard.ActiveExpansionSystemId = activeProject?.SystemId;
blackboard.EnemyFactionCount = state.EnemyFactionCount;
blackboard.EnemyShipCount = state.EnemyShipCount;
blackboard.EnemyStationCount = state.EnemyStationCount;
blackboard.MilitaryShipCount = state.MilitaryShipCount;
blackboard.MinerShipCount = state.MinerShipCount;
blackboard.TransportShipCount = state.TransportShipCount;
blackboard.ConstructorShipCount = state.ConstructorShipCount;
blackboard.ControlledSystemCount = state.ControlledSystemCount;
blackboard.AvailableShipIds.Clear();
foreach (var ship in world.Ships.Where(ship =>
ship.Health > 0f
&& string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)))
{
blackboard.AvailableShipIds.Add(ship.Id);
}
blackboard.CommoditySignals.Clear();
foreach (var commodityId in EnumerateTrackedCommodityIds())
{
var commodity = economy.GetCommodity(commodityId);
blackboard.CommoditySignals.Add(new FactionCommoditySignalRuntime
{
ItemId = commodityId,
AvailableStock = commodity.AvailableStock,
OnHand = commodity.OnHand,
ProductionRatePerSecond = commodity.ProductionRatePerSecond,
CommittedProductionRatePerSecond = commodity.CommittedProductionRatePerSecond,
UsageRatePerSecond = commodity.OperationalUsageRatePerSecond,
NetRatePerSecond = commodity.NetRatePerSecond,
ProjectedNetRatePerSecond = commodity.ProjectedNetRatePerSecond,
LevelSeconds = commodity.LevelSeconds,
Level = commodity.Level.ToString().ToLowerInvariant(),
ProjectedProductionRatePerSecond = commodity.ProjectedProductionRatePerSecond,
BuyBacklog = commodity.BuyBacklog,
ReservedForConstruction = commodity.ReservedForConstruction,
});
}
blackboard.ThreatSignals.Clear();
foreach (var threat in world.Systems
.Select(system => new FactionThreatSignalRuntime
{
ScopeId = system.Definition.Id,
ScopeKind = "system",
EnemyShipCount = world.Ships.Count(ship =>
ship.Health > 0f
&& string.Equals(ship.SystemId, system.Definition.Id, StringComparison.Ordinal)
&& !string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)),
EnemyStationCount = world.Stations.Count(station =>
string.Equals(station.SystemId, system.Definition.Id, StringComparison.Ordinal)
&& !string.Equals(station.FactionId, commander.FactionId, StringComparison.Ordinal)),
})
.Where(threat => threat.EnemyShipCount > 0 || threat.EnemyStationCount > 0))
{
blackboard.ThreatSignals.Add(threat);
}
commander.FactionBlackboard = blackboard;
return blackboard;
}
internal void RefreshObjectives(
SimulationWorld world,
CommanderRuntime commander,
FactionPlanningState state,
IReadOnlyList<(string GoalName, float Priority)> rankedGoals)
{
var blackboard = commander.FactionBlackboard ?? throw new InvalidOperationException("Faction blackboard must exist before objectives are refreshed.");
var objectiveIndex = commander.Objectives.ToDictionary(objective => objective.MergeKey, StringComparer.Ordinal);
var touchedObjectiveIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var enemyFaction in world.Factions.Where(faction =>
!string.Equals(faction.Id, commander.FactionId, StringComparison.Ordinal)))
{
var priority = rankedGoals.FirstOrDefault(goal => string.Equals(goal.GoalName, "exterminate-rival", StringComparison.Ordinal)).Priority;
var objective = GetOrCreateObjective(
commander,
objectiveIndex,
touchedObjectiveIds,
mergeKey: $"destroy-faction:{enemyFaction.Id}",
kind: FactionObjectiveKind.DestroyFaction,
priority: MathF.Max(priority, 1f),
configure: current =>
{
current.TargetFactionId = enemyFaction.Id;
current.State = enemyFaction.ShipsLost >= 0 ? current.State : FactionObjectiveState.Planned;
});
}
RefreshSupportObjective(
commander,
state,
rankedGoals,
objectiveIndex,
touchedObjectiveIds,
FactionObjectiveKind.BootstrapWarIndustry,
"bootstrap-war-industry",
"ensure-war-industry");
RefreshSupportObjective(
commander,
state,
rankedGoals,
objectiveIndex,
touchedObjectiveIds,
FactionObjectiveKind.BuildShipyard,
"build-shipyard",
"ensure-war-industry");
RefreshSupportObjective(
commander,
state,
rankedGoals,
objectiveIndex,
touchedObjectiveIds,
FactionObjectiveKind.BuildAttackFleet,
"build-attack-fleet",
"ensure-war-fleet");
RefreshSupportObjective(
commander,
state,
rankedGoals,
objectiveIndex,
touchedObjectiveIds,
FactionObjectiveKind.EnsureWaterSecurity,
"secure-water",
"ensure-water-security");
RefreshSupportObjective(
commander,
state,
rankedGoals,
objectiveIndex,
touchedObjectiveIds,
FactionObjectiveKind.EnsureMiningCapacity,
"ensure-mining-capacity",
"ensure-mining-capacity");
RefreshSupportObjective(
commander,
state,
rankedGoals,
objectiveIndex,
touchedObjectiveIds,
FactionObjectiveKind.EnsureConstructionCapacity,
"ensure-construction-capacity",
"ensure-construction-capacity");
for (var index = 0; index < commander.Objectives.Count; index += 1)
{
var objective = commander.Objectives[index];
if (!touchedObjectiveIds.Contains(objective.Id))
{
continue;
}
_stepFactory.EnsureObjectiveSteps(objective, blackboard, state);
_dependencyResolver.ResolveDependencies(world, commander, objective, blackboard, state, objectiveIndex, touchedObjectiveIds);
}
foreach (var objective in commander.Objectives)
{
objective.UpdatedAtCycle = commander.PlanningCycle;
if (!touchedObjectiveIds.Contains(objective.Id) && objective.State is not FactionObjectiveState.Complete and not FactionObjectiveState.Cancelled)
{
objective.State = FactionObjectiveState.Cancelled;
objective.InvalidationReason = "Objective no longer relevant to the current faction plan.";
}
}
}
private void RefreshSupportObjective(
CommanderRuntime commander,
FactionPlanningState state,
IReadOnlyList<(string GoalName, float Priority)> rankedGoals,
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
ISet<string> touchedObjectiveIds,
FactionObjectiveKind kind,
string mergeKey,
string goalName)
{
var priority = rankedGoals.FirstOrDefault(goal => string.Equals(goal.GoalName, goalName, StringComparison.Ordinal)).Priority;
if (priority <= 0f)
{
return;
}
var objective = GetOrCreateObjective(
commander,
objectiveIndex,
touchedObjectiveIds,
mergeKey,
kind,
priority,
configure: _ => { });
}
private static FactionObjectiveRuntime GetOrCreateObjective(
CommanderRuntime commander,
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
ISet<string> touchedObjectiveIds,
string mergeKey,
FactionObjectiveKind kind,
float priority,
Action<FactionObjectiveRuntime> configure)
{
if (!objectiveIndex.TryGetValue(mergeKey, out var objective))
{
objective = new FactionObjectiveRuntime
{
Id = $"obj-{commander.FactionId}-{commander.Objectives.Count + 1}",
MergeKey = mergeKey,
Kind = kind,
Priority = priority,
CreatedAtCycle = commander.PlanningCycle,
UpdatedAtCycle = commander.PlanningCycle,
};
commander.Objectives.Add(objective);
objectiveIndex[mergeKey] = objective;
}
objective.Priority = MathF.Max(objective.Priority, priority);
if (objective.State is FactionObjectiveState.Cancelled or FactionObjectiveState.Failed)
{
objective.State = FactionObjectiveState.Planned;
objective.InvalidationReason = null;
}
configure(objective);
touchedObjectiveIds.Add(objective.Id);
return objective;
}
private static IEnumerable<string> EnumerateTrackedCommodityIds()
{
yield return "ore";
yield return "refinedmetals";
yield return "hullparts";
yield return "claytronics";
yield return "water";
yield return "energycells";
}
}
internal sealed class ObjectiveDependencyResolver
{
internal void ResolveDependencies(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionBlackboardRuntime blackboard,
FactionPlanningState state,
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
ISet<string> touchedObjectiveIds)
{
switch (objective.Kind)
{
case FactionObjectiveKind.DestroyFaction:
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "bootstrap-war-industry", FactionObjectiveKind.BootstrapWarIndustry, objective.Priority - 10f);
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "build-shipyard", FactionObjectiveKind.BuildShipyard, objective.Priority - 12f);
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "build-attack-fleet", FactionObjectiveKind.BuildAttackFleet, objective.Priority - 6f);
break;
case FactionObjectiveKind.BootstrapWarIndustry:
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "refinedmetals", objective.Priority - 2f);
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "hullparts", objective.Priority - 4f);
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "claytronics", objective.Priority - 4f);
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "secure-water", FactionObjectiveKind.EnsureWaterSecurity, objective.Priority - 15f);
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-mining-capacity", FactionObjectiveKind.EnsureMiningCapacity, objective.Priority - 8f);
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-construction-capacity", FactionObjectiveKind.EnsureConstructionCapacity, objective.Priority - 9f);
break;
case FactionObjectiveKind.BuildShipyard:
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "refinedmetals", objective.Priority - 2f);
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "hullparts", objective.Priority - 1f);
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "claytronics", objective.Priority - 1f);
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-construction-capacity", FactionObjectiveKind.EnsureConstructionCapacity, objective.Priority - 5f);
break;
case FactionObjectiveKind.BuildAttackFleet:
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "build-shipyard", FactionObjectiveKind.BuildShipyard, objective.Priority - 2f);
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "bootstrap-war-industry", FactionObjectiveKind.BootstrapWarIndustry, objective.Priority - 3f);
break;
case FactionObjectiveKind.EnsureCommoditySupply:
if (string.Equals(objective.CommodityId, "refinedmetals", StringComparison.Ordinal))
{
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-mining-capacity", FactionObjectiveKind.EnsureMiningCapacity, objective.Priority - 2f);
}
break;
}
foreach (var step in objective.Steps)
{
step.DependencyStepIds.Clear();
foreach (var dependencyId in objective.PrerequisiteObjectiveIds)
{
if (commander.Objectives.FirstOrDefault(candidate => candidate.Id == dependencyId) is not { } dependencyObjective)
{
continue;
}
foreach (var dependencyStep in dependencyObjective.Steps)
{
if (dependencyStep.Status != FactionPlanStepStatus.Complete)
{
step.DependencyStepIds.Add(dependencyStep.Id);
}
}
}
}
}
private static void EnsureCommodityDependency(
CommanderRuntime commander,
FactionObjectiveRuntime parent,
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
ISet<string> touchedObjectiveIds,
string commodityId,
float priority)
{
var dependency = AddDependency(
commander,
parent,
objectiveIndex,
touchedObjectiveIds,
$"ensure-commodity:{commodityId}",
FactionObjectiveKind.EnsureCommoditySupply,
priority);
dependency.CommodityId = commodityId;
}
private static FactionObjectiveRuntime AddDependency(
CommanderRuntime commander,
FactionObjectiveRuntime parent,
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
ISet<string> touchedObjectiveIds,
string mergeKey,
FactionObjectiveKind kind,
float priority)
{
if (!objectiveIndex.TryGetValue(mergeKey, out var dependency))
{
dependency = new FactionObjectiveRuntime
{
Id = $"obj-{commander.FactionId}-{commander.Objectives.Count + 1}",
MergeKey = mergeKey,
Kind = kind,
Priority = MathF.Max(1f, priority),
CreatedAtCycle = commander.PlanningCycle,
UpdatedAtCycle = commander.PlanningCycle,
ParentObjectiveId = parent.Id,
};
commander.Objectives.Add(dependency);
objectiveIndex[mergeKey] = dependency;
}
else
{
dependency.Priority = MathF.Max(dependency.Priority, priority);
}
dependency.ParentObjectiveId ??= parent.Id;
parent.PrerequisiteObjectiveIds.Add(dependency.Id);
touchedObjectiveIds.Add(dependency.Id);
return dependency;
}
}
internal sealed class ObjectiveStepFactory
{
internal void EnsureObjectiveSteps(
FactionObjectiveRuntime objective,
FactionBlackboardRuntime blackboard,
FactionPlanningState state)
{
if (objective.Steps.Count > 0)
{
return;
}
switch (objective.Kind)
{
case FactionObjectiveKind.DestroyFaction:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.AttackFactionAssets, objective.Priority, targetFactionId: objective.TargetFactionId));
break;
case FactionObjectiveKind.BootstrapWarIndustry:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.MonitorExpansionProject, objective.Priority, notes: "Maintain the war-industry support program until the full chain exists."));
break;
case FactionObjectiveKind.BuildShipyard:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureShipyardSite, objective.Priority, moduleId: "module_gen_build_l_01"));
break;
case FactionObjectiveKind.BuildAttackFleet:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.ProduceFleet, objective.Priority));
break;
case FactionObjectiveKind.EnsureCommoditySupply:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureCommodityProduction, objective.Priority, commodityId: objective.CommodityId));
break;
case FactionObjectiveKind.EnsureWaterSecurity:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureWaterSupply, objective.Priority, commodityId: "water"));
break;
case FactionObjectiveKind.EnsureMiningCapacity:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureMiningCapacity, objective.Priority));
break;
case FactionObjectiveKind.EnsureConstructionCapacity:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureConstructionCapacity, objective.Priority));
break;
case FactionObjectiveKind.EnsureTransportCapacity:
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureTransportCapacity, objective.Priority));
break;
}
}
private static FactionPlanStepRuntime CreateStep(
FactionObjectiveRuntime objective,
FactionPlanStepKind kind,
float priority,
string? commodityId = null,
string? moduleId = null,
string? targetFactionId = null,
string? notes = null)
{
return new FactionPlanStepRuntime
{
Id = $"{objective.Id}-step-{objective.Steps.Count + 1}",
ObjectiveId = objective.Id,
Kind = kind,
Priority = priority,
CommodityId = commodityId,
ModuleId = moduleId,
TargetFactionId = targetFactionId,
Notes = notes,
};
}
}
internal sealed class FactionObjectiveExecutor
{
internal void Execute(
SimulationEngine engine,
SimulationWorld world,
CommanderRuntime commander,
FactionPlanningState state)
{
var blackboard = commander.FactionBlackboard ?? throw new InvalidOperationException("Faction blackboard must exist before objectives are executed.");
commander.ActiveGoalName = null;
commander.ActiveActionName = null;
ResetRuntimeAssignments(commander);
var touchedTaskIds = new HashSet<string>(StringComparer.Ordinal);
var assignedAssetIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var objective in commander.Objectives
.Where(objective => objective.State is not FactionObjectiveState.Cancelled and not FactionObjectiveState.Failed)
.OrderByDescending(objective => objective.Priority))
{
EvaluateObjective(world, commander, objective);
foreach (var step in objective.Steps.OrderByDescending(step => step.Priority))
{
EvaluateStep(world, commander, objective, step, blackboard, state);
EmitTasks(engine, world, commander, objective, step, blackboard, state, touchedTaskIds, assignedAssetIds);
}
}
ReconcileStaleTasks(commander, touchedTaskIds);
}
private static void ResetRuntimeAssignments(CommanderRuntime commander)
{
foreach (var objective in commander.Objectives)
{
objective.AssignedAssetIds.Clear();
foreach (var step in objective.Steps)
{
step.AssignedAssetIds.Clear();
step.IssuedTaskIds.Clear();
}
}
foreach (var task in commander.IssuedTasks)
{
task.AssignedAssetIds.Clear();
}
}
private static void EvaluateObjective(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective)
{
objective.BlockingReason = null;
objective.InvalidationReason = null;
var incompleteSteps = objective.Steps.Where(step => step.Status != FactionPlanStepStatus.Complete).ToList();
if (incompleteSteps.Count == 0)
{
objective.State = FactionObjectiveState.Complete;
return;
}
if (incompleteSteps.All(step => step.Status == FactionPlanStepStatus.Blocked))
{
objective.State = FactionObjectiveState.Blocked;
objective.BlockingReason = incompleteSteps.FirstOrDefault(step => !string.IsNullOrWhiteSpace(step.BlockingReason))?.BlockingReason;
return;
}
objective.State = FactionObjectiveState.Active;
}
private static void EvaluateStep(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
FactionPlanningState state)
{
step.LastEvaluatedCycle = commander.PlanningCycle;
step.BlockingReason = null;
if (step.DependencyStepIds.Count > 0 && HasIncompleteDependencies(commander, step))
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Waiting for prerequisite objective steps to complete.";
return;
}
switch (step.Kind)
{
case FactionPlanStepKind.EnsureCommodityProduction:
EvaluateCommodityStep(step, blackboard);
break;
case FactionPlanStepKind.EnsureShipyardSite:
if (state.HasShipFactory)
{
step.Status = FactionPlanStepStatus.Complete;
step.ProducedFacts.Add("shipyard-online");
}
else
{
step.Status = blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionModuleId, step.ModuleId, StringComparison.Ordinal)
? FactionPlanStepStatus.Running
: FactionPlanStepStatus.Ready;
}
break;
case FactionPlanStepKind.ProduceFleet:
step.Status = state.MilitaryShipCount >= blackboard.TargetWarshipCount
? FactionPlanStepStatus.Complete
: FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.AttackFactionAssets:
if (blackboard.EnemyFactionCount <= 0)
{
step.Status = FactionPlanStepStatus.Complete;
}
else if (state.MilitaryShipCount < Math.Max(2, blackboard.TargetWarshipCount / 2))
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Insufficient military strength to commit to a faction attack objective.";
}
else
{
step.Status = FactionPlanStepStatus.Running;
}
break;
case FactionPlanStepKind.EnsureWaterSupply:
step.Status = IsCommodityOperational(blackboard, "water", 300f)
? FactionPlanStepStatus.Complete
: blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionCommodityId, "water", StringComparison.Ordinal)
? FactionPlanStepStatus.Running
: FactionPlanStepStatus.Ready;
break;
case FactionPlanStepKind.EnsureMiningCapacity:
step.Status = state.MinerShipCount >= 2 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.EnsureConstructionCapacity:
step.Status = state.ConstructorShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.EnsureTransportCapacity:
step.Status = state.TransportShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.MonitorExpansionProject:
step.Status = blackboard.HasWarIndustrySupplyChain ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
}
}
private static void EvaluateCommodityStep(
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard)
{
if (string.IsNullOrWhiteSpace(step.CommodityId))
{
step.Status = FactionPlanStepStatus.Failed;
step.BlockingReason = "Commodity planning step is missing a target commodity.";
return;
}
var completed = IsCommodityOperational(blackboard, step.CommodityId, 240f);
step.Status = completed ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Ready;
if (completed)
{
step.ProducedFacts.Add($"commodity-online:{step.CommodityId}");
}
}
private static bool IsCommodityOperational(
FactionBlackboardRuntime blackboard,
string commodityId,
float minimumLevelSeconds)
{
var signal = blackboard.CommoditySignals.FirstOrDefault(candidate =>
string.Equals(candidate.ItemId, commodityId, StringComparison.Ordinal));
if (signal is null)
{
return false;
}
return signal.ProjectedProductionRatePerSecond > 0.01f
&& signal.ProjectedNetRatePerSecond >= -0.01f
&& signal.LevelSeconds >= minimumLevelSeconds
&& signal.Level is "stable" or "surplus";
}
private static void EmitTasks(
SimulationEngine engine,
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
FactionPlanningState state,
ISet<string> touchedTaskIds,
ISet<string> assignedAssetIds)
{
if (step.Status is FactionPlanStepStatus.Complete or FactionPlanStepStatus.Cancelled or FactionPlanStepStatus.Failed)
{
SyncTerminalTasks(commander, step, touchedTaskIds);
return;
}
commander.ActiveGoalName ??= objective.Kind.ToString();
commander.ActiveActionName ??= step.Kind.ToString();
switch (step.Kind)
{
case FactionPlanStepKind.EnsureCommodityProduction:
if (blackboard.HasActiveExpansionProject)
{
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: blackboard.ActiveExpansionCommodityId ?? step.CommodityId,
moduleId: blackboard.ActiveExpansionModuleId,
targetSystemId: blackboard.ActiveExpansionSystemId,
targetSiteId: blackboard.ActiveExpansionSiteId,
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Expansion project already active for faction.");
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
step.Status = FactionPlanStepStatus.Running;
return;
}
if (step.CommodityId is null)
{
return;
}
var project = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, step.CommodityId);
if (project is not null)
{
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
step.TargetSiteId = project.SiteId;
step.Status = FactionPlanStepStatus.Running;
step.Notes = $"Queued expansion project for {project.CommodityId}.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: project.CommodityId,
moduleId: project.ModuleId,
targetSystemId: project.SystemId,
targetSiteId: project.SiteId,
blockingReason: null,
notes: step.Notes);
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
}
else
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = $"Unable to derive an expansion project for {step.CommodityId}.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: step.CommodityId,
moduleId: step.ModuleId,
targetSystemId: null,
targetSiteId: null,
blockingReason: step.BlockingReason,
notes: step.Notes);
}
break;
case FactionPlanStepKind.EnsureShipyardSite:
if (blackboard.HasActiveExpansionProject)
{
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: blackboard.ActiveExpansionCommodityId,
moduleId: blackboard.ActiveExpansionModuleId ?? step.ModuleId,
targetSystemId: blackboard.ActiveExpansionSystemId,
targetSiteId: blackboard.ActiveExpansionSiteId,
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Shipyard support project waiting on current expansion site.");
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
step.Status = FactionPlanStepStatus.Running;
return;
}
var shipyardProject = FactionIndustryPlanner.CreateShipyardFoundationProject(world, commander.FactionId);
if (shipyardProject is not null)
{
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, shipyardProject);
step.TargetSiteId = shipyardProject.SiteId;
step.Status = FactionPlanStepStatus.Running;
step.Notes = "Queued shipyard foundation project.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: shipyardProject.CommodityId,
moduleId: shipyardProject.ModuleId,
targetSystemId: shipyardProject.SystemId,
targetSiteId: shipyardProject.SiteId,
blockingReason: null,
notes: step.Notes);
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
}
else
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Unable to identify a viable shipyard foundation project.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: step.CommodityId,
moduleId: step.ModuleId,
targetSystemId: null,
targetSiteId: null,
blockingReason: step.BlockingReason,
notes: step.Notes);
}
break;
case FactionPlanStepKind.ProduceFleet:
if (!blackboard.HasShipyard)
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Fleet production requires an online shipyard.";
}
UpsertShipProductionTask(
commander,
objective,
step,
touchedTaskIds,
shipRole: "military",
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Maintain military ship production until war fleet target is satisfied.");
AssignShipyardAssets(world, commander, objective, step);
break;
case FactionPlanStepKind.AttackFactionAssets:
UpsertAttackTask(commander, objective, step, touchedTaskIds);
AssignCombatAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
break;
case FactionPlanStepKind.EnsureWaterSupply:
if (blackboard.HasActiveExpansionProject)
{
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: blackboard.ActiveExpansionCommodityId,
moduleId: blackboard.ActiveExpansionModuleId,
targetSystemId: blackboard.ActiveExpansionSystemId,
targetSiteId: blackboard.ActiveExpansionSiteId,
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Water support project waiting on current expansion site.");
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
step.Status = FactionPlanStepStatus.Running;
return;
}
var waterProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water");
if (waterProject is not null)
{
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, waterProject);
step.Status = FactionPlanStepStatus.Running;
step.Notes = "Queued water expansion project.";
step.TargetSiteId = waterProject.SiteId;
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: waterProject.CommodityId,
moduleId: waterProject.ModuleId,
targetSystemId: waterProject.SystemId,
targetSiteId: waterProject.SiteId,
blockingReason: null,
notes: step.Notes);
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
}
else
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Unable to derive an expansion project for water.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: "water",
moduleId: step.ModuleId,
targetSystemId: null,
targetSiteId: null,
blockingReason: step.BlockingReason,
notes: step.Notes);
}
break;
case FactionPlanStepKind.EnsureMiningCapacity:
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "mining", step.BlockingReason, "Maintain mining ship production until logistical capacity is healthy.");
AssignShipyardAssets(world, commander, objective, step);
break;
case FactionPlanStepKind.EnsureConstructionCapacity:
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "construction", step.BlockingReason, "Maintain construction ship production until expansion support is healthy.");
AssignShipyardAssets(world, commander, objective, step);
break;
case FactionPlanStepKind.EnsureTransportCapacity:
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "transport", step.BlockingReason, "Maintain transport ship production until logistical throughput is healthy.");
AssignShipyardAssets(world, commander, objective, step);
break;
case FactionPlanStepKind.MonitorExpansionProject:
UpsertWarIndustryTask(commander, objective, step, touchedTaskIds);
break;
}
}
private static void ReconcileStaleTasks(CommanderRuntime commander, ISet<string> touchedTaskIds)
{
foreach (var task in commander.IssuedTasks)
{
task.UpdatedAtCycle = commander.PlanningCycle;
if (touchedTaskIds.Contains(task.Id))
{
continue;
}
if (task.State is FactionIssuedTaskState.Complete or FactionIssuedTaskState.Cancelled)
{
continue;
}
task.State = FactionIssuedTaskState.Cancelled;
task.BlockingReason = "Task no longer backed by an active faction plan step.";
task.AssignedAssetIds.Clear();
}
}
private static void SyncTerminalTasks(
CommanderRuntime commander,
FactionPlanStepRuntime step,
ISet<string> touchedTaskIds)
{
foreach (var task in commander.IssuedTasks.Where(candidate =>
string.Equals(candidate.StepId, step.Id, StringComparison.Ordinal)))
{
task.State = step.Status switch
{
FactionPlanStepStatus.Complete => FactionIssuedTaskState.Complete,
FactionPlanStepStatus.Failed => FactionIssuedTaskState.Cancelled,
FactionPlanStepStatus.Cancelled => FactionIssuedTaskState.Cancelled,
_ => task.State,
};
task.BlockingReason = step.BlockingReason;
task.UpdatedAtCycle = commander.PlanningCycle;
step.IssuedTaskIds.Add(task.Id);
touchedTaskIds.Add(task.Id);
}
}
private static void UpsertExpansionTask(
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
ISet<string> touchedTaskIds,
string? commodityId,
string? moduleId,
string? targetSystemId,
string? targetSiteId,
string? blockingReason,
string? notes)
{
var task = GetOrCreateTask(
commander,
objective,
step,
touchedTaskIds,
$"expand-industry:{objective.Id}:{step.Id}:{commodityId ?? moduleId ?? "general"}",
FactionIssuedTaskKind.ExpandIndustry);
task.CommodityId = commodityId;
task.ModuleId = moduleId;
task.TargetSystemId = targetSystemId;
task.TargetSiteId = targetSiteId;
task.Notes = notes;
task.BlockingReason = blockingReason;
task.State = MapStepStatus(step.Status);
}
private static void UpsertShipProductionTask(
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
ISet<string> touchedTaskIds,
string shipRole,
string? blockingReason,
string? notes)
{
var task = GetOrCreateTask(
commander,
objective,
step,
touchedTaskIds,
$"produce-ships:{shipRole}",
FactionIssuedTaskKind.ProduceShips);
task.ShipRole = shipRole;
task.BlockingReason = blockingReason;
task.Notes = notes;
task.State = MapStepStatus(step.Status);
}
private static void UpsertAttackTask(
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
ISet<string> touchedTaskIds)
{
var task = GetOrCreateTask(
commander,
objective,
step,
touchedTaskIds,
$"attack-faction:{step.TargetFactionId ?? objective.TargetFactionId ?? "unknown"}",
FactionIssuedTaskKind.AttackFactionAssets);
task.TargetFactionId = step.TargetFactionId ?? objective.TargetFactionId;
task.Notes = step.Notes ?? "Commit combat ships against the hostile faction.";
task.BlockingReason = step.BlockingReason;
task.State = MapStepStatus(step.Status);
}
private static void UpsertWarIndustryTask(
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
ISet<string> touchedTaskIds)
{
var task = GetOrCreateTask(
commander,
objective,
step,
touchedTaskIds,
"sustain-war-industry",
FactionIssuedTaskKind.SustainWarIndustry);
task.Notes = step.Notes ?? "Maintain the faction war-industry bootstrap program.";
task.BlockingReason = step.BlockingReason;
task.State = MapStepStatus(step.Status);
}
private static FactionIssuedTaskRuntime GetOrCreateTask(
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
ISet<string> touchedTaskIds,
string mergeKey,
FactionIssuedTaskKind kind)
{
var task = commander.IssuedTasks.FirstOrDefault(candidate =>
string.Equals(candidate.MergeKey, mergeKey, StringComparison.Ordinal));
if (task is null)
{
task = new FactionIssuedTaskRuntime
{
Id = $"task-{commander.FactionId}-{commander.IssuedTasks.Count + 1}",
MergeKey = mergeKey,
Kind = kind,
ObjectiveId = objective.Id,
StepId = step.Id,
Priority = step.Priority,
CreatedAtCycle = commander.PlanningCycle,
UpdatedAtCycle = commander.PlanningCycle,
};
commander.IssuedTasks.Add(task);
}
task.Priority = MathF.Max(task.Priority, step.Priority);
task.UpdatedAtCycle = commander.PlanningCycle;
task.BlockingReason = null;
task.Notes = step.Notes;
step.IssuedTaskIds.Add(task.Id);
touchedTaskIds.Add(task.Id);
return task;
}
private static FactionIssuedTaskState MapStepStatus(FactionPlanStepStatus status) =>
status switch
{
FactionPlanStepStatus.Planned => FactionIssuedTaskState.Planned,
FactionPlanStepStatus.Ready => FactionIssuedTaskState.Active,
FactionPlanStepStatus.Running => FactionIssuedTaskState.Active,
FactionPlanStepStatus.Blocked => FactionIssuedTaskState.Blocked,
FactionPlanStepStatus.Complete => FactionIssuedTaskState.Complete,
_ => FactionIssuedTaskState.Cancelled,
};
private static void AssignCombatAssets(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
IReadOnlyCollection<string> taskIds,
ISet<string> assignedAssetIds)
{
var availableShips = world.Ships
.Where(ship =>
ship.Health > 0f
&& string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)
&& string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal)
&& assignedAssetIds.Add(ship.Id))
.OrderBy(ship => ship.SystemId, StringComparer.Ordinal)
.ThenBy(ship => ship.Id, StringComparer.Ordinal)
.Take(4)
.ToList();
ApplyAssignments(commander, objective, step, taskIds, availableShips.Select(ship => ship.Id));
}
private static void AssignConstructionAssets(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
IReadOnlyCollection<string> taskIds,
ISet<string> assignedAssetIds)
{
var availableShips = world.Ships
.Where(ship =>
ship.Health > 0f
&& string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)
&& string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal)
&& assignedAssetIds.Add(ship.Id))
.OrderBy(ship => ship.SystemId, StringComparer.Ordinal)
.ThenBy(ship => ship.Id, StringComparer.Ordinal)
.Take(2)
.ToList();
ApplyAssignments(commander, objective, step, taskIds, availableShips.Select(ship => ship.Id));
}
private static void AssignShipyardAssets(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step)
{
var stationIds = world.Stations
.Where(station =>
string.Equals(station.FactionId, commander.FactionId, StringComparison.Ordinal)
&& station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal))
.Select(station => station.Id)
.OrderBy(id => id, StringComparer.Ordinal)
.ToList();
foreach (var stationId in stationIds)
{
objective.AssignedAssetIds.Add(stationId);
step.AssignedAssetIds.Add(stationId);
}
foreach (var taskId in step.IssuedTaskIds)
{
if (commander.IssuedTasks.FirstOrDefault(candidate => candidate.Id == taskId) is not { } task)
{
continue;
}
foreach (var stationId in stationIds)
{
task.AssignedAssetIds.Add(stationId);
}
}
}
private static void ApplyAssignments(
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
IReadOnlyCollection<string> taskIds,
IEnumerable<string> assetIds)
{
var materializedAssetIds = assetIds.ToList();
foreach (var assetId in materializedAssetIds)
{
objective.AssignedAssetIds.Add(assetId);
step.AssignedAssetIds.Add(assetId);
}
foreach (var taskId in taskIds)
{
if (commander.IssuedTasks.FirstOrDefault(candidate => candidate.Id == taskId) is not { } task)
{
continue;
}
foreach (var assetId in materializedAssetIds)
{
task.AssignedAssetIds.Add(assetId);
}
}
}
private static bool HasIncompleteDependencies(CommanderRuntime commander, FactionPlanStepRuntime step) =>
step.DependencyStepIds
.Select(dependencyId => commander.Objectives
.SelectMany(objective => objective.Steps)
.FirstOrDefault(candidate => candidate.Id == dependencyId))
.Any(dependency => dependency is not null && dependency.Status != FactionPlanStepStatus.Complete);
}