Skip to content

Commit 4ccf01b

Browse files
committed
Optimize AI expansion logic: refine base limit, placement priority, and expansion radius
1 parent e9a70f0 commit 4ccf01b

2,879 files changed

Lines changed: 406076 additions & 60 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

OpenRA.Launcher/OpenRA.Launcher.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>Exe</OutputType>
44
<AssemblyName>OpenRA</AssemblyName>
@@ -24,6 +24,7 @@
2424
<ItemGroup>
2525
<None Include="App.config" />
2626
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj" />
27+
<ProjectReference Include="..\OpenRA.Platforms.Default\OpenRA.Platforms.Default.csproj" />
2728
<AdditionalFiles Include="Properties/launchSettings.json" />
2829
</ItemGroup>
2930
</Project>

OpenRA.Mods.Common/ServerCommands.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,16 @@ public static string ManageProductionCommand(JObject json, World world)
882882
}
883883
}
884884

885+
public static string ExpandBaseCommand(JObject json, World world)
886+
{
887+
var player = ResolvePlayer(json, world);
888+
var expansionManager = player.PlayerActor.TraitOrDefault<CopilotExpansionManager>();
889+
if (expansionManager == null)
890+
return "Player does not have CopilotExpansionManager trait.";
891+
892+
expansionManager.StartExpansion(player);
893+
return "Base expansion started.";
894+
}
885895

886896
public static string PlaceBuildingCommand(JObject json, World world)
887897
{
@@ -1563,6 +1573,7 @@ public void WorldLoaded(World w, WorldRenderer wr)
15631573

15641574
w.CopilotServer.CommandHandlers["place_building"] = PlaceBuildingCommand;
15651575
w.CopilotServer.CommandHandlers["manage_production"] = ManageProductionCommand;
1576+
w.CopilotServer.CommandHandlers["expand_base"] = ExpandBaseCommand;
15661577
w.CopilotServer.QueryHandlers["start_production"] = StartProductionCommand;
15671578

15681579
w.CopilotServer.QueryHandlers["query_actor"] = ActorQueryCommand;

OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public class BaseBuilderBotModuleInfo : ConditionalTraitInfo
7474
public readonly int InititalMinimumRefineryCount = 1;
7575

7676
[Desc("Number of refineries to build additionally after building a barracks.")]
77-
public readonly int AdditionalMinimumRefineryCount = 1;
77+
public readonly int AdditionalMinimumRefineryCount = 2;
7878

7979
[Desc("Additional delay (in ticks) between structure production checks when there is no active production.",
8080
"StructureProductionRandomBonusDelay is added to this.")]
@@ -149,6 +149,14 @@ public CPos GetRandomBaseCenter()
149149
return randomConstructionYard?.Location ?? initialBaseCenter;
150150
}
151151

152+
public IEnumerable<Actor> GetBaseActors()
153+
{
154+
if (constructionYardBuildings.Actors.Any())
155+
return constructionYardBuildings.Actors;
156+
157+
return Enumerable.Empty<Actor>();
158+
}
159+
152160
public CPos DefenseCenter { get; private set; }
153161

154162
// Actor, ActorCount.
@@ -319,10 +327,23 @@ public bool HasAdequateRefineryCount() =>
319327
AIUtils.CountActorByCommonName(powerBuildings) == 0 ||
320328
AIUtils.CountActorByCommonName(constructionYardBuildings) == 0;
321329

322-
int MinimumRefineryCount() =>
323-
AIUtils.CountActorByCommonName(barracksBuildings) > 0
324-
? Info.InititalMinimumRefineryCount + Info.AdditionalMinimumRefineryCount
325-
: Info.InititalMinimumRefineryCount;
330+
public int MinimumRefineryCount()
331+
{
332+
var baseCount = System.Math.Max(1, AIUtils.CountActorByCommonName(constructionYardBuildings));
333+
334+
// Start with initial count
335+
var count = Info.InititalMinimumRefineryCount;
336+
337+
// If we have barracks, add additional count
338+
if (AIUtils.CountActorByCommonName(barracksBuildings) > 0)
339+
count += Info.AdditionalMinimumRefineryCount;
340+
341+
// For every additional base beyond the first, add 3 refineries
342+
if (baseCount > 1)
343+
count += (baseCount - 1) * 3;
344+
345+
return count;
346+
}
326347

327348
List<MiniYamlNode> IGameSaveTraitData.IssueTraitData(Actor self)
328349
{

OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ ActorInfo GetProducibleBuilding(HashSet<string> actors, IEnumerable<ActorInfo> b
223223
if (!baseBuilder.Info.BuildingLimits.TryGetValue(actor.Name, out var limit))
224224
return true;
225225

226+
// Allow dynamic refinery count to override static limits from YAML
227+
if (baseBuilder.Info.RefineryTypes.Contains(actor.Name))
228+
limit = Math.Max(limit, baseBuilder.MinimumRefineryCount());
229+
226230
return playerBuildings.Count(a => a.Info.Name == actor.Name) < limit;
227231
});
228232

@@ -470,44 +474,77 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue)
470474
return (null, 0);
471475
}
472476

473-
var baseCenter = baseBuilder.GetRandomBaseCenter();
477+
var baseActors = baseBuilder.GetBaseActors();
478+
var baseLocations = new List<CPos>();
474479

475-
switch (type)
480+
if (baseActors.Any())
476481
{
477-
case BuildingType.Defense:
482+
// Strategy:
483+
// Refinery: Prioritize the newest base (highest ActorID) to support expansion economy.
484+
// Others: Randomize to distribute buildings.
485+
if (type == BuildingType.Refinery)
486+
{
487+
baseLocations.AddRange(baseActors.OrderByDescending(a => a.ActorID).Select(a => a.Location));
488+
}
489+
else
490+
{
491+
baseLocations.AddRange(baseActors.Select(a => a.Location).Shuffle(world.LocalRandom));
492+
}
493+
}
494+
else
495+
{
496+
// Fallback if no construction yards (e.g. only initial base center known)
497+
baseLocations.Add(baseBuilder.GetRandomBaseCenter());
498+
}
478499

479-
// Build near the closest enemy structure
480-
var closestEnemy = world.ActorsHavingTrait<Building>()
481-
.Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy)
482-
.ClosestToIgnoringPath(world.Map.CenterOfCell(baseBuilder.DefenseCenter));
500+
foreach (var baseCenter in baseLocations)
501+
{
502+
switch (type)
503+
{
504+
case BuildingType.Defense:
483505

484-
var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter;
506+
// Build near the closest enemy structure
507+
var closestEnemy = world.ActorsHavingTrait<Building>()
508+
.Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy)
509+
.ClosestToIgnoringPath(world.Map.CenterOfCell(baseBuilder.DefenseCenter));
485510

486-
return FindPos(baseBuilder.DefenseCenter, targetCell, baseBuilder.Info.MinimumDefenseRadius, baseBuilder.Info.MaximumDefenseRadius);
511+
var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter;
487512

488-
case BuildingType.Refinery:
513+
// Defense placement is usually centered around DefenseCenter, but we use baseCenter as fallback target if no enemy.
514+
// Note: If we really want to support multiple defense centers, that would require larger changes.
515+
// For now, we return immediately for Defense as it uses global DefenseCenter.
516+
return FindPos(baseBuilder.DefenseCenter, targetCell, baseBuilder.Info.MinimumDefenseRadius, baseBuilder.Info.MaximumDefenseRadius);
489517

490-
// Try and place the refinery near a resource field
491-
if (resourceLayer != null)
492-
{
493-
var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius)
494-
.Where(a => resourceLayer.GetResource(a).Type != null)
495-
.Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck);
518+
case BuildingType.Refinery:
496519

497-
foreach (var r in nearbyResources)
520+
// Try and place the refinery near a resource field
521+
if (resourceLayer != null)
498522
{
499-
var found = FindPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
500-
if (found.Location != null)
501-
return found;
502-
}
503-
}
523+
var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius)
524+
.Where(a => resourceLayer.GetResource(a).Type != null)
525+
.Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck);
504526

505-
// Try and find a free spot somewhere else in the base
506-
return FindPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
527+
foreach (var r in nearbyResources)
528+
{
529+
var found = FindPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
530+
if (found.Location != null)
531+
return found;
532+
}
533+
}
507534

508-
case BuildingType.Building:
509-
return FindPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius,
510-
distanceToBaseIsImportant ? baseBuilder.Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange);
535+
// Try and find a free spot somewhere else in the base
536+
var fallback = FindPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
537+
if (fallback.Location != null)
538+
return fallback;
539+
break;
540+
541+
case BuildingType.Building:
542+
var building = FindPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius,
543+
distanceToBaseIsImportant ? baseBuilder.Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange);
544+
if (building.Location != null)
545+
return building;
546+
break;
547+
}
511548
}
512549

513550
// Can't find a build location

OpenRA.Mods.Common/Traits/BotModules/McvManagerBotModule.cs

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ public class McvManagerBotModuleInfo : ConditionalTraitInfo
4444
[Desc("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")]
4545
public readonly bool RestrictMCVDeploymentFallbackToBase = true;
4646

47+
[Desc("Cash threshold to trigger expansion (building a new MCV).")]
48+
public readonly int ExpansionCashThreshold = 1000;
49+
50+
[Desc("Maximum number of bases (construction yards) allowed.")]
51+
public readonly int MaxBaseCount = 2;
52+
53+
[Desc("Minimum distance from existing bases for a new expansion.")]
54+
public readonly int MinimumExpansionDistance = 30;
55+
56+
[Desc("Maximum distance from existing bases for a new expansion.")]
57+
public readonly int MaximumExpansionDistance = 50;
58+
4759
public override object Create(ActorInitializer init) { return new McvManagerBotModule(init.Self, this); }
4860
}
4961

@@ -66,6 +78,9 @@ public CPos GetRandomBaseCenter()
6678

6779
IBotPositionsUpdated[] notifyPositionsUpdated;
6880
IBotRequestUnitProduction[] requestUnitProduction;
81+
PlayerResources playerResources;
82+
IResourceLayer resourceLayer;
83+
TechTree techTree;
6984

7085
CPos initialBaseCenter;
7186
int scanInterval;
@@ -85,6 +100,9 @@ protected override void Created(Actor self)
85100
{
86101
notifyPositionsUpdated = self.Owner.PlayerActor.TraitsImplementing<IBotPositionsUpdated>().ToArray();
87102
requestUnitProduction = self.Owner.PlayerActor.TraitsImplementing<IBotRequestUnitProduction>().ToArray();
103+
playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
104+
resourceLayer = self.World.WorldActor.TraitOrDefault<IResourceLayer>();
105+
techTree = self.Owner.PlayerActor.TraitOrDefault<TechTree>();
88106
}
89107

90108
protected override void TraitEnabled(Actor self)
@@ -131,9 +149,36 @@ bool ShouldBuildMCV()
131149
if (!allowedToBuildMCV)
132150
return false;
133151

134-
// Build MCV if we don't have the desired number of construction yards, unless we have no factory (can't build it).
135-
return AIUtils.CountActorByCommonName(constructionYards) < Info.MinimumConstructionYardCount &&
136-
AIUtils.CountActorByCommonName(mcvFactories) > 0;
152+
var constructionYardCount = AIUtils.CountActorByCommonName(constructionYards);
153+
154+
// Recovery Mode: Build MCV if we don't have any construction yards (and we have a factory to build it).
155+
if (constructionYardCount < Info.MinimumConstructionYardCount && AIUtils.CountActorByCommonName(mcvFactories) > 0)
156+
return true;
157+
158+
// Expansion Mode: Build MCV if we have excess cash and haven't reached max base count.
159+
var activeMcvs = AIUtils.CountActorByCommonName(mcvs);
160+
161+
// Count MCVs currently in production queue (to avoid double ordering)
162+
var mcvsInProduction = world.ActorsWithTrait<ProductionQueue>()
163+
.Where(a => a.Actor.Owner == player)
164+
.SelectMany(a => a.Trait.AllQueued())
165+
.Count(i => Info.McvTypes.Contains(i.Item));
166+
167+
if (constructionYardCount + activeMcvs + mcvsInProduction < Info.MaxBaseCount)
168+
{
169+
// Check if we can build any MCV (prerequisites met)
170+
foreach (var mcvType in Info.McvTypes)
171+
{
172+
if (!world.Map.Rules.Actors.TryGetValue(mcvType, out var actorInfo))
173+
continue;
174+
175+
var buildable = actorInfo.TraitInfoOrDefault<BuildableInfo>();
176+
if (buildable != null && techTree != null && techTree.HasPrerequisites(buildable.Prerequisites))
177+
return true;
178+
}
179+
}
180+
181+
return false;
137182
}
138183

139184
void DeployMcvs(IBot bot, bool chooseLocation)
@@ -151,12 +196,16 @@ void DeployMcv(IBot bot, Actor mcv, bool move)
151196
if (move)
152197
{
153198
// If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base!
199+
var baseCount = AIUtils.CountActorByCommonName(constructionYards);
154200
var restrictToBase =
155201
Info.RestrictMCVDeploymentFallbackToBase &&
156-
AIUtils.CountActorByCommonName(constructionYards) > 0;
202+
baseCount > 0;
203+
204+
// If we are expanding (have at least one base), we should look for a spot further away
205+
var isExpansion = baseCount > 0;
157206

158207
var transformsInfo = mcv.Info.TraitInfo<TransformsInfo>();
159-
var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset, restrictToBase);
208+
var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset, restrictToBase, isExpansion);
160209
if (desiredLocation == null)
161210
return;
162211

@@ -175,7 +224,7 @@ void DeployMcv(IBot bot, Actor mcv, bool move)
175224
bot.QueueOrder(new Order("DeployTransform", mcv, true));
176225
}
177226

178-
CPos? ChooseMcvDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant)
227+
CPos? ChooseMcvDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant, bool isExpansion)
179228
{
180229
var actorInfo = world.Map.Rules.Actors[actorType];
181230
var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>();
@@ -201,9 +250,36 @@ void DeployMcv(IBot bot, Actor mcv, bool move)
201250
}
202251

203252
var baseCenter = GetRandomBaseCenter();
253+
var targetCenter = baseCenter;
254+
var minRange = Info.MinBaseRadius;
255+
var maxRange = distanceToBaseIsImportant ? Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange;
256+
257+
if (isExpansion && resourceLayer != null)
258+
{
259+
// Find a resource patch that is far enough from existing bases
260+
var existingBases = constructionYards.Actors.Select(a => a.Location).ToList();
261+
var maxSearchRadius = System.Math.Min(Info.MaximumExpansionDistance, world.Map.Grid.MaximumTileSearchRange);
262+
var minSearchRadius = System.Math.Min(Info.MinimumExpansionDistance, maxSearchRadius);
263+
264+
var potentialResourceTiles = world.Map.FindTilesInAnnulus(baseCenter, minSearchRadius, maxSearchRadius)
265+
.Where(c => resourceLayer.GetResource(c).Type != null)
266+
.Shuffle(world.LocalRandom)
267+
.Take(20);
268+
269+
foreach (var tile in potentialResourceTiles)
270+
{
271+
// Ensure this tile is far from ALL existing bases
272+
if (existingBases.All(baseLoc => (tile - baseLoc).LengthSquared >= Info.MinimumExpansionDistance * Info.MinimumExpansionDistance))
273+
{
274+
targetCenter = tile;
275+
minRange = 2; // Close to resources
276+
maxRange = 10;
277+
break;
278+
}
279+
}
280+
}
204281

205-
return FindPos(baseCenter, baseCenter, Info.MinBaseRadius,
206-
distanceToBaseIsImportant ? Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange);
282+
return FindPos(targetCenter, targetCenter, minRange, maxRange);
207283
}
208284

209285
List<MiniYamlNode> IGameSaveTraitData.IssueTraitData(Actor self)

0 commit comments

Comments
 (0)