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(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 objectiveIndex, ISet 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 objectiveIndex, ISet touchedObjectiveIds, string mergeKey, FactionObjectiveKind kind, float priority, Action 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 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 objectiveIndex, ISet 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 objectiveIndex, ISet 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 objectiveIndex, ISet 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 record StepExecutionAssessment( FactionPlanStepStatus Status, string StatusReason, string? BlockingReason = null, StepExecutionBinding? Binding = null, IndustryExpansionProject? ExpectedProject = null); internal sealed record StepExecutionBinding( string Kind, string? TargetId, string Summary); 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."); var activeProject = FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId); commander.ActiveGoalName = null; commander.ActiveActionName = null; ResetRuntimeAssignments(commander); var touchedTaskIds = new HashSet(StringComparer.Ordinal); var assignedAssetIds = new HashSet(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)) { var assessment = EvaluateStep(world, commander, objective, step, blackboard, state, activeProject); EmitTasks(engine, world, commander, objective, step, assessment, 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 StepExecutionAssessment EvaluateStep( SimulationWorld world, CommanderRuntime commander, FactionObjectiveRuntime objective, FactionPlanStepRuntime step, FactionBlackboardRuntime blackboard, FactionPlanningState state, IndustryExpansionProject? activeProject) { step.LastEvaluatedCycle = commander.PlanningCycle; step.BlockingReason = null; step.StatusReason = null; step.ExecutionBindingKind = null; step.ExecutionBindingTargetId = null; step.ExecutionBindingSummary = null; StepExecutionAssessment assessment; if (step.DependencyStepIds.Count > 0 && HasIncompleteDependencies(commander, step)) { assessment = new StepExecutionAssessment( FactionPlanStepStatus.Blocked, "Blocked on prerequisite objective steps.", BlockingReason: "Waiting for prerequisite objective steps to complete."); } else { assessment = step.Kind switch { FactionPlanStepKind.EnsureCommodityProduction => EvaluateCommodityStep(world, commander, step, blackboard, activeProject), FactionPlanStepKind.EnsureShipyardSite => EvaluateShipyardStep(world, commander, step, state, activeProject), FactionPlanStepKind.ProduceFleet => EvaluateFleetProductionStep(commander, step, blackboard, state), FactionPlanStepKind.AttackFactionAssets => EvaluateAttackStep(commander, step, blackboard, state), FactionPlanStepKind.EnsureWaterSupply => EvaluateWaterStep(world, commander, step, blackboard, activeProject), FactionPlanStepKind.EnsureMiningCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.MinerShipCount, 2, "mining"), FactionPlanStepKind.EnsureConstructionCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.ConstructorShipCount, 1, "construction"), FactionPlanStepKind.EnsureTransportCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.TransportShipCount, 1, "transport"), FactionPlanStepKind.MonitorExpansionProject => EvaluateWarIndustryMonitorStep(world, commander, step, blackboard, activeProject), _ => new StepExecutionAssessment(FactionPlanStepStatus.Failed, "Unknown step kind."), }; } ApplyAssessment(step, assessment); return assessment; } private static StepExecutionAssessment EvaluateCommodityStep( SimulationWorld world, CommanderRuntime commander, FactionPlanStepRuntime step, FactionBlackboardRuntime blackboard, IndustryExpansionProject? activeProject) { if (string.IsNullOrWhiteSpace(step.CommodityId)) { return new StepExecutionAssessment( FactionPlanStepStatus.Failed, "Commodity step is missing a required commodity.", BlockingReason: "Commodity planning step is missing a target commodity."); } if (IsCommodityOperational(blackboard, step.CommodityId, 240f)) { step.ProducedFacts.Add($"commodity-online:{step.CommodityId}"); return new StepExecutionAssessment( FactionPlanStepStatus.Complete, $"Commodity {step.CommodityId} is operational in the faction economy."); } var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, step.CommodityId, ignoreActiveExpansionProject: true); return EvaluateExpansionRequirement(step, expectedProject, activeProject); } private static StepExecutionAssessment EvaluateShipyardStep( SimulationWorld world, CommanderRuntime commander, FactionPlanStepRuntime step, FactionPlanningState state, IndustryExpansionProject? activeProject) { if (state.HasShipFactory) { step.ProducedFacts.Add("shipyard-online"); return new StepExecutionAssessment( FactionPlanStepStatus.Complete, "Faction already has an online shipyard."); } var expectedProject = FactionIndustryPlanner.CreateShipyardFoundationProject(world, commander.FactionId, ignoreActiveExpansionProject: true); return EvaluateExpansionRequirement(step, expectedProject, activeProject); } private static StepExecutionAssessment EvaluateWaterStep( SimulationWorld world, CommanderRuntime commander, FactionPlanStepRuntime step, FactionBlackboardRuntime blackboard, IndustryExpansionProject? activeProject) { if (IsCommodityOperational(blackboard, "water", 300f)) { step.ProducedFacts.Add("commodity-online:water"); return new StepExecutionAssessment( FactionPlanStepStatus.Complete, "Water supply is operational."); } var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water", ignoreActiveExpansionProject: true); return EvaluateExpansionRequirement(step, expectedProject, activeProject); } private static StepExecutionAssessment EvaluateFleetProductionStep( CommanderRuntime commander, FactionPlanStepRuntime step, FactionBlackboardRuntime blackboard, FactionPlanningState state) { if (state.MilitaryShipCount >= blackboard.TargetWarshipCount) { return new StepExecutionAssessment( FactionPlanStepStatus.Complete, "Target war fleet size has been reached."); } if (!blackboard.HasShipyard) { return new StepExecutionAssessment( FactionPlanStepStatus.Blocked, "Fleet production requires an online shipyard.", BlockingReason: "Fleet production requires an online shipyard."); } if (TryFindIssuedTaskBinding(commander, step, out var binding)) { return new StepExecutionAssessment( FactionPlanStepStatus.Running, "Military fleet production is already bound to an issued ship-production task.", Binding: binding); } return new StepExecutionAssessment( FactionPlanStepStatus.Ready, "Shipyard is available; military fleet production can begin."); } private static StepExecutionAssessment EvaluateAttackStep( CommanderRuntime commander, FactionPlanStepRuntime step, FactionBlackboardRuntime blackboard, FactionPlanningState state) { if (blackboard.EnemyFactionCount <= 0) { return new StepExecutionAssessment( FactionPlanStepStatus.Complete, "No hostile faction remains to attack."); } if (state.MilitaryShipCount < Math.Max(2, blackboard.TargetWarshipCount / 2)) { return new StepExecutionAssessment( FactionPlanStepStatus.Blocked, "Insufficient military strength to begin the attack objective.", BlockingReason: "Insufficient military strength to commit to a faction attack objective."); } if (TryFindIssuedTaskBinding(commander, step, out var binding)) { return new StepExecutionAssessment( FactionPlanStepStatus.Running, "Attack objective is already bound to a matching combat task.", Binding: binding); } return new StepExecutionAssessment( FactionPlanStepStatus.Ready, "Combat strength is available; attack execution can begin."); } private static StepExecutionAssessment EvaluateCapacityStep( CommanderRuntime commander, FactionPlanStepRuntime step, bool hasShipyard, int currentCount, int requiredCount, string shipRole) { if (currentCount >= requiredCount) { return new StepExecutionAssessment( FactionPlanStepStatus.Complete, $"Faction already meets the required {shipRole} ship capacity."); } if (!hasShipyard) { return new StepExecutionAssessment( FactionPlanStepStatus.Blocked, $"No shipyard is currently assigned to produce {shipRole} ships.", BlockingReason: $"Ship capacity expansion for {shipRole} requires an online shipyard."); } if (TryFindIssuedTaskBinding(commander, step, out var binding)) { return new StepExecutionAssessment( FactionPlanStepStatus.Running, $"{shipRole} ship production is already bound to a matching issued task.", Binding: binding); } return new StepExecutionAssessment( FactionPlanStepStatus.Ready, $"Shipyard capacity is available; {shipRole} ship production can begin."); } private static StepExecutionAssessment EvaluateWarIndustryMonitorStep( SimulationWorld world, CommanderRuntime commander, FactionPlanStepRuntime step, FactionBlackboardRuntime blackboard, IndustryExpansionProject? activeProject) { if (blackboard.HasWarIndustrySupplyChain) { return new StepExecutionAssessment( FactionPlanStepStatus.Complete, "War-industry supply chain is operational."); } foreach (var commodityId in new[] { "refinedmetals", "hullparts", "claytronics" }) { if (IsCommodityOperational(blackboard, commodityId, 240f)) { continue; } var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, commodityId, ignoreActiveExpansionProject: true); return EvaluateExpansionRequirement(step, expectedProject, activeProject); } return new StepExecutionAssessment( FactionPlanStepStatus.Ready, "War-industry prerequisites are unresolved but no matching active project is bound yet."); } private static StepExecutionAssessment EvaluateExpansionRequirement( FactionPlanStepRuntime step, IndustryExpansionProject? expectedProject, IndustryExpansionProject? activeProject) { if (expectedProject is null) { return new StepExecutionAssessment( FactionPlanStepStatus.Blocked, "Unable to derive a valid expansion plan for the step outcome.", BlockingReason: BuildMissingPlanReason(step)); } if (activeProject is not null && ProjectsSemanticallyMatch(expectedProject, activeProject)) { return new StepExecutionAssessment( FactionPlanStepStatus.Running, $"Running on matching active expansion project {DescribeProject(activeProject)}.", Binding: new StepExecutionBinding( "expansion-project", activeProject.SiteId, $"Matched active project {DescribeProject(activeProject)}."), ExpectedProject: activeProject); } if (activeProject is not null) { return new StepExecutionAssessment( FactionPlanStepStatus.Blocked, $"Blocked by unrelated active expansion {DescribeProject(activeProject)}; step requires {DescribeProject(expectedProject)}.", BlockingReason: $"Active expansion {DescribeProject(activeProject)} does not satisfy required outcome {DescribeProject(expectedProject)}.", ExpectedProject: expectedProject); } return new StepExecutionAssessment( FactionPlanStepStatus.Ready, $"Ready to start required expansion {DescribeProject(expectedProject)}.", ExpectedProject: expectedProject); } private static void ApplyAssessment( FactionPlanStepRuntime step, StepExecutionAssessment assessment) { step.Status = assessment.Status; step.StatusReason = assessment.StatusReason; step.BlockingReason = assessment.BlockingReason; step.ExecutionBindingKind = assessment.Binding?.Kind; step.ExecutionBindingTargetId = assessment.Binding?.TargetId; step.ExecutionBindingSummary = assessment.Binding?.Summary; } 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, StepExecutionAssessment assessment, ISet touchedTaskIds, ISet 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: EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds); break; case FactionPlanStepKind.EnsureShipyardSite: EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds); break; case FactionPlanStepKind.ProduceFleet: 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); PromoteShipProductionStepToRunning(step, "military"); break; case FactionPlanStepKind.AttackFactionAssets: UpsertAttackTask(commander, objective, step, touchedTaskIds); AssignCombatAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds); PromoteCombatStepToRunning(step); break; case FactionPlanStepKind.EnsureWaterSupply: EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds); 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); PromoteShipProductionStepToRunning(step, "mining"); 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); PromoteShipProductionStepToRunning(step, "construction"); 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); PromoteShipProductionStepToRunning(step, "transport"); break; case FactionPlanStepKind.MonitorExpansionProject: UpsertWarIndustryTask(commander, objective, step, touchedTaskIds); break; } } private static void EmitExpansionExecution( SimulationWorld world, CommanderRuntime commander, FactionObjectiveRuntime objective, FactionPlanStepRuntime step, StepExecutionAssessment assessment, ISet touchedTaskIds, ISet assignedAssetIds) { var project = assessment.ExpectedProject; if (project is null) { UpsertExpansionTask( commander, objective, step, touchedTaskIds, commodityId: step.CommodityId, moduleId: step.ModuleId, targetSystemId: null, targetSiteId: null, blockingReason: step.BlockingReason, notes: step.StatusReason); return; } if (step.Status == FactionPlanStepStatus.Ready) { FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project); project = project with { SiteId = project.SiteId ?? FindMatchingSiteId(world, commander.FactionId, project) }; step.TargetSiteId = project.SiteId; step.Status = FactionPlanStepStatus.Running; step.StatusReason = $"Started required expansion {DescribeProject(project)}."; step.ExecutionBindingKind = "expansion-project"; step.ExecutionBindingTargetId = project.SiteId; step.ExecutionBindingSummary = $"Started site {project.SiteId ?? "pending"} for {DescribeProject(project)}."; } if (step.Status == FactionPlanStepStatus.Running) { step.TargetSiteId ??= project.SiteId; UpsertExpansionTask( commander, objective, step, touchedTaskIds, commodityId: project.CommodityId, moduleId: project.ModuleId, targetSystemId: project.SystemId, targetSiteId: project.SiteId, blockingReason: step.BlockingReason, notes: step.StatusReason); AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds); return; } UpsertExpansionTask( commander, objective, step, touchedTaskIds, commodityId: project.CommodityId, moduleId: project.ModuleId, targetSystemId: project.SystemId, targetSiteId: project.SiteId, blockingReason: step.BlockingReason, notes: step.StatusReason); } private static void PromoteShipProductionStepToRunning(FactionPlanStepRuntime step, string shipRole) { if (step.Status != FactionPlanStepStatus.Ready || step.AssignedAssetIds.Count == 0) { return; } step.Status = FactionPlanStepStatus.Running; step.StatusReason = $"{titleCase(shipRole)} ship production is bound to shipyard assets."; step.ExecutionBindingKind = "shipyard-production"; step.ExecutionBindingTargetId = step.AssignedAssetIds.FirstOrDefault(); step.ExecutionBindingSummary = $"Using shipyard assets {string.Join(", ", step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal))}."; } private static void PromoteCombatStepToRunning(FactionPlanStepRuntime step) { if (step.Status != FactionPlanStepStatus.Ready) { return; } if (step.AssignedAssetIds.Count <= 0) { step.Status = FactionPlanStepStatus.Blocked; step.BlockingReason = "No combat ships were available for the attack step."; step.StatusReason = step.BlockingReason; return; } step.Status = FactionPlanStepStatus.Running; step.StatusReason = "Attack step is bound to assigned combat ships."; step.ExecutionBindingKind = "combat-assets"; step.ExecutionBindingTargetId = step.AssignedAssetIds.FirstOrDefault(); step.ExecutionBindingSummary = $"Using combat ships {string.Join(", ", step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal))}."; } private static void ReconcileStaleTasks(CommanderRuntime commander, ISet 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 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 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 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 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 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 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 bool ProjectsSemanticallyMatch( IndustryExpansionProject expectedProject, IndustryExpansionProject activeProject) => string.Equals(expectedProject.CommodityId, activeProject.CommodityId, StringComparison.Ordinal) && string.Equals(expectedProject.ModuleId, activeProject.ModuleId, StringComparison.Ordinal) && string.Equals(expectedProject.SystemId, activeProject.SystemId, StringComparison.Ordinal) && string.Equals(expectedProject.CelestialId, activeProject.CelestialId, StringComparison.Ordinal); private static string DescribeProject(IndustryExpansionProject project) => $"{project.CommodityId}/{project.ModuleId} @ {project.SystemId}:{project.CelestialId}"; private static string BuildMissingPlanReason(FactionPlanStepRuntime step) => step.Kind switch { FactionPlanStepKind.EnsureCommodityProduction => $"Unable to derive an expansion project for required commodity {step.CommodityId}.", FactionPlanStepKind.EnsureWaterSupply => "Unable to derive an expansion project for water supply.", FactionPlanStepKind.EnsureShipyardSite => "Unable to identify a viable shipyard foundation project.", _ => "Unable to derive the required execution plan for this step.", }; private static bool TryFindIssuedTaskBinding( CommanderRuntime commander, FactionPlanStepRuntime step, out StepExecutionBinding binding) { var task = commander.IssuedTasks.FirstOrDefault(candidate => string.Equals(candidate.StepId, step.Id, StringComparison.Ordinal) && candidate.State == FactionIssuedTaskState.Active); if (task is null) { binding = default!; return false; } var targetId = task.TargetSiteId ?? task.TargetFactionId ?? task.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault(); binding = new StepExecutionBinding( "issued-task", targetId, $"Reusing active issued task {task.Kind} ({task.Id})."); return true; } private static string? FindMatchingSiteId( SimulationWorld world, string factionId, IndustryExpansionProject project) => world.ConstructionSites .Where(site => string.Equals(site.FactionId, factionId, StringComparison.Ordinal) && string.Equals(site.TargetKind, "station-foundation", StringComparison.Ordinal) && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed && string.Equals(site.TargetDefinitionId, project.CommodityId, StringComparison.Ordinal) && string.Equals(site.BlueprintId, project.ModuleId, StringComparison.Ordinal) && string.Equals(site.SystemId, project.SystemId, StringComparison.Ordinal) && string.Equals(site.CelestialId, project.CelestialId, StringComparison.Ordinal)) .Select(site => site.Id) .FirstOrDefault(); private static string titleCase(string value) => string.IsNullOrWhiteSpace(value) ? value : char.ToUpperInvariant(value[0]) + value[1..]; private static void AssignCombatAssets( SimulationWorld world, CommanderRuntime commander, FactionObjectiveRuntime objective, FactionPlanStepRuntime step, IReadOnlyCollection taskIds, ISet 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 taskIds, ISet 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 taskIds, IEnumerable 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); }