feat: massive AI generation

This commit is contained in:
2026-03-21 02:21:05 -04:00
parent 3b56785f9a
commit 6ccc708ae1
80 changed files with 16929 additions and 5427 deletions

View File

@@ -0,0 +1,283 @@
namespace SpaceGame.Api.Geopolitics.Contracts;
public sealed record SystemRouteLinkSnapshot(
string Id,
string SourceSystemId,
string DestinationSystemId,
float Distance,
bool IsPrimaryLane);
public sealed record DiplomaticRelationSnapshot(
string Id,
string FactionAId,
string FactionBId,
string Status,
string Posture,
float TrustScore,
float TensionScore,
float GrievanceScore,
string TradeAccessPolicy,
string MilitaryAccessPolicy,
string? WarStateId,
DateTimeOffset? CeasefireUntilUtc,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> ActiveTreatyIds,
IReadOnlyList<string> ActiveIncidentIds);
public sealed record TreatySnapshot(
string Id,
string Kind,
string Status,
string TradeAccessPolicy,
string MilitaryAccessPolicy,
string? Summary,
DateTimeOffset CreatedAtUtc,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> FactionIds);
public sealed record DiplomaticIncidentSnapshot(
string Id,
string Kind,
string Status,
string SourceFactionId,
string TargetFactionId,
string? SystemId,
string? BorderEdgeId,
string Summary,
float Severity,
float EscalationScore,
DateTimeOffset CreatedAtUtc,
DateTimeOffset LastObservedAtUtc);
public sealed record BorderTensionSnapshot(
string Id,
string RelationId,
string BorderEdgeId,
string FactionAId,
string FactionBId,
string Status,
float TensionScore,
float IncidentScore,
float MilitaryPressure,
float AccessFriction,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> SystemIds);
public sealed record WarStateSnapshot(
string Id,
string RelationId,
string FactionAId,
string FactionBId,
string Status,
string WarGoal,
float EscalationScore,
DateTimeOffset StartedAtUtc,
DateTimeOffset? CeasefireUntilUtc,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> ActiveFrontLineIds);
public sealed record DiplomaticStateSnapshot(
IReadOnlyList<DiplomaticRelationSnapshot> Relations,
IReadOnlyList<TreatySnapshot> Treaties,
IReadOnlyList<DiplomaticIncidentSnapshot> Incidents,
IReadOnlyList<BorderTensionSnapshot> BorderTensions,
IReadOnlyList<WarStateSnapshot> Wars);
public sealed record TerritoryClaimSnapshot(
string Id,
string? SourceClaimId,
string FactionId,
string SystemId,
string CelestialId,
string Status,
string ClaimKind,
float ClaimStrength,
DateTimeOffset UpdatedAtUtc);
public sealed record TerritoryInfluenceSnapshot(
string Id,
string SystemId,
string FactionId,
float ClaimStrength,
float AssetStrength,
float LogisticsStrength,
float TotalInfluence,
bool IsContesting,
DateTimeOffset UpdatedAtUtc);
public sealed record TerritoryControlStateSnapshot(
string SystemId,
string? ControllerFactionId,
string? PrimaryClaimantFactionId,
string ControlKind,
bool IsContested,
float ControlScore,
float StrategicValue,
IReadOnlyList<string> ClaimantFactionIds,
IReadOnlyList<string> InfluencingFactionIds,
DateTimeOffset UpdatedAtUtc);
public sealed record SectorStrategicProfileSnapshot(
string SystemId,
string? ControllerFactionId,
string ZoneKind,
bool IsContested,
float StrategicValue,
float SecurityRating,
float TerritorialPressure,
float LogisticsValue,
string? EconomicRegionId,
string? FrontLineId,
DateTimeOffset UpdatedAtUtc);
public sealed record BorderEdgeSnapshot(
string Id,
string SourceSystemId,
string DestinationSystemId,
string? SourceFactionId,
string? DestinationFactionId,
bool IsContested,
string? RelationId,
float TensionScore,
float CorridorImportance,
DateTimeOffset UpdatedAtUtc);
public sealed record FrontLineSnapshot(
string Id,
string Kind,
string Status,
string? AnchorSystemId,
float PressureScore,
float SupplyRisk,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> FactionIds,
IReadOnlyList<string> SystemIds,
IReadOnlyList<string> BorderEdgeIds);
public sealed record TerritoryZoneSnapshot(
string Id,
string SystemId,
string? FactionId,
string Kind,
string Status,
string? Reason,
DateTimeOffset UpdatedAtUtc);
public sealed record TerritoryPressureSnapshot(
string Id,
string SystemId,
string? FactionId,
string Kind,
float PressureScore,
float SecurityScore,
float HostileInfluence,
float CorridorRisk,
DateTimeOffset UpdatedAtUtc);
public sealed record TerritoryStateSnapshot(
IReadOnlyList<TerritoryClaimSnapshot> Claims,
IReadOnlyList<TerritoryInfluenceSnapshot> Influences,
IReadOnlyList<TerritoryControlStateSnapshot> ControlStates,
IReadOnlyList<SectorStrategicProfileSnapshot> StrategicProfiles,
IReadOnlyList<BorderEdgeSnapshot> BorderEdges,
IReadOnlyList<FrontLineSnapshot> FrontLines,
IReadOnlyList<TerritoryZoneSnapshot> Zones,
IReadOnlyList<TerritoryPressureSnapshot> Pressures);
public sealed record EconomicRegionSnapshot(
string Id,
string? FactionId,
string Label,
string Kind,
string Status,
string CoreSystemId,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> SystemIds,
IReadOnlyList<string> StationIds,
IReadOnlyList<string> FrontLineIds,
IReadOnlyList<string> CorridorIds);
public sealed record SupplyNetworkSnapshot(
string Id,
string RegionId,
float ThroughputScore,
float RiskScore,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> StationIds,
IReadOnlyList<string> ProducerItemIds,
IReadOnlyList<string> ConsumerItemIds,
IReadOnlyList<string> ConstructionItemIds);
public sealed record LogisticsCorridorSnapshot(
string Id,
string? FactionId,
string Kind,
string Status,
float RiskScore,
float ThroughputScore,
string AccessState,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> SystemPathIds,
IReadOnlyList<string> RegionIds,
IReadOnlyList<string> BorderEdgeIds);
public sealed record RegionalProductionProfileSnapshot(
string RegionId,
string PrimaryIndustry,
int ShipyardCount,
int StationCount,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<string> ProducedItemIds,
IReadOnlyList<string> ScarceItemIds);
public sealed record RegionalTradeBalanceSnapshot(
string RegionId,
int ImportsRequiredCount,
int ExportsSurplusCount,
int CriticalShortageCount,
float NetTradeScore,
DateTimeOffset UpdatedAtUtc);
public sealed record RegionalBottleneckSnapshot(
string Id,
string RegionId,
string ItemId,
string Cause,
string Status,
float Severity,
DateTimeOffset UpdatedAtUtc);
public sealed record RegionalSecurityAssessmentSnapshot(
string RegionId,
float SupplyRisk,
float BorderPressure,
int ActiveWarCount,
int HostileRelationCount,
float AccessFriction,
DateTimeOffset UpdatedAtUtc);
public sealed record RegionalEconomicAssessmentSnapshot(
string RegionId,
float SustainmentScore,
float ProductionDepth,
float ConstructionPressure,
float CorridorDependency,
DateTimeOffset UpdatedAtUtc);
public sealed record EconomyRegionStateSnapshot(
IReadOnlyList<EconomicRegionSnapshot> Regions,
IReadOnlyList<SupplyNetworkSnapshot> SupplyNetworks,
IReadOnlyList<LogisticsCorridorSnapshot> Corridors,
IReadOnlyList<RegionalProductionProfileSnapshot> ProductionProfiles,
IReadOnlyList<RegionalTradeBalanceSnapshot> TradeBalances,
IReadOnlyList<RegionalBottleneckSnapshot> Bottlenecks,
IReadOnlyList<RegionalSecurityAssessmentSnapshot> SecurityAssessments,
IReadOnlyList<RegionalEconomicAssessmentSnapshot> EconomicAssessments);
public sealed record GeopoliticalStateSnapshot(
int Cycle,
DateTimeOffset UpdatedAtUtc,
IReadOnlyList<SystemRouteLinkSnapshot> Routes,
DiplomaticStateSnapshot Diplomacy,
TerritoryStateSnapshot Territory,
EconomyRegionStateSnapshot EconomyRegions);

View File

@@ -0,0 +1,336 @@
namespace SpaceGame.Api.Geopolitics.Runtime;
public sealed class GeopoliticalStateRuntime
{
public int Cycle { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<SystemRouteLinkRuntime> Routes { get; } = [];
public DiplomaticStateRuntime Diplomacy { get; set; } = new();
public TerritoryStateRuntime Territory { get; set; } = new();
public EconomyRegionStateRuntime EconomyRegions { get; set; } = new();
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class SystemRouteLinkRuntime
{
public required string Id { get; init; }
public required string SourceSystemId { get; set; }
public required string DestinationSystemId { get; set; }
public float Distance { get; set; }
public bool IsPrimaryLane { get; set; } = true;
}
public sealed class DiplomaticStateRuntime
{
public List<DiplomaticRelationRuntime> Relations { get; } = [];
public List<TreatyRuntime> Treaties { get; } = [];
public List<DiplomaticIncidentRuntime> Incidents { get; } = [];
public List<BorderTensionRuntime> BorderTensions { get; } = [];
public List<WarStateRuntime> Wars { get; } = [];
}
public sealed class DiplomaticRelationRuntime
{
public required string Id { get; init; }
public required string FactionAId { get; set; }
public required string FactionBId { get; set; }
public string Status { get; set; } = "active";
public string Posture { get; set; } = "neutral";
public float TrustScore { get; set; }
public float TensionScore { get; set; }
public float GrievanceScore { get; set; }
public string TradeAccessPolicy { get; set; } = "restricted";
public string MilitaryAccessPolicy { get; set; } = "restricted";
public string? WarStateId { get; set; }
public DateTimeOffset? CeasefireUntilUtc { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ActiveTreatyIds { get; } = [];
public List<string> ActiveIncidentIds { get; } = [];
}
public sealed class TreatyRuntime
{
public required string Id { get; init; }
public required string Kind { get; set; }
public string Status { get; set; } = "active";
public string TradeAccessPolicy { get; set; } = "restricted";
public string MilitaryAccessPolicy { get; set; } = "restricted";
public string? Summary { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> FactionIds { get; } = [];
}
public sealed class DiplomaticIncidentRuntime
{
public required string Id { get; init; }
public required string Kind { get; set; }
public string Status { get; set; } = "active";
public required string SourceFactionId { get; set; }
public required string TargetFactionId { get; set; }
public string? SystemId { get; set; }
public string? BorderEdgeId { get; set; }
public required string Summary { get; set; }
public float Severity { get; set; }
public float EscalationScore { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset LastObservedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class BorderTensionRuntime
{
public required string Id { get; init; }
public required string RelationId { get; set; }
public required string BorderEdgeId { get; set; }
public required string FactionAId { get; set; }
public required string FactionBId { get; set; }
public string Status { get; set; } = "active";
public float TensionScore { get; set; }
public float IncidentScore { get; set; }
public float MilitaryPressure { get; set; }
public float AccessFriction { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> SystemIds { get; } = [];
}
public sealed class WarStateRuntime
{
public required string Id { get; init; }
public required string RelationId { get; set; }
public required string FactionAId { get; set; }
public required string FactionBId { get; set; }
public string Status { get; set; } = "active";
public string WarGoal { get; set; } = "territorial-pressure";
public float EscalationScore { get; set; }
public DateTimeOffset StartedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? CeasefireUntilUtc { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ActiveFrontLineIds { get; } = [];
}
public sealed class TerritoryStateRuntime
{
public List<TerritoryClaimRuntime> Claims { get; } = [];
public List<TerritoryInfluenceRuntime> Influences { get; } = [];
public List<TerritoryControlStateRuntime> ControlStates { get; } = [];
public List<SectorStrategicProfileRuntime> StrategicProfiles { get; } = [];
public List<BorderEdgeRuntime> BorderEdges { get; } = [];
public List<FrontLineRuntime> FrontLines { get; } = [];
public List<TerritoryZoneRuntime> Zones { get; } = [];
public List<TerritoryPressureRuntime> Pressures { get; } = [];
}
public sealed class TerritoryClaimRuntime
{
public required string Id { get; init; }
public string? SourceClaimId { get; set; }
public required string FactionId { get; set; }
public required string SystemId { get; set; }
public required string CelestialId { get; set; }
public string Status { get; set; } = "active";
public string ClaimKind { get; set; } = "infrastructure";
public float ClaimStrength { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class TerritoryInfluenceRuntime
{
public required string Id { get; init; }
public required string SystemId { get; set; }
public required string FactionId { get; set; }
public float ClaimStrength { get; set; }
public float AssetStrength { get; set; }
public float LogisticsStrength { get; set; }
public float TotalInfluence { get; set; }
public bool IsContesting { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class TerritoryControlStateRuntime
{
public required string SystemId { get; init; }
public string? ControllerFactionId { get; set; }
public string? PrimaryClaimantFactionId { get; set; }
public string ControlKind { get; set; } = "unclaimed";
public bool IsContested { get; set; }
public float ControlScore { get; set; }
public float StrategicValue { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ClaimantFactionIds { get; } = [];
public List<string> InfluencingFactionIds { get; } = [];
}
public sealed class SectorStrategicProfileRuntime
{
public required string SystemId { get; init; }
public string? ControllerFactionId { get; set; }
public string ZoneKind { get; set; } = "unclaimed";
public bool IsContested { get; set; }
public float StrategicValue { get; set; }
public float SecurityRating { get; set; }
public float TerritorialPressure { get; set; }
public float LogisticsValue { get; set; }
public string? EconomicRegionId { get; set; }
public string? FrontLineId { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class BorderEdgeRuntime
{
public required string Id { get; init; }
public required string SourceSystemId { get; set; }
public required string DestinationSystemId { get; set; }
public string? SourceFactionId { get; set; }
public string? DestinationFactionId { get; set; }
public bool IsContested { get; set; }
public string? RelationId { get; set; }
public float TensionScore { get; set; }
public float CorridorImportance { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class FrontLineRuntime
{
public required string Id { get; init; }
public string Kind { get; set; } = "border-front";
public string Status { get; set; } = "active";
public string? AnchorSystemId { get; set; }
public float PressureScore { get; set; }
public float SupplyRisk { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> FactionIds { get; } = [];
public List<string> SystemIds { get; } = [];
public List<string> BorderEdgeIds { get; } = [];
}
public sealed class TerritoryZoneRuntime
{
public required string Id { get; init; }
public required string SystemId { get; set; }
public string? FactionId { get; set; }
public string Kind { get; set; } = "unclaimed";
public string Status { get; set; } = "active";
public string? Reason { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class TerritoryPressureRuntime
{
public required string Id { get; init; }
public required string SystemId { get; set; }
public string? FactionId { get; set; }
public string Kind { get; set; } = "border-pressure";
public float PressureScore { get; set; }
public float SecurityScore { get; set; }
public float HostileInfluence { get; set; }
public float CorridorRisk { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class EconomyRegionStateRuntime
{
public List<EconomicRegionRuntime> Regions { get; } = [];
public List<SupplyNetworkRuntime> SupplyNetworks { get; } = [];
public List<LogisticsCorridorRuntime> Corridors { get; } = [];
public List<RegionalProductionProfileRuntime> ProductionProfiles { get; } = [];
public List<RegionalTradeBalanceRuntime> TradeBalances { get; } = [];
public List<RegionalBottleneckRuntime> Bottlenecks { get; } = [];
public List<RegionalSecurityAssessmentRuntime> SecurityAssessments { get; } = [];
public List<RegionalEconomicAssessmentRuntime> EconomicAssessments { get; } = [];
}
public sealed class EconomicRegionRuntime
{
public required string Id { get; init; }
public string? FactionId { get; set; }
public required string Label { get; set; }
public string Kind { get; set; } = "balanced-region";
public string Status { get; set; } = "active";
public required string CoreSystemId { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> SystemIds { get; } = [];
public List<string> StationIds { get; } = [];
public List<string> FrontLineIds { get; } = [];
public List<string> CorridorIds { get; } = [];
}
public sealed class SupplyNetworkRuntime
{
public required string Id { get; init; }
public required string RegionId { get; set; }
public float ThroughputScore { get; set; }
public float RiskScore { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> StationIds { get; } = [];
public List<string> ProducerItemIds { get; } = [];
public List<string> ConsumerItemIds { get; } = [];
public List<string> ConstructionItemIds { get; } = [];
}
public sealed class LogisticsCorridorRuntime
{
public required string Id { get; init; }
public string? FactionId { get; set; }
public string Kind { get; set; } = "supply-corridor";
public string Status { get; set; } = "active";
public float RiskScore { get; set; }
public float ThroughputScore { get; set; }
public string AccessState { get; set; } = "restricted";
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> SystemPathIds { get; } = [];
public List<string> RegionIds { get; } = [];
public List<string> BorderEdgeIds { get; } = [];
}
public sealed class RegionalProductionProfileRuntime
{
public required string RegionId { get; set; }
public string PrimaryIndustry { get; set; } = "mixed";
public int ShipyardCount { get; set; }
public int StationCount { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ProducedItemIds { get; } = [];
public List<string> ScarceItemIds { get; } = [];
}
public sealed class RegionalTradeBalanceRuntime
{
public required string RegionId { get; set; }
public int ImportsRequiredCount { get; set; }
public int ExportsSurplusCount { get; set; }
public int CriticalShortageCount { get; set; }
public float NetTradeScore { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class RegionalBottleneckRuntime
{
public required string Id { get; init; }
public required string RegionId { get; set; }
public required string ItemId { get; set; }
public string Cause { get; set; } = "regional-shortage";
public string Status { get; set; } = "active";
public float Severity { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class RegionalSecurityAssessmentRuntime
{
public required string RegionId { get; set; }
public float SupplyRisk { get; set; }
public float BorderPressure { get; set; }
public int ActiveWarCount { get; set; }
public int HostileRelationCount { get; set; }
public float AccessFriction { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class RegionalEconomicAssessmentRuntime
{
public required string RegionId { get; set; }
public float SustainmentScore { get; set; }
public float ProductionDepth { get; set; }
public float ConstructionPressure { get; set; }
public float CorridorDependency { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}

View File

@@ -0,0 +1,923 @@
using System.Globalization;
namespace SpaceGame.Api.Geopolitics.Simulation;
internal sealed class GeopoliticalSimulationService
{
internal void Update(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
{
var state = EnsureState(world);
state.Cycle += 1;
state.UpdatedAtUtc = world.GeneratedAtUtc;
RebuildRoutes(world, state);
RebuildTerritory(world, state);
RebuildDiplomacy(world, state, events);
RebuildEconomyRegions(world, state);
}
internal static GeopoliticalStateRuntime EnsureState(SimulationWorld world)
{
world.Geopolitics ??= new GeopoliticalStateRuntime();
return world.Geopolitics;
}
internal static DiplomaticRelationRuntime? FindRelation(SimulationWorld world, string factionAId, string factionBId)
{
var state = EnsureState(world);
return state.Diplomacy.Relations.FirstOrDefault(relation => string.Equals(relation.Id, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal));
}
internal static WarStateRuntime? FindWarState(SimulationWorld world, string factionAId, string factionBId) =>
EnsureState(world).Diplomacy.Wars.FirstOrDefault(war => string.Equals(war.RelationId, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal) && war.Status == "active");
internal static TerritoryControlStateRuntime? GetSystemControlState(SimulationWorld world, string systemId) =>
EnsureState(world).Territory.ControlStates.FirstOrDefault(state => string.Equals(state.SystemId, systemId, StringComparison.Ordinal));
internal static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) =>
string.Equals(GetSystemControlState(world, systemId)?.ControllerFactionId, factionId, StringComparison.Ordinal);
internal static IReadOnlyList<string> GetControlledSystems(SimulationWorld world, string factionId) =>
EnsureState(world).Territory.ControlStates
.Where(state => string.Equals(state.ControllerFactionId, factionId, StringComparison.Ordinal))
.OrderBy(state => state.SystemId, StringComparer.Ordinal)
.Select(state => state.SystemId)
.ToList();
internal static float GetSystemRouteRisk(SimulationWorld world, string systemId, string? factionId = null)
{
var pressure = EnsureState(world).Territory.Pressures
.Where(entry => string.Equals(entry.SystemId, systemId, StringComparison.Ordinal)
&& (factionId is null || string.Equals(entry.FactionId, factionId, StringComparison.Ordinal)))
.OrderByDescending(entry => entry.CorridorRisk)
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
.FirstOrDefault();
return pressure?.CorridorRisk
?? EnsureState(world).Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == systemId)?.TerritorialPressure
?? 0f;
}
internal static bool HasHostileRelation(SimulationWorld world, string factionAId, string factionBId)
{
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
{
return false;
}
var relation = FindRelation(world, factionAId, factionBId);
return relation is not null && relation.Posture is "hostile" or "war";
}
internal static bool HasTradeAccess(SimulationWorld world, string factionAId, string factionBId)
{
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
{
return true;
}
var relation = FindRelation(world, factionAId, factionBId);
return relation?.TradeAccessPolicy is "open" or "allied";
}
internal static bool HasMilitaryAccess(SimulationWorld world, string factionAId, string factionBId)
{
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
{
return true;
}
var relation = FindRelation(world, factionAId, factionBId);
return relation?.MilitaryAccessPolicy is "open" or "allied";
}
internal static EconomicRegionRuntime? GetPrimaryEconomicRegion(SimulationWorld world, string factionId, string systemId) =>
EnsureState(world).EconomyRegions.Regions.FirstOrDefault(region =>
string.Equals(region.FactionId, factionId, StringComparison.Ordinal)
&& region.SystemIds.Contains(systemId, StringComparer.Ordinal));
private static void RebuildRoutes(SimulationWorld world, GeopoliticalStateRuntime state)
{
state.Routes.Clear();
if (world.Systems.Count <= 1)
{
return;
}
var systems = world.Systems
.OrderBy(system => system.Definition.Id, StringComparer.Ordinal)
.ToList();
var routeIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var system in systems)
{
foreach (var neighbor in systems
.Where(candidate => candidate.Definition.Id != system.Definition.Id)
.Select(candidate => new
{
candidate.Definition.Id,
Distance = system.Position.DistanceTo(candidate.Position),
})
.OrderBy(candidate => candidate.Distance)
.ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
.Take(Math.Min(3, systems.Count - 1)))
{
var routeId = BuildPairId("route", system.Definition.Id, neighbor.Id);
if (!routeIds.Add(routeId))
{
continue;
}
state.Routes.Add(new SystemRouteLinkRuntime
{
Id = routeId,
SourceSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? system.Definition.Id : neighbor.Id,
DestinationSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? neighbor.Id : system.Definition.Id,
Distance = neighbor.Distance,
IsPrimaryLane = true,
});
}
}
}
private static void RebuildTerritory(SimulationWorld world, GeopoliticalStateRuntime state)
{
state.Territory.Claims.Clear();
state.Territory.Influences.Clear();
state.Territory.ControlStates.Clear();
state.Territory.StrategicProfiles.Clear();
state.Territory.BorderEdges.Clear();
state.Territory.FrontLines.Clear();
state.Territory.Zones.Clear();
state.Territory.Pressures.Clear();
var nowUtc = world.GeneratedAtUtc;
foreach (var claim in world.Claims.Where(claim => claim.State != ClaimStateKinds.Destroyed))
{
state.Territory.Claims.Add(new TerritoryClaimRuntime
{
Id = $"territory-{claim.Id}",
SourceClaimId = claim.Id,
FactionId = claim.FactionId,
SystemId = claim.SystemId,
CelestialId = claim.CelestialId,
Status = claim.State,
ClaimKind = "infrastructure",
ClaimStrength = claim.State == ClaimStateKinds.Active ? 1f : 0.65f,
UpdatedAtUtc = nowUtc,
});
}
var influencesBySystem = new Dictionary<string, List<TerritoryInfluenceRuntime>>(StringComparer.Ordinal);
foreach (var system in world.Systems)
{
var claimsByFaction = state.Territory.Claims
.Where(claim => claim.SystemId == system.Definition.Id)
.GroupBy(claim => claim.FactionId, StringComparer.Ordinal);
var stationsByFaction = world.Stations
.Where(station => station.SystemId == system.Definition.Id)
.GroupBy(station => station.FactionId, StringComparer.Ordinal);
var shipsByFaction = world.Ships
.Where(ship => ship.SystemId == system.Definition.Id && ship.Health > 0f)
.GroupBy(ship => ship.FactionId, StringComparer.Ordinal);
var sitesByFaction = world.ConstructionSites
.Where(site => site.SystemId == system.Definition.Id && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed)
.GroupBy(site => site.FactionId, StringComparer.Ordinal);
var factionIds = claimsByFaction.Select(group => group.Key)
.Concat(stationsByFaction.Select(group => group.Key))
.Concat(shipsByFaction.Select(group => group.Key))
.Concat(sitesByFaction.Select(group => group.Key))
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal)
.ToList();
var influences = new List<TerritoryInfluenceRuntime>();
foreach (var factionId in factionIds)
{
var claimStrength = claimsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(claim => claim.ClaimStrength * 40f) ?? 0f;
var stationStrength = (stationsByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 50f;
var siteStrength = (sitesByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 18f;
var shipStrength = shipsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(ship =>
ship.Definition.Kind switch
{
"military" => 9f,
"construction" => 4f,
"transport" => 3f,
_ when ship.Definition.Kind == "mining" || ship.Definition.Kind == "miner" => 3f,
_ => 2f,
}) ?? 0f;
var logisticsStrength = MathF.Min(30f, stationStrength * 0.18f) + siteStrength;
influences.Add(new TerritoryInfluenceRuntime
{
Id = $"influence-{system.Definition.Id}-{factionId}",
SystemId = system.Definition.Id,
FactionId = factionId,
ClaimStrength = claimStrength,
AssetStrength = stationStrength + shipStrength,
LogisticsStrength = logisticsStrength,
TotalInfluence = claimStrength + stationStrength + shipStrength + logisticsStrength,
UpdatedAtUtc = nowUtc,
});
}
influences.Sort((left, right) =>
{
var total = right.TotalInfluence.CompareTo(left.TotalInfluence);
return total != 0 ? total : string.Compare(left.FactionId, right.FactionId, StringComparison.Ordinal);
});
if (influences.Count > 1)
{
var lead = influences[0].TotalInfluence;
foreach (var influence in influences.Skip(1))
{
influence.IsContesting = influence.TotalInfluence >= (lead * 0.7f);
}
influences[0].IsContesting = influences[1].TotalInfluence >= (lead * 0.7f);
}
influencesBySystem[system.Definition.Id] = influences;
state.Territory.Influences.AddRange(influences);
var top = influences.FirstOrDefault();
var second = influences.Skip(1).FirstOrDefault();
var contested = top is not null && second is not null && second.TotalInfluence >= (top.TotalInfluence * 0.7f);
var controllerFactionId = top is not null && (!contested || top.TotalInfluence >= second!.TotalInfluence + 20f)
? top.FactionId
: null;
var primaryClaimantFactionId = state.Territory.Claims
.Where(claim => claim.SystemId == system.Definition.Id)
.GroupBy(claim => claim.FactionId, StringComparer.Ordinal)
.OrderByDescending(group => group.Sum(claim => claim.ClaimStrength))
.ThenBy(group => group.Key, StringComparer.Ordinal)
.Select(group => group.Key)
.FirstOrDefault();
var strategicValue = EstimateSystemStrategicValue(world, system.Definition.Id);
var controlState = new TerritoryControlStateRuntime
{
SystemId = system.Definition.Id,
ControllerFactionId = controllerFactionId,
PrimaryClaimantFactionId = primaryClaimantFactionId,
ControlKind = contested
? "contested"
: controllerFactionId is not null
? "controlled"
: primaryClaimantFactionId is not null
? "claimed"
: "unclaimed",
IsContested = contested,
ControlScore = top?.TotalInfluence ?? 0f,
StrategicValue = strategicValue,
UpdatedAtUtc = nowUtc,
};
controlState.ClaimantFactionIds.AddRange(state.Territory.Claims
.Where(claim => claim.SystemId == system.Definition.Id)
.Select(claim => claim.FactionId)
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal));
controlState.InfluencingFactionIds.AddRange(influences
.Select(influence => influence.FactionId)
.OrderBy(id => id, StringComparer.Ordinal));
state.Territory.ControlStates.Add(controlState);
}
foreach (var route in state.Routes)
{
var left = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.SourceSystemId);
var right = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.DestinationSystemId);
var differentControllers = !string.Equals(left.ControllerFactionId, right.ControllerFactionId, StringComparison.Ordinal);
var contested = left.IsContested || right.IsContested || differentControllers;
if (!contested && left.ControllerFactionId is null && right.ControllerFactionId is null)
{
continue;
}
state.Territory.BorderEdges.Add(new BorderEdgeRuntime
{
Id = $"border-{route.Id}",
SourceSystemId = route.SourceSystemId,
DestinationSystemId = route.DestinationSystemId,
SourceFactionId = left.ControllerFactionId ?? left.PrimaryClaimantFactionId,
DestinationFactionId = right.ControllerFactionId ?? right.PrimaryClaimantFactionId,
IsContested = contested,
TensionScore = MathF.Min(1f, MathF.Abs((left.ControlScore - right.ControlScore) / MathF.Max(50f, left.ControlScore + right.ControlScore))),
CorridorImportance = route.Distance <= 0.01f ? 0f : Math.Clamp((left.StrategicValue + right.StrategicValue) / MathF.Max(route.Distance, 1f), 0f, 1f),
UpdatedAtUtc = nowUtc,
});
}
foreach (var control in state.Territory.ControlStates)
{
var adjacentBorders = state.Territory.BorderEdges.Where(edge => edge.SourceSystemId == control.SystemId || edge.DestinationSystemId == control.SystemId).ToList();
var hostileBorderCount = adjacentBorders.Count(edge => edge.IsContested);
var corridorImportance = adjacentBorders.Sum(edge => edge.CorridorImportance);
var zoneKind = control.IsContested
? "contested"
: control.ControllerFactionId is null && control.PrimaryClaimantFactionId is not null
? "buffer"
: control.ControllerFactionId is not null && hostileBorderCount == 0
? "core"
: control.ControllerFactionId is not null && corridorImportance > 1.1f
? "corridor"
: control.ControllerFactionId is not null
? "frontier"
: "unclaimed";
state.Territory.Zones.Add(new TerritoryZoneRuntime
{
Id = $"zone-{control.SystemId}",
SystemId = control.SystemId,
FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
Kind = zoneKind,
Status = "active",
Reason = zoneKind == "corridor" ? "high-corridor-importance" : zoneKind == "frontier" ? "hostile-border-contact" : zoneKind,
UpdatedAtUtc = nowUtc,
});
state.Territory.StrategicProfiles.Add(new SectorStrategicProfileRuntime
{
SystemId = control.SystemId,
ControllerFactionId = control.ControllerFactionId,
ZoneKind = zoneKind,
IsContested = control.IsContested,
StrategicValue = control.StrategicValue,
SecurityRating = Math.Clamp(1f - (hostileBorderCount * 0.22f), 0f, 1f),
TerritorialPressure = Math.Clamp(hostileBorderCount * 0.25f, 0f, 1f),
LogisticsValue = Math.Clamp(corridorImportance, 0f, 1f),
UpdatedAtUtc = nowUtc,
});
state.Territory.Pressures.Add(new TerritoryPressureRuntime
{
Id = $"pressure-{control.SystemId}",
SystemId = control.SystemId,
FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
Kind = control.IsContested ? "contested-pressure" : "territorial-pressure",
PressureScore = Math.Clamp(hostileBorderCount * 0.28f, 0f, 1f),
SecurityScore = Math.Clamp(1f - (hostileBorderCount * 0.2f), 0f, 1f),
HostileInfluence = influencesBySystem.GetValueOrDefault(control.SystemId)?.Skip(control.ControllerFactionId is null ? 0 : 1).Sum(entry => entry.TotalInfluence) ?? 0f,
CorridorRisk = Math.Clamp(corridorImportance > 0.8f && hostileBorderCount > 0 ? 0.7f : hostileBorderCount * 0.2f, 0f, 1f),
UpdatedAtUtc = nowUtc,
});
}
}
private static void RebuildDiplomacy(SimulationWorld world, GeopoliticalStateRuntime state, ICollection<SimulationEventRecord> events)
{
state.Diplomacy.Relations.Clear();
state.Diplomacy.Treaties.Clear();
state.Diplomacy.BorderTensions.Clear();
state.Diplomacy.Wars.Clear();
var nowUtc = world.GeneratedAtUtc;
var factionPairs = world.Factions
.OrderBy(faction => faction.Id, StringComparer.Ordinal)
.SelectMany((left, index) => world.Factions.Skip(index + 1).Select(right => (left, right)));
foreach (var (leftFaction, rightFaction) in factionPairs)
{
var borderEdges = state.Territory.BorderEdges
.Where(edge =>
(string.Equals(edge.SourceFactionId, leftFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, rightFaction.Id, StringComparison.Ordinal))
|| (string.Equals(edge.SourceFactionId, rightFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, leftFaction.Id, StringComparison.Ordinal)))
.OrderBy(edge => edge.Id, StringComparer.Ordinal)
.ToList();
var sharedBorderPressure = borderEdges.Sum(edge => edge.TensionScore + (edge.IsContested ? 0.25f : 0f));
var conflictSystems = borderEdges.SelectMany(edge => new[] { edge.SourceSystemId, edge.DestinationSystemId }).Distinct(StringComparer.Ordinal).ToList();
var hostilePresence = world.Ships.Count(ship =>
ship.Health > 0f
&& ((ship.FactionId == leftFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))
|| (ship.FactionId == rightFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))));
var incidentSeverity = Math.Clamp(sharedBorderPressure + (hostilePresence * 0.03f), 0f, 1.6f);
var relationId = BuildRelationId(leftFaction.Id, rightFaction.Id);
var posture = incidentSeverity switch
{
>= 1.1f => "war",
>= 0.65f => "hostile",
>= 0.3f => "wary",
_ => "neutral",
};
var relation = new DiplomaticRelationRuntime
{
Id = relationId,
FactionAId = leftFaction.Id,
FactionBId = rightFaction.Id,
Status = "active",
Posture = posture,
TrustScore = Math.Clamp(0.7f - incidentSeverity, 0f, 1f),
TensionScore = Math.Clamp(incidentSeverity, 0f, 1f),
GrievanceScore = Math.Clamp(sharedBorderPressure, 0f, 1f),
TradeAccessPolicy = posture is "war" or "hostile" ? "restricted" : "open",
MilitaryAccessPolicy = posture == "neutral" ? "transit" : posture == "wary" ? "restricted" : "denied",
UpdatedAtUtc = nowUtc,
};
if (relation.Posture == "neutral")
{
var treaty = new TreatyRuntime
{
Id = $"treaty-open-trade-{relationId}",
Kind = "trade-understanding",
Status = "active",
TradeAccessPolicy = "open",
MilitaryAccessPolicy = "restricted",
Summary = $"Open civilian trade between {leftFaction.Label} and {rightFaction.Label}.",
CreatedAtUtc = nowUtc,
UpdatedAtUtc = nowUtc,
};
treaty.FactionIds.Add(leftFaction.Id);
treaty.FactionIds.Add(rightFaction.Id);
state.Diplomacy.Treaties.Add(treaty);
relation.ActiveTreatyIds.Add(treaty.Id);
relation.TradeAccessPolicy = "open";
}
state.Diplomacy.Relations.Add(relation);
foreach (var borderEdge in borderEdges)
{
borderEdge.RelationId = relation.Id;
borderEdge.TensionScore = Math.Clamp(borderEdge.TensionScore + (relation.TensionScore * 0.35f), 0f, 1f);
var tension = new BorderTensionRuntime
{
Id = $"tension-{borderEdge.Id}",
RelationId = relation.Id,
BorderEdgeId = borderEdge.Id,
FactionAId = leftFaction.Id,
FactionBId = rightFaction.Id,
Status = relation.Posture is "war" or "hostile" ? "escalating" : "stable",
TensionScore = relation.TensionScore,
IncidentScore = incidentSeverity,
MilitaryPressure = Math.Clamp(hostilePresence * 0.05f, 0f, 1f),
AccessFriction = relation.TradeAccessPolicy == "open" ? 0.15f : 0.75f,
UpdatedAtUtc = nowUtc,
};
tension.SystemIds.Add(borderEdge.SourceSystemId);
tension.SystemIds.Add(borderEdge.DestinationSystemId);
state.Diplomacy.BorderTensions.Add(tension);
if (tension.TensionScore >= 0.35f)
{
var incidentId = $"incident-border-{relationId}-{borderEdge.Id}";
var incident = new DiplomaticIncidentRuntime
{
Id = incidentId,
Kind = borderEdge.IsContested ? "border-clash" : "border-friction",
Status = relation.Posture == "war" ? "escalated" : "active",
SourceFactionId = leftFaction.Id,
TargetFactionId = rightFaction.Id,
SystemId = borderEdge.SourceSystemId,
BorderEdgeId = borderEdge.Id,
Summary = $"{leftFaction.Label} and {rightFaction.Label} are under pressure on {borderEdge.SourceSystemId}/{borderEdge.DestinationSystemId}.",
Severity = tension.TensionScore,
EscalationScore = tension.IncidentScore,
CreatedAtUtc = nowUtc,
LastObservedAtUtc = nowUtc,
};
state.Diplomacy.Incidents.Add(incident);
relation.ActiveIncidentIds.Add(incident.Id);
}
}
if (relation.Posture == "war")
{
var warId = $"war-{relationId}";
var war = new WarStateRuntime
{
Id = warId,
RelationId = relation.Id,
FactionAId = leftFaction.Id,
FactionBId = rightFaction.Id,
Status = "active",
WarGoal = "border-dominance",
EscalationScore = relation.TensionScore,
StartedAtUtc = nowUtc,
UpdatedAtUtc = nowUtc,
};
relation.WarStateId = war.Id;
state.Diplomacy.Wars.Add(war);
}
}
BuildFrontLines(state, nowUtc, events);
}
private static void BuildFrontLines(GeopoliticalStateRuntime state, DateTimeOffset nowUtc, ICollection<SimulationEventRecord> events)
{
foreach (var group in state.Diplomacy.BorderTensions
.Where(tension => tension.TensionScore >= 0.35f)
.GroupBy(tension => BuildPairId("front", tension.FactionAId, tension.FactionBId), StringComparer.Ordinal))
{
var tensions = group.OrderByDescending(tension => tension.TensionScore).ThenBy(tension => tension.Id, StringComparer.Ordinal).ToList();
var front = new FrontLineRuntime
{
Id = group.Key,
Kind = state.Diplomacy.Wars.Any(war => war.RelationId == tensions[0].RelationId && war.Status == "active") ? "war-front" : "border-front",
Status = "active",
AnchorSystemId = tensions.SelectMany(tension => tension.SystemIds).GroupBy(systemId => systemId, StringComparer.Ordinal).OrderByDescending(entry => entry.Count()).ThenBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => entry.Key).FirstOrDefault(),
PressureScore = Math.Clamp(tensions.Sum(tension => tension.TensionScore) / tensions.Count, 0f, 1f),
SupplyRisk = Math.Clamp(tensions.Sum(tension => tension.AccessFriction) / tensions.Count, 0f, 1f),
UpdatedAtUtc = nowUtc,
};
front.FactionIds.Add(tensions[0].FactionAId);
front.FactionIds.Add(tensions[0].FactionBId);
front.SystemIds.AddRange(tensions.SelectMany(tension => tension.SystemIds).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
front.BorderEdgeIds.AddRange(tensions.Select(tension => tension.BorderEdgeId).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
state.Territory.FrontLines.Add(front);
foreach (var war in state.Diplomacy.Wars.Where(war => string.Equals(war.RelationId, tensions[0].RelationId, StringComparison.Ordinal)))
{
war.ActiveFrontLineIds.Add(front.Id);
}
events.Add(new SimulationEventRecord("front-line", front.Id, "front-updated", $"Front {front.Id} pressure {front.PressureScore.ToString("0.00", CultureInfo.InvariantCulture)}.", nowUtc, "geopolitics"));
}
foreach (var profile in state.Territory.StrategicProfiles)
{
profile.FrontLineId = state.Territory.FrontLines.FirstOrDefault(front => front.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
}
}
private static void RebuildEconomyRegions(SimulationWorld world, GeopoliticalStateRuntime state)
{
state.EconomyRegions.Regions.Clear();
state.EconomyRegions.SupplyNetworks.Clear();
state.EconomyRegions.Corridors.Clear();
state.EconomyRegions.ProductionProfiles.Clear();
state.EconomyRegions.TradeBalances.Clear();
state.EconomyRegions.Bottlenecks.Clear();
state.EconomyRegions.SecurityAssessments.Clear();
state.EconomyRegions.EconomicAssessments.Clear();
var nowUtc = world.GeneratedAtUtc;
foreach (var faction in world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal))
{
var factionSystems = state.Territory.ControlStates
.Where(control => string.Equals(control.ControllerFactionId ?? control.PrimaryClaimantFactionId, faction.Id, StringComparison.Ordinal))
.Select(control => control.SystemId)
.Distinct(StringComparer.Ordinal)
.OrderBy(systemId => systemId, StringComparer.Ordinal)
.ToList();
if (factionSystems.Count == 0)
{
continue;
}
var connectedComponents = BuildConnectedComponents(factionSystems, state.Routes);
foreach (var component in connectedComponents)
{
var coreSystemId = component
.OrderByDescending(systemId => world.Stations.Count(station => station.FactionId == faction.Id && station.SystemId == systemId))
.ThenBy(systemId => systemId, StringComparer.Ordinal)
.First();
var regionId = $"region-{faction.Id}-{coreSystemId}";
var stations = world.Stations
.Where(station => station.FactionId == faction.Id && component.Contains(station.SystemId, StringComparer.Ordinal))
.OrderBy(station => station.Id, StringComparer.Ordinal)
.ToList();
var economy = BuildRegionalEconomy(world, faction.Id, component);
var regionKind = ResolveRegionKind(stations, economy);
var frontLineIds = state.Territory.FrontLines
.Where(front => front.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal)))
.Select(front => front.Id)
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal)
.ToList();
var region = new EconomicRegionRuntime
{
Id = regionId,
FactionId = faction.Id,
Label = $"{faction.Label} {coreSystemId}",
Kind = regionKind,
Status = "active",
CoreSystemId = coreSystemId,
UpdatedAtUtc = nowUtc,
};
region.SystemIds.AddRange(component.OrderBy(id => id, StringComparer.Ordinal));
region.StationIds.AddRange(stations.Select(station => station.Id));
region.FrontLineIds.AddRange(frontLineIds);
state.EconomyRegions.Regions.Add(region);
var producerItems = economy.Commodities
.Where(entry => entry.Value.ProductionRatePerSecond > 0.01f)
.OrderByDescending(entry => entry.Value.ProductionRatePerSecond)
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
.Take(8)
.Select(entry => entry.Key)
.ToList();
var scarceItems = economy.Commodities
.Where(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low)
.OrderByDescending(entry => CommodityOperationalSignal.ComputeNeedScore(entry.Value, 240f))
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
.Take(8)
.Select(entry => entry.Key)
.ToList();
var supplyNetwork = new SupplyNetworkRuntime
{
Id = $"network-{regionId}",
RegionId = regionId,
ThroughputScore = Math.Clamp(stations.Count * 0.18f, 0f, 1f),
RiskScore = Math.Clamp(frontLineIds.Count * 0.24f, 0f, 1f),
UpdatedAtUtc = nowUtc,
};
supplyNetwork.StationIds.AddRange(stations.Select(station => station.Id));
supplyNetwork.ProducerItemIds.AddRange(producerItems);
supplyNetwork.ConsumerItemIds.AddRange(scarceItems);
supplyNetwork.ConstructionItemIds.AddRange(world.ConstructionSites
.Where(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal))
.SelectMany(site => site.RequiredItems.Keys)
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal));
state.EconomyRegions.SupplyNetworks.Add(supplyNetwork);
var productionProfile = new RegionalProductionProfileRuntime
{
RegionId = regionId,
PrimaryIndustry = regionKind,
ShipyardCount = stations.Count(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
StationCount = stations.Count,
UpdatedAtUtc = nowUtc,
};
productionProfile.ProducedItemIds.AddRange(producerItems);
productionProfile.ScarceItemIds.AddRange(scarceItems);
state.EconomyRegions.ProductionProfiles.Add(productionProfile);
state.EconomyRegions.TradeBalances.Add(new RegionalTradeBalanceRuntime
{
RegionId = regionId,
ImportsRequiredCount = economy.Commodities.Count(entry => entry.Value.BuyBacklog > 0.01f),
ExportsSurplusCount = economy.Commodities.Count(entry => entry.Value.SellBacklog > 0.01f || entry.Value.Level == CommodityLevelKind.Surplus),
CriticalShortageCount = scarceItems.Count,
NetTradeScore = Math.Clamp((economy.Commodities.Sum(entry => entry.Value.ProjectedNetRatePerSecond) + 5f) / 10f, -1f, 1f),
UpdatedAtUtc = nowUtc,
});
if (scarceItems.FirstOrDefault() is { } bottleneckItemId)
{
state.EconomyRegions.Bottlenecks.Add(new RegionalBottleneckRuntime
{
Id = $"bottleneck-{regionId}-{bottleneckItemId}",
RegionId = regionId,
ItemId = bottleneckItemId,
Cause = "regional-shortage",
Status = "active",
Severity = Math.Clamp(CommodityOperationalSignal.ComputeNeedScore(economy.GetCommodity(bottleneckItemId), 240f), 0f, 10f),
UpdatedAtUtc = nowUtc,
});
}
var supplyRisk = Math.Clamp(frontLineIds.Count * 0.2f, 0f, 1f);
state.EconomyRegions.SecurityAssessments.Add(new RegionalSecurityAssessmentRuntime
{
RegionId = regionId,
SupplyRisk = supplyRisk,
BorderPressure = Math.Clamp(frontLineIds.Count * 0.22f, 0f, 1f),
ActiveWarCount = state.Diplomacy.Wars.Count(war => war.ActiveFrontLineIds.Intersect(frontLineIds, StringComparer.Ordinal).Any()),
HostileRelationCount = state.Diplomacy.Relations.Count(relation => relation.Posture is "hostile" or "war"),
AccessFriction = Math.Clamp(state.Diplomacy.BorderTensions.Where(tension => tension.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal))).DefaultIfEmpty().Average(tension => tension?.AccessFriction ?? 0f), 0f, 1f),
UpdatedAtUtc = nowUtc,
});
state.EconomyRegions.EconomicAssessments.Add(new RegionalEconomicAssessmentRuntime
{
RegionId = regionId,
SustainmentScore = Math.Clamp(1f - (scarceItems.Count * 0.12f) - (supplyRisk * 0.35f), 0f, 1f),
ProductionDepth = Math.Clamp(producerItems.Count / 8f, 0f, 1f),
ConstructionPressure = Math.Clamp(world.ConstructionSites.Count(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal)) * 0.22f, 0f, 1f),
CorridorDependency = Math.Clamp(frontLineIds.Count * 0.18f, 0f, 1f),
UpdatedAtUtc = nowUtc,
});
}
}
BuildCorridors(world, state, nowUtc);
foreach (var profile in state.Territory.StrategicProfiles)
{
profile.EconomicRegionId = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
}
}
private static void BuildCorridors(SimulationWorld world, GeopoliticalStateRuntime state, DateTimeOffset nowUtc)
{
foreach (var route in state.Routes)
{
var sourceRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.SourceSystemId, StringComparer.Ordinal));
var destinationRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.DestinationSystemId, StringComparer.Ordinal));
if (sourceRegion is null && destinationRegion is null)
{
continue;
}
var borderEdge = state.Territory.BorderEdges.FirstOrDefault(edge =>
(edge.SourceSystemId == route.SourceSystemId && edge.DestinationSystemId == route.DestinationSystemId)
|| (edge.SourceSystemId == route.DestinationSystemId && edge.DestinationSystemId == route.SourceSystemId));
var risk = borderEdge?.TensionScore ?? 0f;
var corridor = new LogisticsCorridorRuntime
{
Id = $"corridor-{route.Id}",
FactionId = sourceRegion?.FactionId ?? destinationRegion?.FactionId,
Kind = borderEdge?.IsContested == true ? "frontier-corridor" : "supply-corridor",
Status = borderEdge?.IsContested == true ? "risky" : "active",
RiskScore = Math.Clamp(risk + ((sourceRegion is not null && destinationRegion is not null && sourceRegion.Id != destinationRegion.Id) ? 0.15f : 0f), 0f, 1f),
ThroughputScore = Math.Clamp(((sourceRegion?.StationIds.Count ?? 0) + (destinationRegion?.StationIds.Count ?? 0)) / 10f, 0f, 1f),
AccessState = ResolveCorridorAccessState(world, borderEdge, sourceRegion, destinationRegion),
UpdatedAtUtc = nowUtc,
};
corridor.SystemPathIds.Add(route.SourceSystemId);
corridor.SystemPathIds.Add(route.DestinationSystemId);
if (sourceRegion is not null)
{
corridor.RegionIds.Add(sourceRegion.Id);
}
if (destinationRegion is not null && !corridor.RegionIds.Contains(destinationRegion.Id, StringComparer.Ordinal))
{
corridor.RegionIds.Add(destinationRegion.Id);
}
if (borderEdge is not null)
{
corridor.BorderEdgeIds.Add(borderEdge.Id);
}
state.EconomyRegions.Corridors.Add(corridor);
if (sourceRegion is not null && !sourceRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
{
sourceRegion.CorridorIds.Add(corridor.Id);
}
if (destinationRegion is not null && !destinationRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
{
destinationRegion.CorridorIds.Add(corridor.Id);
}
}
}
private static string ResolveCorridorAccessState(
SimulationWorld world,
BorderEdgeRuntime? borderEdge,
EconomicRegionRuntime? sourceRegion,
EconomicRegionRuntime? destinationRegion)
{
if (sourceRegion?.FactionId is null || destinationRegion?.FactionId is null)
{
return borderEdge?.IsContested == true ? "restricted" : "open";
}
var relation = FindRelation(world, sourceRegion.FactionId, destinationRegion.FactionId);
if (relation is null)
{
return "restricted";
}
return relation.Posture switch
{
"war" => "denied",
"hostile" => "restricted",
_ => relation.TradeAccessPolicy,
};
}
private static FactionEconomySnapshot BuildRegionalEconomy(SimulationWorld world, string factionId, IReadOnlyCollection<string> systemIds)
{
var snapshot = new FactionEconomySnapshot();
foreach (var station in world.Stations.Where(station => station.FactionId == factionId && systemIds.Contains(station.SystemId, StringComparer.Ordinal)))
{
foreach (var (itemId, amount) in station.Inventory)
{
snapshot.GetCommodity(itemId).OnHand += amount;
}
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
{
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
if (recipe is null)
{
continue;
}
var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe);
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
foreach (var input in recipe.Inputs)
{
snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond;
}
foreach (var output in recipe.Outputs)
{
snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond;
}
}
}
foreach (var order in world.MarketOrders.Where(order => order.FactionId == factionId))
{
var relatedSystemId = world.Stations.FirstOrDefault(station => station.Id == order.StationId)?.SystemId
?? world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId)?.SystemId;
if (relatedSystemId is null || !systemIds.Contains(relatedSystemId, StringComparer.Ordinal))
{
continue;
}
var commodity = snapshot.GetCommodity(order.ItemId);
if (order.Kind == MarketOrderKinds.Buy)
{
commodity.BuyBacklog += order.RemainingAmount;
}
else if (order.Kind == MarketOrderKinds.Sell)
{
commodity.SellBacklog += order.RemainingAmount;
}
}
foreach (var site in world.ConstructionSites.Where(site => site.FactionId == factionId && systemIds.Contains(site.SystemId, StringComparer.Ordinal)))
{
foreach (var required in site.RequiredItems)
{
var remaining = MathF.Max(0f, required.Value - (site.DeliveredItems.TryGetValue(required.Key, out var delivered) ? delivered : 0f));
if (remaining > 0.01f)
{
snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining;
}
}
}
return snapshot;
}
private static List<List<string>> BuildConnectedComponents(IReadOnlyCollection<string> systems, IReadOnlyCollection<SystemRouteLinkRuntime> routes)
{
var remaining = systems.ToHashSet(StringComparer.Ordinal);
var adjacency = routes
.SelectMany(route => new[]
{
(route.SourceSystemId, route.DestinationSystemId),
(route.DestinationSystemId, route.SourceSystemId),
})
.GroupBy(entry => entry.Item1, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.Select(entry => entry.Item2).ToList(), StringComparer.Ordinal);
var components = new List<List<string>>();
while (remaining.Count > 0)
{
var start = remaining.OrderBy(id => id, StringComparer.Ordinal).First();
var frontier = new Queue<string>();
frontier.Enqueue(start);
remaining.Remove(start);
var component = new List<string>();
while (frontier.Count > 0)
{
var current = frontier.Dequeue();
component.Add(current);
foreach (var neighbor in adjacency.GetValueOrDefault(current, []))
{
if (remaining.Remove(neighbor))
{
frontier.Enqueue(neighbor);
}
}
}
components.Add(component);
}
return components;
}
private static string ResolveRegionKind(IReadOnlyCollection<StationRuntime> stations, FactionEconomySnapshot economy)
{
if (stations.Any(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)))
{
return "shipbuilding-region";
}
if (stations.Count(station => StationSimulationService.DetermineStationRole(station) == "refinery") >= 2)
{
return "industrial-core";
}
if (economy.Commodities.Any(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low))
{
return "frontier-sustainment";
}
return stations.Count <= 2 ? "extraction-region" : "balanced-region";
}
private static float EstimateSystemStrategicValue(SimulationWorld world, string systemId)
{
var stationValue = world.Stations.Count(station => station.SystemId == systemId) * 30f;
var constructionValue = world.ConstructionSites.Count(site => site.SystemId == systemId && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed) * 18f;
var nodeValue = world.Nodes.Count(node => node.SystemId == systemId) * 8f;
return stationValue + constructionValue + nodeValue;
}
private static string BuildRelationId(string factionAId, string factionBId) =>
BuildPairId("relation", factionAId, factionBId);
private static string BuildPairId(string prefix, string leftId, string rightId)
{
return string.Compare(leftId, rightId, StringComparison.Ordinal) <= 0
? $"{prefix}-{leftId}-{rightId}"
: $"{prefix}-{rightId}-{leftId}";
}
}