feat(ai): improving agents planning and memory
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user